Hi! I’d like to tell you something about Java Stream API – how and when you should use it.
Introduction
Java Stream API was added in Java 8 as a consequence of lambda introduction. The next versions of Java contains some improvements in this area but the most important, core functionality originates from Java 8. What makes the Java Stream API be a breakthrough is the possibility of functional programming. Thanks to it we can write code in a very concise and readable way.
Demo
enum Breed {
DOG, CAT, HAMSTER
}
@Value
class Pet {
String name;
int age;
Breed breed;
}
@Value
class Person {
String name;
String surname;
int age;
List<Pet> pets;
}
List<Person> people = Arrays.asList(
new Person("Joe", "Smith", 40,
Collections.singletonList(
new Pet("Poppy", 2, Breed.DOG)
)),
new Person("Adam", "Davis", 12,
Arrays.asList(
new Pet("Daisy", 1, Breed.HAMSTER),
new Pet("Rosie", 1, Breed.HAMSTER)
)),
new Person("Ava", "Miller", 25,
Arrays.asList(
new Pet("Tilly", 3, Breed.CAT),
new Pet("Luna", 2, Breed.CAT),
new Pet("Phoebe", 5, Breed.CAT)
)),
new Person("Elias", "Taylor", 36, Collections.emptyList()),
new Person("Amelia", "Wilson", 36,
Arrays.asList(
new Pet("Molly", 6, Breed.CAT),
new Pet("Alfie", 1, Breed.DOG),
new Pet("Teddy", 2, Breed.HAMSTER)
)),
new Person("Adelaide", "Martin", 36, Collections.emptyList())
);
Say we want to group pets older than one year by Breed, whose owner is adult. To do that in a traditional way we write the following code:
Map<Breed, List<Pet>> breedPetMap = new HashMap<>();
for (Person person : people) {
if (person.getAge() >= 18) {
for (Pet pet : person.getPets()) {
if (pet.getAge() > 1) {
if (!breedPetMap.containsKey(pet.getBreed())) {
breedPetMap.put(pet.getBreed(), new ArrayList<>());
}
breedPetMap.get(pet.getBreed()).add(pet);
}
}
}
}
As we can see this code requires nesting what makes it less readable. Additionally we have to take care of list creation what effects in more code.
All this stuff we could do without nested blocks in just 6 lines:
Map<Breed, List<Pet>> breedPetMapProducedByStream = people.stream()
.filter(person -> person.getAge() >= 18)
.map(Person::getPets)
.flatMap(List::stream)
.filter(pet -> pet.getAge() > 1)
.collect(Collectors.groupingBy(Pet::getBreed));
Stream creation
To use streams with collections just call stream method on any collection:
Collection<String> anyCollection = new ArrayList<>();
Stream<String> stream = anyCollection.stream();
Another ways to create stream:
Stream<String> stringStream = Stream.of("one", "two", "three");
// OR:
Collection<String> collection = new ArrayList<>();
StreamSupport.stream(collection.spliterator(), false);
As you can see there are many ways of stream creation. The second method is a little different because it allows us to specify if created stream should work in parallel. Parallel streams may seem to be a breakthrough however they should be used with caution. Inappropriate usage can cause serious problems. But in this article we won’t cover this topic.
Intermediate Stream operations
Intermediate Stream operations are executed at the end of stream. The end of stream takes place when we use any terminal operation.
Map
To modify input we can use map method. Parameter of this method is functional interface Function which is used in stream terminal operations. This functional interface have one input and output. It’s also important to keep in mind that map is necessary to avoid side effects so we shouldn’t modify input – just produce output based on input.
Filter
To filter some elements in stream we can use filter method. Its parameter is Predicate – functional interface that have one parameter and returns boolean. The boolean value specify whether element is passed (true) or not (false).
FlatMap
Sometimes we need to create stream of collections and after some operations create one stream of all collections elements. To do that we can use flatMap. This stream method behaves like map but expects stream of elements as returned value. FlatMap will create one stream of all streams returned by function passed in this method.
Sorted
This method sorts elements in stream. It’s overloaded and have optional input. We can specify how elements should be sorted passing Comparator. If we don’t pass anything then elements will be sorted in natural order assuming that stream elements implement Comparable interface.
Distinct
Distinct removes duplicated elements in the stream and haven’t any input.
Limit
Limit removes elements from stream so that there are only first N elements where N is specified by limit input.
Terminal Operations
Terminal operations causes all intermediate ones to be called and return some response value that is not stream as long as you haven’t stream of streams 🙂
Collect
Collect is the most important terminal operation. It’s overloaded and have two versions:
<R> R collect(Supplier<R> var1, BiConsumer<R, ? super T> var2, BiConsumer<R, R> var3)
<R, A> R collect(Collector<? super T, A, R> var1);
Usually the second version of Collect method is used combined with Collectors static methods. I think that the most widely used is Collectors.toList() which exports stream elements to list but we can also export elements to any other collection such as Set and even Map.
Reduce
We can also finish stream combining all elements and returning only one. It’s useful when we want to join list of strings to create e.g. string of strings seperated by comma.
There are much more stream methods that can be used to make our code shorter and more readable. If you’re interested I recommend you to read Java SE Streams documentation which you can find in links below.
Conclusion
As we can see Java Stream API is really powerful but with great power comes great responsibility. Overused streams can cause more problems than profits. Long and complicated streams can turn out to be hard to read and understand by other programmers. Moreover it’s good practice to use pure functions without any side effects in streams. If you feel that it’s hard to write such a code better stick to traditional methods. I hope you like it.