正如我在本系列的前两篇文章中写到的那样,我喜欢通过用不同的语言编写小程序来解决小问题,这样我可以比较它们解决问题的不同方法。我在本系列中使用的例子是将大宗物资分成价值相近的食品篮,分发给你所在社区的困难邻居,你可以在本系列的第一篇文章中阅读。
在第一篇文章中,我使用 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
实例列表——也就是说,该类是惰性的。作为一般设计原则,我发现值得决定一个类的行为应该是急切的还是惰性的,当它似乎无关紧要时,我选择惰性的。在这种情况下,这是一个重要的决定吗?也许——这种惰性设计使得每次调用unpack()
时都可以生成一个新的Unit
实例列表,这可能在将来被证明是一件好事。无论如何,养成始终思考急切与惰性行为的习惯是一个好习惯。
眼尖的读者会注意到,与 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(使用那些疯狂的货币单位,就像其他示例中一样)。很容易看出,除了 1 包 10 袋装的大米外,该组织还购买了 10 包 1 袋装的意大利面。实用程序类在幕后做了一些工作,但这在这一点上并不重要,因为设计工作很棒!
请注意,这里使用了 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 条评论