Got a problem with your Java Stream usage but not sure how to fix it?

Relax. I'll give you some insight here that might just help you figure out what's going wrong.

But even if you're not having any problems, you might find it useful to know a little bit more about the Stream API.

In either case, read on.

It's Called peek()

It's the peek() method. And it might be your best friend when it comes to debugging problems associated with a Stream object.

As the name of the method implies, peek() gives you some insight into what's going on during the intermediary operations on your Java Stream.

It's often the case that developers use it to log the state of the Stream object at certain points in processing. Then, they'll evaluate the log file to ensure that what they expect to happen is what's actually happening.

But peek() might give you some grief if you don't know how to use it properly.

When It Doesn't Work

That peek() method ain't gonna get you anything without a terminal operation.

So if you're here wondering why your peek() debugging isn't behaving as expected, you might want to look at that first.

By the way: if you're unsure about what a terminal operation is, it's the "stopping point" when you work with a Stream object. As the name implies, it's the last thing you do before you stop working with the Stream completely.

There are several different types of terminal operations defined by the Java Stream API. You'll have to use one of them if you want peek() to do anything.

Let me show you an example. Consider this model class:

public class Employee {

    private String id;    
    private String lastName;
    private String firstName;

//getters and setters

}

Use that to crank out some test data like this:

List<Employee> employees = new ArrayList<>();

Employee employee1 = new Employee();
employee1.setId("A17");
employee1.setFirstName("Frank");
employee1.setLastName("Smith");
employees.add(employee1);

Employee employee2 = new Employee();
employee2.setId("B12");
employee2.setFirstName("Bunny");
employee2.setLastName("Cheshire");
employees.add(employee2);

Employee employee3 = new Employee();
employee3.setId("C11");
employee3.setFirstName("Glenn");
employee3.setLastName("Kin");
employees.add(employee3);

Now you've got a List of three (3) employees. You can, of course, convert that List into a Stream to filter, sort, and retrieve the info you're looking for.

But for now, just use peek() on the Stream and see what happens.

employees
    .stream()
    .peek(System.out::println);

On paper, that looks like it should print out all the Employee objects in the List on a separate line. But it doesn't do that.

In fact, it doesn't print anything.

Why? Because there's no terminal operation on that Stream object.

Now if you do this:

employees
    .stream()
    .peek(System.out::println)
    .collect(Collectors.toList());

That's going to give you a printout. It has a terminal operation.

So let's look at how you might use peek() to debug problems.

The Current State of Affairs

Let's say you've got a Stream and you're performing multiple intermediate operations on it.

But the finished product isn't giving you what you want. And you wonder where things are going wrong.

Use peek() to figure it out.

Take a look at this code:

List<String> lastNames = employees
                            .stream()
                            .map(employee -> employee.getLastName())
                            .peek(System.out::println)
                            .sorted()
                            .collect(Collectors.toList());

Let's say something is going wrong with that map() method. But you aren't sure what it is.

(Yeah, I know this particular example of map() isn't too complicated, but just roll with it.)

In that case, you can use the peek() method as you see above. The example above will print every element in the Stream (the employee's last name) on a separate line. 

It will look like this:

Smith
Cheshire
Kin

But once it's once it's done with that printout, it will resume processing the Stream with the additional operations.

So peek() is helpful because it gives you a snapshot of the Stream state at any given point in time.

Consider Being Consumed

The peek() method, by the way, takes a Consumer as its sole parameter.

A Consumer is a functional interface that accepts a single input and doesn't return a result.

That's why it works with method references like System.out::println.

It works with lambda expressions as well. You could rewrite the code above as follows:

List<String> lastNames = employees
                            .stream()
                            .map(employee -> employee.getLastName())
                            .peek(lastName -> System.out.println(lastName))
                            .sorted()
                            .collect(Collectors.toList());

That's going to give you the same output you saw before.

Wrapping It Up

Now you have another tool in your debugger toolbox. Feel free to use it to identify, isolate, and resolve problems with the Java Stream API.

And feel free to experiment with peek() and different Consumers so you become an expert in using it.

Have fun!

Photo by Ketut Subiyanto from Pexels