Intro

T is usually the Type of the object in the stream. R is usually the type of the Result of the method.

How to use Stream?

To use the Streams methods, we need a Stream object (obviously). If we have a collection like a List, this doesn’t implement Stream. However, the Collection interface has a method, stream(), which returns a Stream object for the Collection.

List<String> strings = List.of("hey","bitch");

//call stream() to get a stream of these Strings
Stream<String> stream = strings.stream();

//limit() gives up to maximum 4 elements
Stream<String> limit = stream.limit(4);

The limit() method returns another Stream of Strings, which we assigned to a variable called limit.

Btw, if we try printing out this variable limit, we don’t get the elements in that list, we get this weird SliceOps things.

Like everything in Java, the stream variables in the example are Objects. But a stream does not contain the elements in the collection. It’s more like the set of instructions for the operations to perform on the Collection data

Stream methods that return another Stream are called Intermediate Operations. These are instructions of things to do, but they don’t actually perform the operation on their own.

How to see result of using Stream’s methods?

Stream is an instruction, like A recipe in a book only tells someone how to cook or bake something. Opening the recipe doesn’t automatically present you with a freshly baked chocolate cake. You need to gather the ingredients according to the recipe and follow the instructions exactly to come up with the result you want.

To get that cake, we need to call a “do it”. This “do it” method is called Terminary Operation which actually returns something to you unlike Intermediary Operations.

An example of terminary operation is Stream’s .count() method

//call stream() to get a stream of these Strings
Stream<String> stream = strings.stream();

//limit() gives up to maximum 4 elements
Stream<String> limit = stream.limit(4);

long result = limit.count();
System.out.println(result);
// outputs 4

This works but not that useful. One of the most common things to do with Streams is put the results into another type of collection. The API documentation for this method might seem intimidating with all the generic types, but the simplest case is straightforward.

아무거나

Stream pipeline

There are several steps to making this pipeline

아무거나

You need at least the first and last step to use Stream API.

Also, operations can be chained as so without putting each stage in its own variable:

List<String> result = strings.stream()
  .sorted()
  .limit(4)
  .collect(Collectors.toList());

We can stack as many intermediary operations before calling the final terminal operation in a single stream pipeline.

Example in Spring

with Stream:

    public List<ProjectResponseDto> findAll(){
        List<Project> list = projectRepository.findAll();
        return list.stream().map(ProjectResponseDto::new).collect(Collectors.toList());
    }

without stream:

public List<BoardResponseDto> findAll() {

    Sort sort = Sort.by(Direction.DESC, "id", "createdDate");
    List<Board> list = boardRepository.findAll(sort);
    
    /* Stream API를 사용하지 않은 경우 */
    List<BoardResponseDto> boardList = new ArrayList<>();
    
    for (Board entity : list) {
        boardList.add(new BoardResponseDto(entity));
    }
    
    return boardList;
}

Streams are lazy!

Each intermediate operation is just an instruction like a recipe book- it does not perform the instruction itself to create chocolate cake itself. So intermediate operations are lazily evaluated.

On the other hand, terminal operation looks at whole list of instructions, these intermediate operations in the pipeline and then run the whole set together in 1 go. Terminal operations are eager, they run as soon as they are called.

This is efficient because instead of having to iterate over the whole original collection for each intermediate operation, it is possible to perform all the intermediate operations in 1 go while going through that collection once.

Precautions with Stream

Can’t reuse Streams

Once a terminal operation has been called on a stream, you can’t reuse any parts of that stream; you have to create a new one. Once a pipeline has executed, that stream is closed and can’t be used in another pipeline, even if you stored part of it in a variable for reusing elsewhere. If you try to reuse a stream in any way, you’ll get an Exception.

Can’t change underlying collection while stream is operating

Think about it— if someone asked you a question about what was in a shopping list and then someone else was scribbling on that shopping list at the same time, you’d give confusing answers too

Stream operations do not change the original collection

The Streams API is a way to query a collection, but it doesn’t make changes to the collection itself.

You can use the Streams API to look through that collection and return results based on the contents of the collection, but your original collection will remain the same as it was. This is helpful because you can query collections while knowing data in your original collection is safe.