sources/tech/20200921 Solve a real-world problem using Java.md
21 KiB
Solve a real-world problem using Java
See how Java differs from Python and Groovy as it's used to solve a charity's real-world problem.
As I wrote in the first two articles in this series, I enjoy solving small problems by writing small programs in different languages, so I can compare the different ways they approach the solution. The example I'm using in this series is dividing bulk supplies into hampers of similar value to distribute to struggling neighbors in your community, which you can read about in the first article in this series.
In the first article, I solved this problem using the Groovy programming language, which is like Python in many ways, but syntactically it's more like C and Java. In the second article, I solved it in Python with a very similar design and effort, which demonstrates the resemblance between the languages.
Now I'll try it in Java.
The Java solution
When working in Java, I find myself declaring utility classes to hold tuples of data (the new record feature is going to be great for that), rather than using the language support for maps offered in Groovy and Python. This is because Java encourages creating maps that map one specific type to another specific type, but in Groovy or Python, it's cool to have a map with mixed-type keys and mixed-type values.
The first task is to define these utility classes, and the first is the Unit
class:
class Unit {
private [String][5] item, brand;
private int price;
public Unit([String][5] item, [String][5] brand, int price) {
this.item = item;
this.brand = brand;
this.price = price;
}
public [String][5] getItem() { return this.item; }
public [String][5] getBrand() { return this.brand; }
public int getPrice() { return this.price; }
@Override
public [String][5] toString() { return [String][5].format("item: %s brand: %s price: %d",item,brand,price); }
}
There's nothing too startling here. I effectively created a class whose instances are immutable since there are no setters for fields item
, brand
, or price
and they are declared private
. As a general rule, I don't see value in creating a mutable data structure unless I'm going to mutate it; and in this application, I don't see any value in mutating the Unit
class.
While more effort is required to create these utility classes, creating them encourages a bit more design effort than just using a map, which can be a good thing. In this case, I realized that a bulk package is composed of a number of individual units, so I created the Pack
class:
class Pack {
private Unit unit;
private int count;
public Pack([String][5] item, [String][5] brand, int unitCount, int packPrice) {
this.unit = new Unit(item, brand, unitCount > 0 ? packPrice / unitCount : 0);
this.count = unitCount;
}
public [String][5] getItem() { return unit.getItem(); }
public [String][5] getBrand() { return unit.getBrand(); }
public int getUnitPrice() { return unit.getPrice(); }
public int getUnitCount() { return count; }
public List<Unit> unpack() { return [Collections][6].nCopies(count, unit); }
@Override
public [String][5] toString() { return [String][5].format("item: %s brand: %s unitCount: %d unitPrice: %d",unit.getItem(),unit.getBrand(),count,unit.getPrice()); }
}
Similar to the Unit
class, the Pack
class is immutable. A couple of things worth mentioning here:
- I could have passed a
Unit
instance into thePack
constructor. I chose not to because the bundled, physical nature of a bulk package encouraged me to think of the "unit-ness" as an internal thing not visible from the outside but that requires unpacking to expose the units. Is this an important decision in this case? Probably not, but to me, at least, it's always good to think through this kind of consideration. - Which leads to the
unpack()
method. ThePack
class creates the list ofUnit
instances only when you call this method—that is, the class is lazy. As a general design principle, I've found it's worthwhile to decide whether a class' behavior should be eager or lazy, and when it doesn't seem to matter, I go with lazy. Is this an important decision in this case? Maybe—this lazy design enables a new list ofUnit
instances to be generated on every call ofunpack()
, which could prove to be a good thing down the road. In any case, getting in the habit of always thinking about eager vs. lazy behavior is a good habit.
The sharp-eyed reader will note that, unlike in the Groovy and Python examples where I was mostly focused on compact code and spent way less time thinking about design decisions, here, I separated the definition of a Pack
from the number of Pack
instances purchased. Again, from a design perspective, this seemed like a good idea as the Pack
is conceptually quite independent of the number of Pack
instances acquired.
Given this, I need one more utility class: the Bought
class:
class Bought {
private Pack pack;
private int count;
public Bought(Pack pack, int packCount) {
this.pack = pack;
this.count = packCount;
}
public [String][5] getItem() { return pack.getItem(); }
public [String][5] getBrand() { return pack.getBrand(); }
public int getUnitPrice() { return pack.getUnitPrice(); }
public int getUnitCount() { return pack.getUnitCount() * count; }
public List<Unit> unpack() { return [Collections][6].nCopies(count, pack.unpack()).stream().flatMap([List][7]::stream).collect(Collectors.toList()); }
@Override
public [String][5] toString() { return [String][5].format("item: %s brand: %s bought: %d pack(s) totalUnitCount: %d unitPrice: %d",pack.getItem(),pack.getBrand(),count,pack.getUnitCount() * count,pack.getUnitPrice()); }
}
Notably:
- I decided to pass a
Pack
into the constructor. Why? Because to my way of thinking, the physical structure of the purchased bulk packages is external, not internal, as in the case of the individual bulk packages. Once again, it may not be important in this application, but I believe it's always good to think about these things. If nothing else, note that I am not married to symmetry! - Once again the
unpack()
method demonstrates the lazy design principle. This goes to more effort to generate a list ofUnit
instances (rather than a list of lists ofUnit
instances, which would be easier but require flattening further out in the code).
OK! Time to move on and solve the problem. First, declare the purchased packs:
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)
};
This is pretty nice from a readability perspective: there is one pack of Best Family Rice containing 10 units that cost 5,650 (using those crazy monetary units, like in the other examples). It's straightforward to see that in addition to the one bulk pack of 10 bags of rice, the organization acquired 10 bulk packs of one bag each of spaghetti. The utility classes are doing some work under the covers, but that's not important at this point because of the great design job!
Note the var
keyword is used here; it's one of the nice features in recent versions of Java that help make the language a bit less verbose (the principle is called DRY—don't repeat yourself) by letting the compiler infer the variable's data type from the right-side expression's type. This looks kind of similar to the Groovy def
keyword, but since Groovy by default is dynamically typed and Java is statically typed, the typing information inferred in Java by var
persists throughout the lifetime of that variable.
Finally, it's worth mentioning that packs
here is an array and not a List
instance. If you were reading this data from a separate file, you would probably prefer to create it as a list.
Next, unpack the bulk packages. Because the unpacking of Pack
instances is delegated into lists of Unit
instances, you can use that like this:
var units = Stream.of(packs)
.flatMap(bought -> {
return bought.unpack().stream(); })
.collect(Collectors.toList());
This uses some of the nice functional programming features introduced in later Java versions. Convert the array packs
declared previously to a Java stream, use flatmap()
with a lambda to flatten the sublists of units generated by the unpack()
method of the Bought
class, and collect the resulting stream elements back into a list.
As in the Groovy and Java solutions, the final step is repacking the units into the hampers for distribution. Here's the code—it's not much wordier than the Groovy version (tiresome semicolons aside) nor really all that different:
var valueIdeal = 5000;
var valueMax = [Math][8].round(valueIdeal * 1.1);
var rnd = new [Random][9]();
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][10].out.println();
[System][10].out.printf("Hamper %d value %d:\n",hamperNumber,value);
hamper.forEach(unit -> {
[System][10].out.printf("%-25s%-25s%7d\n", unit.getItem(), unit.getBrand(),
unit.getPrice());
}); // [2.3]
[System][10].out.printf("Remaining units %d\n",units.size()); // [2.4]
Some clarification, with numbers in brackets in the comments above (e.g., 1) corresponding to the clarifications below:
- 1. Set up the ideal and maximum values to be loaded into any given hamper, initialize Java's random number generator and the hamper number.
- 2. This
while {}
loop will redistribute units into hampers as long as there are more available:- 2.1 Increment the hamper number, get a new empty hamper (a list of
Unit
instances), and set its value to 0. - 2.2 This
for {}
loop will add as many units to the hamper as possible:- 2.2.1 Get a random number between zero and the number of remaining units minus 1.
- 2.2.2 Assume you can't find more units to add.
- 2.2.3 This
for {}
loop, starting at the randomly chosen index, will try to find a unit that can be added to the hamper.- 2.2.3.1 Figure out which unit to look at.
- 2.2.3.2 Add this unit to the hamper if there are only a few left or if the value of the hamper isn't too high once the unit is added and that unit isn't already in the hamper.
- 2.2.3.3 Add the unit to the hamper, increment the hamper value by the unit price, and remove the unit from the available units list.
- 2.2.3.4 As long as there are units left, you can add more, so break out of this loop to keep looking.
- 2.2.4 On exit from this
for {}
loop, if you inspected every remaining unit and could not find one to add to the hamper, the hamper is complete; otherwise, you found one and can continue looking for more.
- 2.3 Print out the contents of the hamper.
- 2.4 Print out the remaining units info.
- 2.1 Increment the hamper number, get a new empty hamper (a list of
When you run this code, the output looks quite similar to the output from the Groovy and Python programs:
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
The last hamper is abbreviated in contents and value.
Closing thoughts
The similarities in the "working code" with the Groovy original are obvious—the close relationship between Groovy and Java is evident. Groovy and Java diverged in a few ways in things that were added to Java after Groovy was released, such as the var
vs. def
keywords and the superficial similarities and differences between Groovy closures and Java lambdas. Moreover, the whole Java streams framework adds a great deal of power and expressiveness to the Java platform (full disclosure, in case it's not obvious—I am but a babe in the Java streams woods).
Java's intent to use maps for mapping instances of a single type to instances of another single type pushes you to use utility classes, or tuples, instead of the more inherently flexible intents in Groovy maps (which are basically just Map<Object,Object>
plus a lot of syntactic sugar to vanish the kinds of casting and instanceof
hassles that you would create in Java) or in Python. The bonus from this is the opportunity to apply some real design effort to these utility classes, which pays off at least insofar as it instills good habits in the programmer.
Aside from the utility classes, there isn't a lot of additional ceremony nor boilerplate in the Java code compared to the Groovy code. Well, except that you need to add a bunch of imports and wrap the "working code" in a class definition, which might look like this:
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][5][] args) {
// the working code shown above
}
}
class Unit { … }
class Pack { … }
class Bought { … }
The same fiddly bits are necessary in Java as they are in Groovy and Python when it comes to grabbing stuff out of the list of Unit
instances for the hampers, involving random numbers, loops through remaining units, etc.
Another issue worth mentioning—this isn't a particularly efficient approach. Removing elements from ArrayLists
, being careless about repeated expressions, and a few other things make this less suitable for a huge redistribution problem. I've been a bit more careful here to stick with integer data. But at least it's quite quick to execute.
Yes, I'm still using the dreaded while { … }
and for { … }
. I still haven't thought of a way to use map and reduce style stream processing in conjunction with a random selection of units for repackaging. Can you?
Stay tuned for the next articles in this series, with versions in Julia and Go.
via: https://opensource.com/article/20/9/problem-solving-java
作者:Chris Hermansen 选题:lujun9972 译者:译者ID 校对:校对者ID