正如我在本系列的前两篇文章中写到的,我喜欢通过用不同的语言编写小程序来解决小问题,这样我可以比较它们解决问题的不同方法。我在本系列中使用的例子是将大宗物资分成价值相近的食品篮,分发给社区中struggling的邻居,您可以在本系列的第一篇文章中阅读相关内容。
在第一篇文章中,我使用 Groovy 编程语言解决了这个问题,它在很多方面都像 Python,但在语法上更像 C 和 Java。在第二篇文章中,我用 Python 解决了这个问题,采用了非常相似的设计和努力,这证明了这两种语言之间的相似之处。
现在我将尝试用 Java 来解决。
Java 解决方案
在使用 Java 时,我发现自己会声明实用程序类来保存数据元组(新的 record 功能将非常适合这一点),而不是使用 Groovy 和 Python 中提供的语言对 map 的支持。这是因为 Java 鼓励创建将一种特定类型映射到另一种特定类型的 map,但在 Groovy 或 Python 中,拥有一个具有混合类型键和混合类型值的 map 是很酷的。
首要任务是定义这些实用程序类,第一个是 Unit
类
class Unit {
private String item, brand;
private int price;
public Unit(String item, String brand, int price) {
this.item = item;
this.brand = brand;
this.price = price;
}
public String getItem() { return this.item; }
public String getBrand() { return this.brand; }
public int getPrice() { return this.price; }
@Override
public String toString() { return String.format("item: %s brand: %s price: %d",item,brand,price); }
}
这里没有什么太令人惊讶的东西。我有效地创建了一个类,其实例是不可变的,因为字段 item
、brand
或 price
没有 setter,并且它们被声明为 private
。作为一般规则,除非我要改变它,否则我看不到创建可变数据结构的价值;在这个应用程序中,我看不到改变 Unit
类的任何价值。
虽然创建这些实用程序类需要更多的工作,但创建它们比仅仅使用 map 更鼓励一些设计工作,这可能是一件好事。在这种情况下,我意识到大宗包装是由许多单独的单元组成的,所以我创建了 Pack
类
class Pack {
private Unit unit;
private int count;
public Pack(String item, String brand, int unitCount, int packPrice) {
this.unit = new Unit(item, brand, unitCount > 0 ? packPrice / unitCount : 0);
this.count = unitCount;
}
public String getItem() { return unit.getItem(); }
public String getBrand() { return unit.getBrand(); }
public int getUnitPrice() { return unit.getPrice(); }
public int getUnitCount() { return count; }
public List<Unit> unpack() { return Collections.nCopies(count, unit); }
@Override
public String toString() { return String.format("item: %s brand: %s unitCount: %d unitPrice: %d",unit.getItem(),unit.getBrand(),count,unit.getPrice()); }
}
与 Unit
类类似,Pack
类也是不可变的。这里有几点值得一提
- 我可以将
Unit
实例传递到Pack
构造函数中。我选择不这样做,因为大宗包装的捆绑物理性质促使我将“单元性”视为一个内部事物,从外部不可见,但需要拆包才能暴露单元。在这种情况下,这是一个重要的决定吗?可能不是,但至少对我来说,总是要仔细考虑这种考虑。 - 这就引出了
unpack()
方法。Pack
类仅在您调用此方法时才创建Unit
实例列表——也就是说,该类是惰性的。作为一般的设计原则,我发现值得决定一个类的行为应该是 eager 还是 lazy,当它看起来无关紧要时,我选择 lazy。在这种情况下,这是一个重要的决定吗?也许——这种惰性设计使得可以在每次调用unpack()
时生成一个新的Unit
实例列表,这可能在未来被证明是一件好事。无论如何,养成始终思考 eager 与 lazy 行为的习惯是一个好习惯。
眼尖的读者会注意到,与 Groovy 和 Python 示例中我主要关注紧凑代码,并且在设计决策上花费的时间少得多不同,在这里,我将 Pack
的定义与购买的 Pack
实例的数量分开了。同样,从设计的角度来看,这似乎是一个好主意,因为 Pack
在概念上与获得的 Pack
实例的数量完全独立。
鉴于此,我需要再添加一个实用程序类:Bought
类
class Bought {
private Pack pack;
private int count;
public Bought(Pack pack, int packCount) {
this.pack = pack;
this.count = packCount;
}
public String getItem() { return pack.getItem(); }
public String getBrand() { return pack.getBrand(); }
public int getUnitPrice() { return pack.getUnitPrice(); }
public int getUnitCount() { return pack.getUnitCount() * count; }
public List<Unit> unpack() { return Collections.nCopies(count, pack.unpack()).stream().flatMap(List::stream).collect(Collectors.toList()); }
@Override
public String toString() { return String.format("item: %s brand: %s bought: %d pack(s) totalUnitCount: %d unitPrice: %d",pack.getItem(),pack.getBrand(),count,pack.getUnitCount() * count,pack.getUnitPrice()); }
}
值得注意的是
- 我决定将
Pack
传递到构造函数中。为什么?因为在我看来,购买的大宗包装的物理结构是外部的,而不是内部的,就像单个大宗包装的情况一样。再一次,这在这个应用程序中可能并不重要,但我相信总是要考虑这些事情是好的。如果非要说有什么不同的话,请注意我并没有固守对称性! unpack()
方法再次证明了惰性设计原则。这需要付出更多努力来生成Unit
实例列表(而不是Unit
实例的列表的列表,这将更容易,但需要在代码中进一步展平)。
好的!是时候继续前进并解决问题了。首先,声明购买的包装
var packs = new Bought[] {
new Bought(new Pack("Rice","Best Family",10,5650),1),
new Bought(new Pack("Spaghetti","Best Family",1,327),10),
new Bought(new Pack("Sardines","Fresh Caught",3,2727),3),
new Bought(new Pack("Chickpeas","Southern Style",2,2600),5),
new Bought(new Pack("Lentils","Southern Style",2,2378),5),
new Bought(new Pack("Vegetable oil","Crafco",12,10020),1),
new Bought(new Pack("UHT milk","Atlantic",6,4560),2),
new Bought(new Pack("Flour","Neighbor Mills",10,5200),1),
new Bought(new Pack("Tomato sauce","Best Family",1,190),10),
new Bought(new Pack("Sugar","Good Price",1,565),10),
new Bought(new Pack("Tea","Superior",5,2720),2),
new Bought(new Pack("Coffee","Colombia Select",2,4180),5),
new Bought(new Pack("Tofu","Gourmet Choice",1,1580),10),
new Bought(new Pack("Bleach","Blanchite",5,3550),2),
new Bought(new Pack("Soap","Sunny Day",6,1794),2)
};
从可读性的角度来看,这非常好:这里有一包 Best Family Rice,包含 10 个单元,成本为 5,650(使用那些疯狂的货币单位,就像在其他示例中一样)。很容易看出,除了 10 袋大米的大宗包装外,该组织还购买了 10 包每包一袋的意大利面。实用程序类在幕后做了一些工作,但这在这一点上并不重要,因为设计工作非常出色!
请注意,这里使用了 var
关键字;它是最新 Java 版本中的一个不错的特性,通过让编译器从右侧表达式的类型推断变量的数据类型,帮助使该语言不那么冗长(该原则称为 DRY—不要重复自己)。这看起来有点类似于 Groovy 的 def
关键字,但由于 Groovy 默认是动态类型的,而 Java 是静态类型的,因此 Java 中由 var
推断的类型信息在变量的整个生命周期内都持续存在。
最后,值得一提的是,这里的 packs
是一个数组,而不是 List
实例。如果您要从单独的文件中读取此数据,您可能更喜欢将其创建为列表。
接下来,拆开大宗包装。由于 Pack
实例的拆包被委托到 Unit
实例列表中,您可以像这样使用它
var units = Stream.of(packs)
.flatMap(bought -> {
return bought.unpack().stream(); })
.collect(Collectors.toList());
这使用了一些在较新 Java 版本中引入的不错的函数式编程特性。将先前声明的数组 packs
转换为 Java 流,将 flatmap()
与 lambda 一起使用以展平 Bought
类的 unpack()
方法生成的单元子列表,并将生成的流元素收集回列表中。
与 Groovy 和 Java 解决方案一样,最后一步是将单元重新包装到食品篮中以进行分发。这是代码——它并不比 Groovy 版本更冗长(令人厌烦的分号除外),也没有什么太大的不同
var valueIdeal = 5000;
var valueMax = Math.round(valueIdeal * 1.1);
var rnd = new Random();
var hamperNumber = 0; // [1]
while (units.size() > 0) { // [2]
hamperNumber++;
var hamper = new ArrayList<Unit>();
var value = 0; // [2.1]
for (boolean canAdd = true; canAdd; ) { // [2.2]
var u = rnd.nextInt(units.size()); // [2.2.1]
canAdd = false; // [2.2.2]
for (int o = 0; o < units.size(); o++) { // [2.2.3]
var uo = (u + o) % units.size();
var unit = units.get(uo); // [2.2.3.1]
if (units.size() < 3 ||
!hamper.contains(unit) &&
(value + unit.getPrice()) < valueMax) { // [2.2.3.2]
hamper.add(unit);
value += unit.getPrice();
units.remove(uo); // [2.2.3.3]
canAdd = units.size() > 0;
break; // [2.2.3.4]
}
}
} // [2.2.4]
System.out.println();
System.out.printf("Hamper %d value %d:\n",hamperNumber,value);
hamper.forEach(unit -> {
System.out.printf("%-25s%-25s%7d\n", unit.getItem(), unit.getBrand(),
unit.getPrice());
}); // [2.3]
System.out.printf("Remaining units %d\n",units.size()); // [2.4]
一些说明,上面注释中的括号中的数字(例如,[1])对应于下面的说明
- 1. 设置要加载到任何给定食品篮中的理想值和最大值,初始化 Java 的随机数生成器和食品篮编号。
- 2. 只要有更多可用单元,此
while {}
循环就会将单元重新分配到食品篮中
- 2.1 递增食品篮编号,获取一个新的空食品篮(
Unit
实例列表),并将其值设置为 0。 - 2.2 此
for {}
循环将尽可能多地向食品篮添加单元
- 2.2.1 获取零到剩余单元数减 1 之间的随机数。
- 2.2.2 假设您找不到更多要添加的单元。
- 2.2.3 此
for {}
循环,从随机选择的索引开始,将尝试查找可以添加到食品篮中的单元。
- 2.2.3.1 找出要查看哪个单元。
- 2.2.3.2 如果只剩下几个单元,或者如果食品篮的价值在添加单元后不会太高并且该单元尚未在食品篮中,则将此单元添加到食品篮中。
- 2.2.3.3 将单元添加到食品篮中,将食品篮价值增加单元价格,并从可用单元列表中删除该单元。
- 2.2.3.4 只要还有单元剩余,您就可以添加更多,因此跳出此循环以继续查找。
- 2.2.4 在退出此
for {}
循环时,如果您检查了每个剩余单元但找不到可以添加到食品篮中的单元,则食品篮已完成;否则,您找到了一个,可以继续寻找更多。
- 2.3 打印出食品篮的内容。
- 2.4 打印出剩余单元信息。
- 2.1 递增食品篮编号,获取一个新的空食品篮(
当您运行此代码时,输出看起来与 Groovy 和 Python 程序的输出非常相似
Hamper 1 value 5465:
Tofu Gourmet Choice 1580
Bleach Blanchite 710
Coffee Colombia Select 2090
Flour Neighbor Mills 520
Sugar Good Price 565
Remaining units 150
Hamper 2 value 5482:
Sardines Fresh Caught 909
Tomato sauce Best Family 190
Vegetable oil Crafco 835
UHT milk Atlantic 760
Chickpeas Southern Style 1300
Lentils Southern Style 1189
Soap Sunny Day 299
Remaining units 143
Hamper 3 value 5353:
Soap Sunny Day 299
Rice Best Family 565
UHT milk Atlantic 760
Flour Neighbor Mills 520
Vegetable oil Crafco 835
Bleach Blanchite 710
Tomato sauce Best Family 190
Sardines Fresh Caught 909
Sugar Good Price 565
Remaining units 134
…
Hamper 23 value 5125:
Sardines Fresh Caught 909
Rice Best Family 565
Spaghetti Best Family 327
Lentils Southern Style 1189
Chickpeas Southern Style 1300
Vegetable oil Crafco 835
Remaining units 4
Hamper 24 value 2466:
UHT milk Atlantic 760
Spaghetti Best Family 327
Vegetable oil Crafco 835
Tea Superior 544
Remaining units 0
最后一个食品篮的内容和价值被缩写了。
结束语
“工作代码”与 Groovy 原版的相似之处显而易见——Groovy 和 Java 之间的密切关系显而易见。Groovy 和 Java 在一些方面有所不同,这些方面是在 Groovy 发布后添加到 Java 中的,例如 var
与 def
关键字,以及 Groovy 闭包和 Java lambda 之间表面上的相似之处和差异。此外,整个 Java 流框架为 Java 平台增加了极大的功能和表达能力(完全公开,以防它不明显——我只是 Java 流领域的新手)。
Java 将 map 用于将单一类型的实例映射到另一种单一类型的实例的意图促使您使用实用程序类或元组,而不是 Groovy map(它们基本上只是 Map<Object,Object>
加上大量的语法糖,以消除您在 Java 中会创建的那种类型转换和 instanceof
麻烦)或 Python 中更灵活的固有意图。这样做的好处是有机会对这些实用程序类应用一些真正的设计工作,这至少在程序员中灌输良好的习惯方面是有回报的。
除了实用程序类之外,与 Groovy 代码相比,Java 代码中没有太多额外的仪式或样板代码。好吧,除了您需要添加一堆导入并将“工作代码”包装在一个类定义中,这可能看起来像这样
import java.lang.*;
import java.util.*;
import java.util.Collections.*;
import java.util.stream.*;
import java.util.stream.Collectors.*;
import java.util.Random.*;
public class Distribute {
static public void main(String[] args) {
// the working code shown above
}
}
class Unit { … }
class Pack { … }
class Bought { … }
在 Java 中,与在 Groovy 和 Python 中一样,当涉及到从 Unit
实例列表中抓取食品篮的东西时,需要相同的繁琐之处,包括随机数、循环遍历剩余单元等等。
另一个值得一提的问题——这不是一种特别有效的方法。从 ArrayLists
中删除元素、对重复表达式的粗心大意以及其他一些事情使得这不太适合解决大型重新分配问题。我在这里更加小心地坚持使用整数数据。但至少它执行起来非常快。
是的,我仍然在使用可怕的 while { … }
和 for { … }
。我仍然没有想到一种将 map 和 reduce 风格的流处理与随机选择单元进行重新包装结合使用的方法。你能想到吗?
4 条评论