In this article, we will learn Java Stream API or in other words Java Streams with examples. Java Streams comes with Java 8 and with stream operations, we can simplify many things such as filtering, sorting, transformations, etc. Let’s start to learn Java Streams with examples!
Java Streams with Examples
Let’s start to learn stream operations on a list. Assume that we have a list of integers from 1 to 10 and we will do a filter of the odd numbers in this list, then limit the execution of the elements after the 4th element, then transform the filtered elements by multiplying 2, and then finally print them. So, let’s list all operations that we will do on this integer list:
- Filter the odd numbers
- Limit the execution after 4th filtered element.
- Transform the element by multiplying by 2.
- Print the results.
We can do all of these operations in one stream chain or stream pipeline! This is the power of Java Stream API! :)
In the below example, the numbers list contains integers (it is a collection), and we are using numbers.stream()
to convert the collection to a stream to do the stream operations. By using filter() method we filter odd numbers, then with the limit() method, we limit the stream processing, and map() method is the transformation method for streams. We use the map method to transform the filtered number to number*2. Finally, by using forEach() terminal stream operation to print the numbers which can come to the end of the stream chain.
public class StreamBasics { List<Integer> numbers = new ArrayList<>(); @BeforeEach void setup() { //Another method: Collections.addAll(numbers, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); numbers.add(1); numbers.add(2); numbers.add(3); numbers.add(4); numbers.add(5); numbers.add(6); numbers.add(7); numbers.add(8); numbers.add(9); numbers.add(10); } @Test public void streamExample() { numbers.stream() .filter(number -> number % 2 == 1) //Filter the odd numbers. .limit(4) //After 4th element stop the stream. Process first 4 element. .map(number -> number * 2) //Transform the number to number*2 .forEach(System.out::println); //Print the results. } }
Output
Let’s do more examples, this time we will add some debugging lines in our stream code to see the execution process.
@Test public void streamOperationsTest() { System.out.println("Stream operations are starting!"); numbers.stream() .filter(number -> { System.out.println("Filter operation for number: " + number); return number % 2 == 1; }) //Filter the odd numbers. .limit(4) //After 4th element stop the stream. Process first 4 element. .map(number -> { System.out.println("Map operation for number: " + number); return number * 2; }) //Transform the number to number*2 .forEach(number -> System.out.println("Result of stream: " + number)); //Print the results. System.out.println("Stream operations finished!"); }
Output
As you can see below, each stream element is processed one by one. The odd numbers up to 4 which are 1, 3, 5, and 7 are processed and for them, you can see the filter, map, and result print lines. For example, the filter operation for number 2 immediately finished at filter operation because it is not an odd number and number 2 could not be processed until the end of the stream. And because of limit(4), after the 4th odd number which is 7, the stream operations finished.
Streams need terminal operations like forEach, collect, count, max, min, findAny, anyMatch, noneMatch, etc. If you do not finish stream one of the terminal operation, you will not get the results. You can test this by commenting .forEach() line in the below example. If we comment .forEach(number -> System.out.println("Result of stream: " + number)); //Print the results.
we cannot get anything as a result because streams need a terminal operation.
@Test public void streamOperationsTest() { System.out.println("Stream operations are starting!"); numbers.stream() .filter(number -> { System.out.println("Filter operation for number: " + number); return number % 2 == 1; }) //Filter the odd numbers. .limit(4) //After 4th element stop the stream. Process first 4 element. .map(number -> { System.out.println("Map operation for number: " + number); return number * 2; }); //Transform the number to number*2 //.forEach(number -> System.out.println("Result of stream: " + number)); //Print the results. //Note: Streams need terminal operations! forEach, collect, count, max, min, findAny, anyMatch, noneMatch, etc. System.out.println("Stream operations finished!"); }
Output
We cannot also reuse the streams. Once we use a terminal operation for a stream that stream’s processing is finished and we cannot use one more terminal operation for the same stream. Let’s see this in an example.
@Test public void streamReUsabilityTest() { Stream<Integer> numbersStream = numbers.stream() .filter(number -> number % 2 == 1) //Filter the odd numbers. .map(number -> number * 2); numbersStream.forEach(System.out::println); numbersStream.forEach(System.out::println); //Here we cannot reuse the stream again!!! }
Output
As you seen below, the first terminal operation is worked fine and when we apply the second terminal operation on the same stream, we got an “IllegalStateException: stream has already been operated upon or closed.”
In this article, I do not cover more topics such as comparator, intermediate operations, terminal operations, optional, stream sources, stream reduce, and more. We will cover these in the other articles.
GitHub Project
Thanks for reading.
Onur Baskirt

Onur Baskirt is a Software Engineering Leader with international experience in world-class companies. Now, he is a Software Engineering Lead at Emirates Airlines in Dubai.