toList() vs collect(Collectors.toList())

I had some extra time this week so went through a bunch of Sonar findings. One was interesting – in Java 17 you can use .toList() instead of .collect(Collectors.toList()) on a stream.

[Yes, I know this was introduced in Java 16. I live in a world where only LTS releases matter]

Cool. I can fix a lot of these without thinking. It’s a search and replace on the project level after all. I then ran the JUnit regression tests and got failures. That was puzzling to me because I’ve been using .toList() in code I write for a good while without incident.

After looking into it, I found the problem. .toList() guarantees the returned List is immutable. However, Collectors.toList() makes no promises about immutability. The result might be immutable. Or you can change it freely. Surprise?

That’s according to the spec. On the JDK I’m using (and Jenkins is using), Collectors.toList() was returning an ArrayList. So people were treating the returned List as mutable and it was working. I added a bunch of “let’s make this explicitly mutable” and then I was able to commit.

Here’s an example that illustrates the diference

import java.util.*;
import java.util.stream.*;

public class PlayTest {

	public static void main(String[] args) {

		var list = List.of("a", "b", "c");
		var collectorReturned = collector(list);
		var toListReturned = toList(list);
		
		System.out.println(collectorReturned.getClass());  // ArrayList (but doesn't have to be)
		System.out.println(toListReturned.getClass());  // class java.util.ImmutableCollections$ListN
		
		collectorReturned.add("x");
		System.out.println(collectorReturned);  // [bb, cc, x]
		toListReturned.add("x");  // throws UnsupportedOperationException

	}

	private static List<String> toList(List<String> list) {
		return list.stream()
				.filter(s -> ! s.equals("a"))
				.map(s -> s + s)
				.toList();
	}

	private static List<String> collector(List<String> list) {
		return list.stream()
				.filter(s -> ! s.equals("a"))
				.map(s -> s + s)
				.collect(Collectors.toList());
				
	}

Collectors.toList() also makes no promises about serializablity or thread safety but I wasn’t expecting it to.

Multi statement lambda and for each anti patterns

When I do a code review of lambda/stream code, I am immediately suspicious of two things – block statement lambdas and forEach().

What’s wrong with this? It’s functional programming right?

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
		
AtomicInteger sum = new AtomicInteger();
List<Integer> odds = new ArrayList<>();
List<Integer> evens = new ArrayList<>();
		
list.forEach(n -> {
	sum.addAndGet(n);
	if (n % 2 == 0) {
		evens.add(n);
	} else {
		odds.add(n);
	}
});
		
odds.forEach(System.out::println);
System.out.println();
evens.forEach(System.out::println);
System.out.println();
System.out.println(sum);

Well? Not really. It does have a lambda. It doesn’t have a stream, but that’s easy enough to fix: list.stream().forEach(…).

All better? No. Just because you are using a stream doesn’t mean you are doing functional programming. I would much rather see this code as:

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
		
		
list.stream()
   .filter(x -> x % 2 == 1)
   .forEach(System.out::println);

System.out.println();

list.stream()
   .filter(x -> x % 2 == 0)
   .forEach(System.out::println);

System.out.println();

list.stream()
   .mapToInt(x -> x)
   .sum();

Yes, I’m still using forEach(). But now I’m using it for one purpose (printing) rather than sticking logic in it.

Whenever I see a forEach() or lambda with more than one statement, my first thought is “could this be clearer or more functional.” Often the answer is yes. Filter(), map() and collect() are you friends.

And if I did need that List?

list.stream()
   .filter(x -> x % 2 == 1)
   .collect(Collectors.toList());

[2019 oracle code one] Lambda, Streams and Collectors Lab

Lambda, Streams and Collectors Lab

Speakers: Stuart Marks, Maurice Naftalin, Jose Paumard & Gustavo Durand

For more blog posts, see The Oracle Code One table of contents


The lab is self paced

https://github.com/JosePaumard/code-one-2019-lambda-stream-collector-lab

I like that it is organized by topic so you can pick what you want to learn. Since “someone” believed I didn’t need to be here, I decided to blog about what I did and learned.

  • O_SimpleCollectors
    • I forgot Comparator.naturalOrder() exists because I hardly use it.
    • I forgot you can’t use Function.identity() with primitives and instead have to write a lambda: ex: x -> x.
  • P_HarderCollectors
    • I almost never use flatMap. I didn’t think to use it combined with splitAsStream to read words from a file. I do a lot of file processing though so this is definitely an idiom I need to remember! I shall type it in for each exercise in this lab that uses it (vs copy/paste) in order to ingrain it in my fingers! reader.lines().flatMap(SPLIT_PATTERN::splitAsStream) [edit: I think I’ve typed this enough times to remember it forever!]
    • The extra challenge to write a groupingBy using toMap. I knew I needed to use a merge function, but the types didn’t match my expectations. I learned that:
      • if you write a value instead of a lambda for the value function, you get a compiler error on the merge function (about the + operator being invalid ) and not the value function for not being a lambda
      • if you write a value function that returns an int instead of a long, you get a compiler error on the merge function and not the value function (because the merge function result is what gets set to the return value)
    • If you try to read from a reader that has already called reader.lines(), you don’t get any lines. Doh!
    • flatMap(String::chars) doesn’t work because chars() returns an IntStream. flatMapToInt(String::chars) does work
    • Entry has comparingByKey() and comparingByValue() methods
    • Remember to use groupingBy when aggregating and toMap when one to one
    • Need to call boxed() on an IntStream to be able to use partitioning by. An IntStream doesn’t have a collect method that takes a Collector as a parameter
    • Partitioning by can take a nested collector (ex: summing int)
  • L_HarderStreams
    • Convert type using mapToInt() before calling max
    • Forgot about Comparator.comparing() – I knew about this one earlier today! I think I’m getting tired :). I’m a morning person. Coding at 6:30pm is less than ideal for me.
    • Character.toString(Int) exists in Java 11. This means you can call mapToInt(String::chars).mapToObj(Character::toString)
    • The concept of using IntStream for an index and referring to a separate list (I would use a for loop for this since I need the index, but it’s a good tool to have in the toolbox)

My take

The lab is great. I like that it can be as easy or as hard as you want. I like they support multiple versions of Java and multiple IDEs. I completed the hard stream and collectors hard exercises

The room is terrible. It’s not really a room. It’s a grid of pipe/drape separated areas. I can hear every word in the room next to us. It got better. Once I got into the lab I was able to tune out the surroundings.

Also, my back hurts. Live blogging was fine. Different angle and I hardly look at the laptop. Coding for two hours (and I did take a break and stretch) was an awkward neck/back angle. How do people code on a laptop full time?