I write more and more Java 8 code. But within the last week I wondered if streams and lambdas may lead to code that nobody understands (like generics back in the days). But I’ll give you a few examples and my opinion about it.
Messy Code because of Streams and Lambdas?
I wrote some code like this:
items.stream() .filter(item -> mapper.hasMappingFor(item)) .map(item -> mapper.map(item)) .findFirst();
Wouldn’t it be more readable for other developers seeing the code for the first time, if I write it like that?
for(Item item : items) { if (mapper.hasMappingFor(item)) return mapper.map(item); }
Or does it just feel that way because I see these for-loops every day? In general, I really like fluent APIs and for me the stream-solution seems to be very elegant. Are developers complaining about Java 8 code because they are just old-fashioned? Or is it really harder to understand and maybe also harder to maintain?
Elegance of Streams and Lambdas
Last week I attend at one of our internal coding events and we worked on the yahtzee kata. I had to leave early but because I had a lot of ideas, I worked on that kata on the train, focusing on Java 8 features and check if the code feels clean. The problem I was solving: get all possible yahtzee categories for a given roll. I solved that with an enumeration of categories, each defining an predicated against a given roll is tested.
public Set<Category> possibleCategories() { return Stream.of(Category.values()) .filter(category -> category.isPossibleCategoryFor(dice)) .collect(Collectors.toSet()); }
And here the definition of the single categories and the check if a dice matches the category:
enum Category { ONES(hasAtLeastOne(ONE)), TWOS(hasAtLeastOne(TWO)), THREES(hasAtLeastOne(THREE)), FOURS(hasAtLeastOne(FOUR)), FIVES(hasAtLeastOne(FIVE)), SIXES(hasAtLeastOne(SIX)), THREE_OF_A_KIND(atLeastOneDieOccursAtLeastXTimes(3)), FOUR_OF_A_KIND(atLeastOneDieOccursAtLeastXTimes(4)), FULL_HOUSE(atLeastOneDieOccursAtLeastXTimes(3) .and(anotherDieOccursAtLeastYTimes(2))), SMALL_STRAIGHT(dicesInARow(4)), LARGE_STRAIGHT(dicesInARow(5)), YAHTZEE(allDiceAreEqual()), CHANCE(matchesForEveryDice()); private Predicate<List<Dice>> rule; private Category(Predicate<List<Dice>> rule) { this.rule = rule; } public boolean isPossibleCategoryFor(Dice dice) { return rule.test(dice); } ... }
Of course you could implement something like this with earlier Java versions as well. Only the method implementations are different.
Here I struggled again, when I wrote the method creating possible straights:
private static Set<Set<Dice>> possibleStraights(int numberOfDice) { return IntStream.range(0, Dice.values().length - numberOfDice) .mapToObj(startDice -> IntStream.range(startDice, startDice + numberOfDice) .mapToObj(die -> Dice.values()[die]).collect(Collectors.toSet())) .collect(Collectors.toSet()); }
I fear everyone of you thought “WTF?” at the first sight. Although I chose an self-explanatory name (at least I hope so), it’s not clear, what the code does. You may hope, that it really creates a set of possible straights. But after I extract a method it seems more readable again.
private static Set<Set<Dice>> possibleStraights(int numberOfDice) { return IntStream.range(0, Dice.values().length - numberOfDice) .mapToObj(startDice -> createStraightStartingAt(startDice, numberOfDice)) .collect(Collectors.toSet()); } private static Set<Dice> createStraightStartingAt(int startDice, int numberOfDice) { return IntStream.range(startDice, startDice + numberOfDice) .mapToObj(die -> Dice.values()[die]).collect(Collectors.toSet()); }
You can find the code on GitHub.
Conclusion
I really think my feeling that the Java 8 code is less readable and maintainable, is just because these Java syntax is quite new. (And maybe I don’t trust other developers, that they are familiar with these new features.)
I am quite sure, that the code is as easy to read and maintain as the “old-fashioned” java style. And in a few month it will be completely common. But we should always think twice, if streams and lambdas are the best possible solution for our problem. Just because we have a hammer, not everything is a nail.
But what do you think? Do you think the code is less cleaner because of the Java 8 syntax?