Real-world tips for streams in Java 8
We made the switch from Java 7 to Java 8 a few months ago, not long after the last public release of JDK 7. This is at my 9-to-5 job. It was practically a seamless transition. Since then, I've enjoyed using one of Java 8's new features: streams! Streams add a functional element to Java that is useful for processing collections of elements.
When I created my first streams, I wasn't aware of the full capabilities offered by this new feature. So, I'd like to share some tips from my real-world use of streams. While I've modified the objects to be more generic, these examples are very similar to streams I created for real services.
The method reference operator
Along with streams, one of the new features in Java 8 is the method reference operator. I think of it as the "double colon" operator usually, because it's two colons in a row. This operator allows you to refer to a class method to produce a function interface type, such as a Predicate or Consumer. For example, you might use Point::toString
to use an object's toString
method in the map function.
Oracle has a good list of example uses of the method reference operator.
I've found the method reference operator to be useful when creating new objects.
Set<String> names = people.stream()
.map(Person::getFirstName)
.collect(toCollection(HashSet::new));
I didn't realize at first that the operator could be used to create new arrays as well.
String[] urls = images.stream().map(i -> i.url)
.filter(Objects::nonNull)
.toArray(String[]::new);
Finally, the operator is useful to get an iterator from the stream.
urlsToLoad.addAll(Arrays.stream(urls)
.map(String::trim)::iterator);
Flat streams
Say you have a list of HTML elements, and each element has a list of CSS classes. You can use the flatMap function to get a list of all the class names. Its argument must be a function that returns a stream.
elements.stream().filter(e -> e.tag.equals("p"))
.flapMap(e -> e.cssClassNames.stream())
.collect(ArrayList::new);
There are also special functions for primitives, such as flatMapToInt, which accepts a function that returns an IntStream. If you have a list of items, each of which has a list of numeric categories, you could obtain a single array of the categories like this:
int[] categories = items.stream()
.filter(b -> b.categories != null)
.flatMapToInt(b -> IntStream.of(b.categories))
.toArray();
Streaming maps
With streams, you can operate on maps in addition to arrays and collections. To do so, you can get a stream on the entrySet. (Of course, you can use the keys or values, but then you get only ... keys and values.) Then, you can use toMap to convert the stream back to a map.
cookies.entrySet().stream()
.filter(c -> c.getValue().equals("1"))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
You can also create a stream from a collection, and convert the stream to a map. Again, Oracle has some examples of how this can be done.
Do not return streams from public methods
It's tempting, but I would advise against returning streams from methods, especially public methods. A stream can be used only once. A stream returned by a public method is bound to cause a bug at some point. A more flexible option is to return a collection. Then, the caller of the method can create a stream from the collection if they so choose.
For example, instead of this signature:
public Stream<String> findFirstNames()
I would recommend this one:
public Set<String> findFirstNames()
If it's a private method, then it might be okay to return a stream, because you can control its usage.
Concatenate streams
Multiple streams can be concatenated into a single stream with the concat function.
You might have a private function that accepts two streams as an argument. It could combine both streams using concat
, then filter them together.
private static List<Point> combine(Stream<Point> a, Stream<Point> b) {
return Stream.concat(a, b)
.filter(p -> p.x >= 0)
.collect(Collectors.toList());
}
Calls to the function might look like the example below. You can create streams with the empty and of functions.
// call with empty stream
List<Point> points1 = combine(PointFactory.randomPoints(10).stream(), Stream.empty());
// inject additional points from another source
List<Point> points2 = combine(points1, Stream.of(new Point(1, 1), new Point(-1, -1)));