Java streams best practices

In this short post I am going to present Java 8 streams best practices. Most of them either I figured out myself or learned from my colleagues.

Let’s start with some “obvious” things about code formatting:

  • You should have at most one stream method call per line. This will make stream operations like map, filter and collect easily recognizable.
// BAD CODE:
strings.stream().filter(s -> s.length() > 2).sorted()
	.map(s -> s.substring(0, 2)).collect(Collectors.toList());

// GOOD CODE:
strings.stream()
	.filter(s -> s.length() > 2)
	.sorted()
	.map(s -> s.substring(0, 2))
	.collect(Collectors.toList());
  • You should import static all of the standard stream related methods. This will make code shorter, easier to read and easier to understand by removing all unnecessary visual noise.
// BAD CODE:
strings.stream()
	.sorted(Comparator.reverseOrder())
	.limit(10)
	.collect(Collectors.toMap(Function.identity(), String::length));

// GOOD CODE:
strings.stream()
	.sorted(reverseOrder())
	.limit(10)
	.collect(toMap(identity(), String::length));
  • You should prefer method references to lambdas.
// AVOID:
strings.stream()
	.map(s -> s.length())
	.collect(toList());

// PREFER:
strings.stream()
	.map(String::length)
	.collect(toList());

Method references are easier to read since we avoid all the visual noise generated by -> and () operators. They are also handled more efficiently by current version of Java. Lambda expressions like s -> s.length() are compiled to a private static method and an invokedynamic instruction.

// s -> s.lenght() is translated into:
private static Integer lambda$main$0(String s) {
	return s.length();
}

Method references are compiled to only invokedynamic instruction.

  • You should use methods from Class<T> to filter stream elements by a type and to cast stream elements to a type.
Stream<Object> objects = Stream.of(
	"a string",
	42,
	new String[] { "an array" },
	"another string");

List<String> strings = objects
	.filter(String.class::isInstance)
	.map(String.class::cast)
	.collect(toList());

Also rember that Class<T>::isInstance only checks if the value can be assigned to a variable of type T. For example Object.class.isInstance("foo") returns true because string "foo" can be assigned to a variable of type Object. If you want to check that stream elements have exactly type T you must use expression:

.filter(x -> (x != null) && x.getClass().equals(T.class))
  • Give meaningful names to frequently used collector expressions. In most cases this means extracting collector expression into its own method.
// USED FROM TIME TO TIME:
Map<Integer, Entity> entityById = entities.stream()
	.collect(toMap(Entity::getId, identity()));

// USED FREQUENTLY:
Map<Integer, Entity> entityById = entities.stream()
	.collect(ExtraCollectors.toByIdMap());

private static class ExtraCollectors {
  public static Collector<Entity,?,Map<Integer,Entity>> toByIdMap() {
	return Collectors.toMap(Entity::getId, identity());
  }
}

You may also consider using static import for your own frequently used collectors.

  • Use the following pattern when you sort stream values at hoc:
List<Student> result = students.stream()
	.sorted(
	  comparing(Student::getSurname)
		.thenComparing(Student::getName, reverseOrder())
		.thenComparing(Student::getAge)
		.thenComparing(Student::getId, reverseOrder())
	)
	.collect(toList());

Notice how we used reverseOrder() to reverse order of sorting by name and id. Also bear in mind that it is always a good idea to extract complicated comparers to its own method or a final field.

  • Use IntStream, LongStream and DoubleStream when working with primitive types. They are faster (they avoid boxing) and easier to use (they add useful methods like sum).
Stream<String> strings = Stream.of("a", "foo", "bar", "baz");

double averageLength = strings
		.mapToInt(String::length)
		.summaryStatistics()
		.getAverage();

Use mapTo[Int|Long|Double] and mapToObj to convert between a stream and a specialized primitive stream.

Also learn about static helper methods exposed by specialized stream classes:

// prints: 0 1 2 3 4 5 6 7 8 9
IntStream.range(0, 10)
	.forEach(System.out::println);

// prints: 1 2 4 8 16 32 64 128 256 512
IntStream.iterate(1, i -> 2*i)
	.limit(10)
	.forEach(System.out::println);

ThreadLocalRandom random = ThreadLocalRandom.current();

// prints: -376368599 2112239618
// just to demo generate method:
IntStream.generate(random::nextInt)
	.limit(2)
	.forEach(System.out::println);

// prints: -1134353240 2007034835
// stream of random int's - more idiomatic way:
random.ints()
	.limit(2)
	.forEach(System.out::println);
  • Avoid using peek(). Try to make your streams free of side-effects.

This list is by no means complete. I will try to add some more practices in the future. Bye!

marcin-chwedczuk

A Programmer, A Geek, A Human