DEV Community

Steve Crow
Steve Crow

Posted on • Originally published at smcrow.net on

Java Stream Uses

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 Strings.

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 Integers 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 Strings. 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)