Introduced in Java 8, the Java Stream API adds functional operations to collections, arrays, and other iterables. Streams are not collections or data structures, but they are used to enhance existing structures.
Here are some examples on how the Stream API can be used.
First Example: Iterate over a list of elements and print out all other elements.
This is a strange example, however it is something that I have needed on a few occasions. Let’s say you have a list of friends and you want to print out each person and their friends. Essentially, you need to operate on each element in the list that is not the current element in the list.
Without Streams
Without streams, one might approach it this way:
for (String person : FRIENDS) {
System.out.println("Friends of " + person + ":");
for (String other : FRIENDS) {
if (!person.equals(other)) {
System.out.println(other);
}
}
}
If you wanted to print them in-line, the task is a little bit different:
for (String person : FRIENDS) {
List<String> friends = new ArrayList<>(FRIENDS);
friends.remove(person);
System.out.println("Friends of " + person + ": " + String.join(", ", friends));
}
This doesn’t look too messy, because we are dealing with a list of String
s.
With Streams
With streams, we can do the following:
FRIENDS.forEach(person -> {
System.out.println("Friends of " + person + ":");
FRIENDS.stream().filter(other -> !person.equals(other)).forEach(System.out::println);
});
If you wanted to print them in-line, it would look something like this:
FRIENDS.forEach(person -> {
String friends = FRIENDS.stream().filter(other -> !person.equals(other)).collect(Collectors.joining(", "));
System.out.println("Friends of " + person + ": " + friends);
});
What’s going on here?
We are using the filter
method which creates a new stream based on the given predicate (kind of like a conditional). The predicate other -> !person.equals(other)
is instructing the new stream to not include the element in the list that matches the current element being iterated on.
In the first example, we are using the forEach
method which takes a lambda expression and calls it to each of the elements.
In the second example, we are using the collect
method which is a terminating method (ends the stream). By passing in Collectors.joining
, we are telling the collect
method that we would like a joined String
to be returned.
Second Example: Multiply all elements by five.
Suppose we want to operate on each of the elements in a list without mutating the original list. In this example, we are going to take a list of Integer
s and multiply them all by five.
Without Streams
Here’s how we might approach this problem without streams:
List<Integer> multiplied = new ArrayList<>();
for (Integer number : NUMBERS) {
multiplied.add(number * 5);
}
// To print the results you most likely used something like StringUtils.join, or Guava's Joiner
// Or you did messy things like this (not recommended because of the replacement):
String output = Arrays.toString(multiplied.toArray()).replace("[", "").replace("]", "");
System.out.println(output);
In the previous example, I touched on how one might want to display the information in-line. It wasn’t as difficult of a problem, because the elements we were joining were already String
s. This is more of a challenge in the current example.
You might be familiar with Apache Commons’s StringUtils
or Google Guava’s Joiner
. As you will see, they are not necessary when using streams.
With Streams
Here’s how me might approach this problem using the streams api:
List<Integer> multiplied = NUMBERS.stream().map(i -> i * 5).collect(Collectors.toList());
String output = multiplied.stream().map(Object::toString).collect(Collectors.joining(", "));
System.out.println(output);
What’s going on here?
This time we’re using the map
function which is similar to forEach
in that it calls a lambda on each of the elements of the stream. However, there is an important key difference:
-
forEach
calls a lambda on each of the elements. -
map
calls a lambda on each of the elements and returns a new stream of the results.
This means that when using map
we can mutate the elements and copy the results into a new List
. We use it both to apply the lambda i -> i * 5
which multiples each element by 5, and to apply the lambda Object::toString
to convert each Integer
to a string for joining.
We are also using the collect
method with Collectors.toList()
which instructs collect
to create a List
and terminate the stream.
Third Example: Create lists of properties from a list of elements.
Suppose we have a list of people with the name and age property. Something like this:
private static final List<Person> PEOPLE = List.of(new Person("Sarah", 29),
new Person("John", 16),
new Person("Mary", 21),
new Person("Susan", 55));
private static class Person {
private String name;
private int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
String getName() {
return name;
}
int getAge() {
return age;
}
}
What if we wanted to create two separate lists? One list will contain the names, and another will contain the ages.
Without Streams
Without streams, this might look something like:
List<String> names = new ArrayList<>();
List<Integer> ages = new ArrayList<>();
for (Person person : PEOPLE) {
names.add(person.getName());
ages.add(person.getAge());
}
// To print the results you most likely used something like StringUtils.join, or Guava's Joiner
// Or you did messy things like this:
System.out.println("Names: " + String.join(", ", names.toArray(new String[0])));
System.out.println("Ages: " + Arrays.toString(ages.toArray()).replace("[", "").replace("]", ""));
With Streams
With streams, we can do the following:
List<String> names = PEOPLE.stream().map(Person::getName).collect(Collectors.toList());
List<Integer> ages = PEOPLE.stream().map(Person::getAge).collect(Collectors.toList());
System.out.println("Names: " + names.stream().collect(Collectors.joining(", ")));
System.out.println("Ages: " + ages.stream().map(Object::toString).collect(Collectors.joining(", ")));
What’s going on here?
This is really just a summary of what we’ve discussed in the previous examples. We are taking advantage of the fact that map
creates a new stream and are calling the getName
and getAge
property on each person.
Conclusion
Hopefully this shares some of the value of using streams in your code. I still feel like they’re a bit clunky compared to some of the offerings in JavaScript or Kotlin. However, I also think it’s a step in the right direction.
Full code for each example can be found on GitHub at cr0wst/java-sandbox
Top comments (0)