In this article, we’ll learn everything regarding Java Stream API that was introduced in Java 8, through detailed examples.
1. What Is a Java Stream?
You can think of a stream as a sequence of elements, that allows you to perform sequential and parallel operations. Furthermore, you cannot add any elements once the stream has been created nor you can retrieve an element by providing an index.
Streams have 2 kinds of operations:
- Intermediate Operations: These operations will filter, modify and/or transform the elements of the stream and then return the stream with the filtered/modified/transformed elements. This allows you to continue performing operations that Stream API provides.
- Terminal Operations: These operations will transform the stream into another primitive or object. After applying a terminal operation, you could have an
int
,long
, String, Optional, any collection(such as List, Set), or even a Map.
An important characteristic of a stream is that once you have performed any of the intermediate or terminal operations, you will be unable to perform more operations unless you chain them. Consider the example below:
// Applying a terminal after an intermediate operation Stream<String> stream = Stream.of("hello"); stream.map(String::toLowerCase); stream.forEach(System.out::println);
The third line will produce the following exception:
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
To remedy this, you have to write the following:
stream .map(String::toLowerCase) .forEach(System.out::println);
Before we delve into streams, I suggest that you read the following articles as they would help a lot in taking full advantage of the stream API.
2. How to Create a Java Stream
There are multiple ways to create a stream. Let’s go through each of them:
2.1 Stream.of(T value), Stream.of(T… values) and Stream.ofNullable(T t)
The first method allows only one value whereas the second allows multiple values. Note that null values are not allowed but if you do want to initialize a stream with a null value, you can use Stream.ofNullable(T t)
.
Stream<String> stream = Stream.of("Hello", "World"); Stream<Integer> streamInteger = Stream.of(5); var s = Stream.ofNullable(null);
2.2 Stream.builder()
This method follows the builder pattern and it is a different way to create and fill a stream. One difference with the previous way of creating a stream is that by using builder, you are obliged to declare your stream as Stream<Object>.
Stream<Object> built = Stream.builder() .add("Code") .add("Learn") .add("Hub") .build();
2.3 Stream.generate(Supplier s) and Stream.empty()
You can also generate a Stream by using Stream.generate(Supplier s)
. Let’s see an example:
Stream<List<String>> streamStringGen = Stream.generate(() -> { List<String> list = new ArrayList<>(); list.add("hello"); list.add("world"); return list; });
As you can observe, this stream now has one element; a list that has two strings. If you want to convert it to a Stream<String>
you would need to use flatMap()
which will see later how it works.
Finally, If you need an empty stream, you can use the Stream.empty()
method.
2.4 Creating a Java Stream From a Collection or Map
The most common use case of streams is to convert for example a list, to a stream, perform some operations, and then back to a list.
List<String> list = List.of("C", "L", "H") .stream() .filter(l -> l.equals("C")) .toList(); Set<String> set = Set.of("C", "L", "H") .stream() .filter(l -> l.equals("C")) .collect(Collectors.toSet()); Map<Long, String> map = Map.of(1L, "C", 2L, "L", 3L, "H") .entrySet() .stream() .filter(l -> l.getValue().equals("C")) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
In the above examples, we used the stream()
method the Collection interface has, then filtered all the values that are equal to “C” and converted them back to the original data structure.
2.5 Stream iterate(final T seed, final UnaryOperator f) Example
This method allows you to declare a seed and use a UnaryOperator to produce the new values. As you would expect, this leads to an infinite stream, so using limit()
is required unless you want to have errors.
So if you would like to create a stream and, print the all powers of two, from 20 to 219, you could do it as shown below:
Stream .iterate(1, i -> i * 2) .limit(20). forEach(System.out::println);
Furthermore, you could produce the first 10 Fibonacci numbers:
Stream .iterate(Arrays.asList(0, 1), fibList -> Arrays.asList(fibList.get(1), fibList.get(0) + fibList.get(1) )) .limit(10) .map(list -> list.get(0)) .forEach(System.out::println);
2.6 Stream iterate(T seed, Predicate hasNext, UnaryOperator next) Example
This works like the previous method, but now you can specify a predicate, that if it fails, the iteration will stop.
So the below snippet will only print the numbers 1-10:
Stream .iterate(1, i -> i < 10, i -> i + 1) .limit(20) .forEach(System.out::println);
2.7 Stream concat(Stream a, Stream b) Example
With stream concat()
you can combine 2 streams into one as shown below:
Stream<String> clh = Stream.of("Code", "Learn", "Hub"); Stream<String> programmingLanguages = Stream.of("Java", "SQL", "Swift", "Python"); Stream .concat(clh, programmingLanguages) .forEach(System.out::println);
3. Intermediate Operations of Java 8 Stream API
As it has already been stated, intermediate operations are all methods that will return a stream. We’ll go through each of them in the following sections.
In order to make our examples more interesting, consider that we have created the following setup:
private record Car(long id, String brand, String model, int horsePower, double price){}; private static List<Car> cars = Arrays.asList( new Car(1, "Opel", "Astra", 105, 20_000), new Car(2, "Lamborghini", "Gallardo", 320, 120_000), new Car(3, "Mercedes", "C180", 200, 40_000), new Car(4, "Opel", "Insignia", 120, 28_000), new Car(5, "VW", "Polo", 92, 17_000), new Car(6, "BMW", "M3", 250, 80_000) );
3.1 Stream filter(Predicate predicate) Example
The predicate is just a simple function that accepts one parameter of any type and returns true
or false
.
The filter()
method allows you to filter only the elements of the stream that match this predicate.
Below you can find an example where we filter only the cars that cost more than $30,000.
cars .stream() .filter(car -> car.price() > 30_000) .forEach(System.out::println);
After we filter, we print every element that currently exists in the stream using forEach(Consumer consumer)
. The output is the following:
Car[id=2, brand=Lamborghini, model=Gallardo, horsePower=320, price=120000.0] Car[id=3, brand=Mercedes, model=C180, horsePower=200, price=40000.0] Car[id=6, brand=BMW, model=M3, horsePower=250, price=80000.0]
If you want to learn more about how filter()
works, check this article.
3.2 Stream map(Function mapper) example
The function is just a simple function that accepts one parameter and returns a value.
The map()
method allows you to transform all the elements of the stream to any type that you might need.
Below you can find an example where we convert the car objects to simple strings that contain the model of each car.
cars .stream() .map(Car::model) .forEach(System.out::println);
Here we used a method reference that is equivalent to the following lambda expression:
car -> car.model()
The output is the following:
Gallardo C180 Insignia Polo M3
If you want to learn more about how map()
works, check this article.
3.3 Stream mapToInt, mapToLong and mapToDouble Functions Examples
These functions convert the elements of the stream to the respective type (int
, long
, double
) while at the same time, your stream will no longer be a stream but a IntStream
, LongStream
or DoubleStream
. As a result, you will be able to use all the extra methods that might be provided by each interface.
Let’s see some examples:
Example of getting the average horsePower, using mapToInt
:
cars .stream() .mapToInt(Car::horsePower).average();
Note that average()
and sum()
are both terminal methods.
Example of getting the sum of cars‘ prices using mapToDouble
:
cars .stream() .mapToDouble(Car::price).sum();
3.4 Stream flatMap(Function mapper) Example
This method is a little bit different than map()
, as the mapper must return a stream. It is used to make deep data structures linear, consider the following list of lists:
List<List<String>> brands = new ArrayList<>(); brands.add(Arrays.asList("Ferrari", "Lamborghini", "Maserati")); brands.add(Arrays.asList("Opel", "VW", "Fiat")); brands.add(Arrays.asList("BMW", "Mercedes", "Audi"));
As you can observe, each list has some brands, let’s say we have a task to convert this to a simple List<String>
. How would we do that? Here comes flatMap
into play:
List<String> brandsFlat = brands .stream() .flatMap(Collection::stream) .toList();
Collection::stream
is a method reference that is equivalent to the lambda expression brandsList -> brandsList.stream()
.
As a result, our list will now contain just strings and not a list of strings.
3.5 Stream flatMapToInt, flatMapToLong and flatMapToDouble Functions Examples
As with section 3.3, the difference between these methods and flatMap()
is that they return an IntStream
, LongStream
, or DoubleStream
.
Let’s say we have a list containing a list of cars:
List<List<Car>> carsDeepList = new ArrayList<>(); carsDeepList.add(cars); carsDeepList.add( Collections.singletonList( new Car(7L, "Opel", "Zafira", 100, 20_000)) ); carsDeepList.add( Collections.singletonList( new Car(8L, "Opel", "Corsa", 80, 18_000)) );
The first addition is the car list we have mentioned in section 3.
Now let’s say that your task is to find the max price of all cars inside the carsDeepList
, you can do it as shown below:
double brandsFlat = carsDeepList .stream() .flatMapToDouble(carsList -> carsList.stream().mapToDouble(Car::price)) .max() .orElse(-1); System.out.println(brandsFlat);
The result will be 120000.0
.
3.6 Stream mapMulti(BiConsumer mapper) Example
This method was added in Java 16 and it accepts a BiConsumer
. This BiConsumer
accepts 2 parameters, the element and a consumer which will be used to produce the new values.
As per Javadocs, this method is similar to flatMap()
but it should be preferred in these two cases:
- When we like to replace each element of the stream with a very small number of elements, since we would avoid creating a new stream like
flatMap()
does. - When it is easier to use a consumer than returning the elements as a stream
Consider an example that falls to the first reason why we might opt for mapMulti
instead of flatMap
.
carsDeepList.stream().<Map<String, String>>mapMulti( ((carsList, consumer) -> { for(Car car: carsList){ if(Math.random() < 0.05){ consumer.accept( Map.of(car.model(), car.brand()) ); } } } ));
This will convert a list of lists of cars to a list of maps with the model as a key and the brand as value, with a 5% probability.
Finally, there are primitive versions of these methods (mapMultiToInt
, mapMultiToLong
, and mapMultiToDouble
) which work like mapMulti
but return an IntStream
, LongStream
, and DoubleStream
respectively.
3.7 Stream distinct() Example
Stream distinct() removes any duplicate elements that might exist in the stream. The decision on whether two objects are the same is based on the result of equals()
method. Let’s jump to an example:
Stream.of(new Car(1, "Opel", "Astra", 105, 20_000), new Car(2, "Lamborghini", "Gallardo", 320, 120_000), new Car(3, "Mercedes", "C180", 200, 40_000), new Car(3, "Mercedes", "C180", 200, 40_000), new Car(4, "Mercedes", "C180", 200, 40_000)) .distinct() .forEach(System.out::println);
As you would expect, the result would be to print 4 cars in total instead of 5. Note that the 5th element has a different id so it is considered a different object. Hence, equals()
would return false
when compared to any other element.
3.7 Stream sorted() Example
This method returns the stream sorted according to the natural order. Note that to apply this function, your objects inside the stream must implement comparable, otherwise, a ClassCastException
will be thrown. Let’s see some examples:
//Will print them in ascending order Stream.of(1, -1, 5, 100, 3, 0) .sorted().forEach(System.out::println); //Will print them in ascending lexicographical order Stream.of("Code", "Learn", "Hub", "Hello", "World") .sorted().forEach(System.out::println); //Will fail as Car object does not implement comparable interface try { Stream.of(new Car(1, "Opel", "Astra", 105, 20_000), new Car(2, "Lamborghini", "Gallardo", 320, 120_000), new Car(3, "Mercedes", "C180", 200, 40_000)) .sorted() .forEach(System.out::println); } catch (Exception e){ e.printStackTrace(); }
As expected, the following will print:
-1 0 1 3 5 100 Code Hello Hub Learn World java.lang.ClassCastException: class StreamExamples$Car cannot be cast to class java.lang.Comparable (StreamExamples$Car is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
Now, if for example, you wanted to sort the cars by ascending price, you could change the record as follows:
private record CarComparable(long id, String brand, String model, int horsePower, double price) implements Comparable<CarComparable>{ @Override public int compareTo(CarComparable o) { double priceDif = this.price - o.price; if(priceDif > 0){ return 1; } else if(priceDif == 0){ return 0; } else { return -1; } } };
Then if you run the following code:
Stream.of(new CarComparable(1, "Opel", "Astra", 105, 20_000), new CarComparable(2, "Lamborghini", "Gallardo", 320, 120_000), new CarComparable(3, "Mercedes", "C180", 200, 40_000)) .sorted() .forEach(System.out::println);
The output would be:
CarComparable[id=1, brand=Opel, model=Astra, horsePower=105, price=20000.0] CarComparable[id=3, brand=Mercedes, model=C180, horsePower=200, price=40000.0] CarComparable[id=2, brand=Lamborghini, model=Gallardo, horsePower=320, price=120000.0]
3.8 Stream sorted(Comparator comparator) Example
This method is just like the previous one but is a lot more flexible since you can pass an inline comparator to choose the sorting rule you might like to use.
Let’s say you’d like cars to be sorted in descending order by the following criteria:
- price
- horsePower
- brand
To achieve that, it would be simple as that:
Stream.of( new Car(1, "Opel", "Astra", 105, 20_000), new Car(2, "Lamborghini", "Gallardo", 320, 120_000), new Car(3, "Mercedes", "C180", 200, 40_000), new Car(4, "BMW", "Z4", 180, 40_000), new Car(5, "Opel", "Mokka", 180, 40_000) ) .sorted(Comparator .comparing(Car::price) .thenComparing(Car::horsePower) .thenComparing(Car::brand) .reversed() ) .forEach(System.out::println);
And the output would be:
Car[id=2, brand=Lamborghini, model=Gallardo, horsePower=320, price=120000.0] Car[id=3, brand=Mercedes, model=C180, horsePower=200, price=40000.0] Car[id=5, brand=Opel, model=Mokka, horsePower=180, price=40000.0] Car[id=4, brand=BMW, model=Z4, horsePower=180, price=40000.0] Car[id=1, brand=Opel, model=Astra, horsePower=105, price=20000.0]
3.9 Stream peek(Consumer action) Example
Stream peek is mainly used for debugging, and as can you infer from its name, you can peek at the state of the stream in order. Additionally, you can use it like forEach
which is void
, in order to transform the state of the elements and then continue using the stream.
Note that since stream operations are lazy, they won’t be run until you collect the result.
Consider the example below:
List<String> models = Stream.of( new Car(1, "Opel", "Astra", 105, 20_000), new Car(2, "Lamborghini", "Gallardo", 320, 120_000), new Car(3, "Mercedes", "C180", 200, 40_000), new Car(4, "BMW", "Z4", 180, 40_000), new Car(5, "Opel", "Mokka", 180, 40_000) ) .filter(car -> car.brand().equals("Opel")) .peek(car -> System.out.println("Filtered car:" + car)) .map(Car::model) .peek(model-> System.out.println("Mapped car object to model:" + model)) .toList();
The output will be the following:
Filtered car:Car[id=1, brand=Opel, model=Astra, horsePower=105, price=20000.0] Mapped car object to model:Astra Filtered car:Car[id=5, brand=Opel, model=Mokka, horsePower=180, price=40000.0] Mapped car object to brand:Mokka
3.10 Stream limit(long maxSize) Example
This method is pretty simple as it allows only maxSize
number of elements in the stream, for example, the following:
Stream.of( new Car(1, "Opel", "Astra", 105, 20_000), new Car(2, "Lamborghini", "Gallardo", 320, 120_000), new Car(3, "Mercedes", "C180", 200, 40_000), new Car(4, "BMW", "Z4", 180, 40_000), new Car(5, "Opel", "Mokka", 180, 40_000) ) .limit(2).forEach(System.out::println);
This will print only the first two elements as the other 3 will be truncated from the stream.
3.11 Stream skip(long n) Example
Stream skip will just ignore the first n
elements and only hold the rest of them so the snippet below:
Stream.of( new Car(1, "Opel", "Astra", 105, 20_000), new Car(2, "Lamborghini", "Gallardo", 320, 120_000), new Car(3, "Mercedes", "C180", 200, 40_000), new Car(4, "BMW", "Z4", 180, 40_000), new Car(5, "Opel", "Mokka", 180, 40_000) ) .skip(4) .forEach(System.out::println);
This will print:
Car[id=5, brand=Opel, model=Mokka, horsePower=180, price=40000.0]
3.12 Stream takeWhile(Predicate predicate)
Stream takeWhile was introduced in Java 9 and it acts differently depending on whether the stream is ordered or unordered:
For ordered streams, it just filters the elements while the predicate returns true
. If at some point the predicate returns false
for an element, this and the rest of the elements will be filtered out. Let’s jump to an example:
Stream.of( new Car(1, "Lamborghini", "Gallardo", 320, 120_000), new Car(2, "Opel", "Astra", 105, 20_000), new Car(3, "Mercedes", "C180", 200, 40_000), new Car(4, "BMW", "Z4", 180, 40_000), new Car(5, "Opel", "Mokka", 180, 40_000) ) .takeWhile(car -> car.price() >= 40_000) .forEach(System.out::println);
So here, the Opel Astra will fail the predicate, and even though the rest of the cars do have price >= $40,000
, the output will be just Car[id=1, brand=Lamborghini, model=Gallardo, horsePower=320, price=120000.0]
When it comes to unordered streams, the behavior is non-deterministic as it can take any subset of the given elements that match the predicate.
3.13 Stream dropWhile(Predicate predicate)
This method does exactly the opposite of takeWhile
, it drops the elements if they match the predicate, and as soon as one element fails the predicate, the rest will be kept in the stream regardless of the predicate result.
So the following snippet:
Stream.of( new Car(1, "Lamborghini", "Gallardo", 320, 120_000), new Car(2, "Opel", "Astra", 105, 20_000), new Car(3, "Mercedes", "C180", 200, 40_000), new Car(4, "BMW", "Z4", 180, 40_000), new Car(5, "Opel", "Mokka", 180, 40_000) ) .dropWhile(car -> car.price() > 20_000) .forEach(System.out::println);
Will lead to a stream without the Lamborghini Gallardo.
4. Terminal Operations of Java 8 Stream API
Terminal operations will lead to an object or primitive that is not of Stream
type. As a result, after applying a terminal operation, you won’t be able to use any of the methods of section 3 without extra steps (e.g. using the stream()
method to retrieve a stream from a list or set).
In the following sections, we’ll see all terminal operations provided by stream API.
4.1 Stream forEach(Consumer consumer) and forEachOrdered(Consumer consumer) Examples
The method forEach
is void
and performs actions on a stream. The difference with forEachOrdered
is noticeable only when using parallel streams as the result will be always the same when it comes to forEachOrdered
, whereas with forEach
it may vary.
System.out.println("Using forEach in simple stream:"); Stream.of("C", "L", "H").forEach(System.out::println); System.out.println("Using forEach in parallel stream:"); Stream.of("C", "L", "H").parallel().forEach(System.out::println); System.out.println("Using forEachOrdered in parallel stream:"); Stream.of("C", "L", "H").parallel().forEachOrdered(System.out::println);
An example of an output of the above would be:
Using forEach in simple stream: C L H Using forEach in parallel stream: L H C Using forEachOrdered in parallel stream: C L H
Moreover, you can perform actions inside forEach
, consider a simple Car POJO
like the record we had in the previous sections, and let’s say we would like to increase the prices of some cars due to inflation:
List<CarPojo> carPojoList = Arrays.asList( new CarPojo("Lamborghini", "Gallardo", 320, 120_000), new CarPojo("Opel", "Astra", 105, 20_000), new CarPojo("Mercedes", "C180", 200, 40_000), new CarPojo("BMW", "Z4", 180, 40_000), new CarPojo("Opel", "Mokka", 180, 40_000) ); carPojoList .stream() .forEach(carPojo -> { switch (carPojo.getBrand()){ case "Opel" -> carPojo.setPrice(carPojo.getPrice() * 1.1); case "Mercedes", "BMW" -> carPojo.setPrice(carPojo.getPrice() * 1.2); case "Lamborghini" -> carPojo.setPrice(carPojo.getPrice() * 1.4); } }); carPojoList.forEach(System.out::println);
After these operations, the result to be printed will have the updated prices:
CarPojo{brand='Lamborghini', model='Gallardo', horsePower=320, price=168000.0} CarPojo{brand='Opel', model='Astra', horsePower=105, price=22000.0} CarPojo{brand='Mercedes', model='C180', horsePower=200, price=48000.0} CarPojo{brand='BMW', model='Z4', horsePower=180, price=48000.0} CarPojo{brand='Opel', model='Mokka', horsePower=180, price=44000.0}
4.2 Stream Object[] to Array and A[] toArray(IntFunction generator) Examples
You can use these two methods to convert a stream to an array. Note that the first one will return an array of type Object
while the second one whatever type you might specify.
Object[] strings = Stream.of("C", "L", "H") .toArray(); String[] stringsArr = Stream.of("C", "L", "H") .toArray(String[]::new);
4.3 Stream Reduce Methods Examples
Reduce methods are used to reduce elements to a simpler value. As reduction methods are quite complex, we’ll just demonstrate some simple examples. If you like to dig in deeper, you can check Java Stream Reduce Tutorial.
Consider the examples below:
//simple reduce without initial value List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream().reduce(Integer::sum).get(); System.out.println(sum); // reduce with initial value int sumWithSeed = numbers.stream().reduce(100,Integer::sum); System.out.println(sumWithSeed); // reduce with initial value and a combiner, allows you to change return value List<Integer> clh = Arrays.asList(1, 2, 3); String result = clh.stream() .reduce("String is: ", (firstNumber, secondNumber) -> firstNumber + secondNumber, String::concat ); System.out.println(result);
The first version just uses a BinaryOperator
to combine the values and produce a result. Note that this version returns an Optional.
The second version works like the first, but it uses a value to start with instead of the default (e.g. for double the default would be 0.0.).
The third version allows you to change the produced result by using a BiFunction
and a BinaryOperator
.
4.4 Stream toList() Example
This method was added in Java 16 and it can replace the method collect(Collectors.toList())
that was previously used.
However, if we dig deeper and use a debugger, we’ll see that the List
produced by Collectors
class will be shown as ArrayList while the List
produced by toList()
will be shown as ImmutableCollections.List
which is unmodifiable.
Indeed, if you try to run the following snippet:
List<String> listCollectors = Stream.of("C", "L", "H").collect(Collectors.toList()); List<String> list = Stream.of("C", "L", "H").toList(); listCollectors.add("A"); list.add("A");
You will get:
Exception in thread "main" java.lang.UnsupportedOperationException
In the second addition where toList()
was used.
4.5 Stream min(Comparator comparator) and max(Comparator comparator) Example
Both of these methods accept a comparator which will determine the criteria for which an object is considered bigger than the other. Both will return an Optional
which will contain the maximum or minimum element, otherwise an empty Optional
if the stream is empty. Additionally, if 2 maximums are found, one will be returned.
Let’s see an example where we find the cheapest and the most expensive car:
List<Car> list = Arrays.asList( new Car(1L,"Lamborghini", "Gallardo", 320, 120_000), new Car(2L, "Opel", "Astra", 105, 20_000), new Car(3L, "Mercedes", "C180", 200, 40_000), new Car(4L, "BMW", "Z4", 180, 40_000), new Car(5L, "Opel", "Mokka", 180, 40_000) ); Car expensive = list.stream().max(Comparator.comparing(Car::price)).orElse(null); Car cheap = list.stream().min(Comparator.comparing(Car::price)).orElse(null); System.out.println("Most expensive car is: "+expensive); System.out.println("Cheapest car is: "+cheap);
The output will be:
Most expensive car is: Car[id=1, brand=Lamborghini, model=Gallardo, horsePower=320, price=120000.0] Cheapest car is: Car[id=2, brand=Opel, model=Astra, horsePower=105, price=20000.0]
4.6 Stream count Example
Stream count
will return a long value equivalent to the total elements that this stream has.
List<Car> list = Arrays.asList( new Car(1L,"Lamborghini", "Gallardo", 320, 120_000), new Car(2L, "Opel", "Astra", 105, 20_000), new Car(3L, "Mercedes", "C180", 200, 40_000), new Car(4L, "BMW", "Z4", 180, 40_000), new Car(5L, "Opel", "Mokka", 180, 40_000) ); System.out.println(list.stream().count()); // Will print 5
4.7 Stream anyMatch(Predicate), allMatch(Predicate) and noneMatch(Predicate) Examples
All of these methods accept a Predicate
and return a boolean:
anyMatch()
will returntrue
if at least one element matches the predicate.allMatch()
will returntrue
if all elements match the predicate.noneMatch()
will returntrue
if none of the elements match the predicate.
Below you can find some examples:
List<Car> list = Arrays.asList( new Car(1L,"Lamborghini", "Gallardo", 320, 120_000), new Car(2L, "Opel", "Astra", 105, 20_000), new Car(3L, "Mercedes", "C180", 200, 40_000), new Car(4L, "BMW", "Z4", 180, 40_000), new Car(5L, "Opel", "Mokka", 180, 40_000) ); boolean allHavePriceLessThan150k = list .stream(). allMatch(car -> car.price() < 150_000); //true boolean atLeastOneIsLamborghini = list .stream() .anyMatch(car -> car.brand().equals("Lamborghini")); //true boolean noneIsFerrari = list .stream() .noneMatch(car -> car.brand().equals("Ferrari")); //true
4.8 Stream findFirst() and findAny() Examples
Both methods return an Optional
, since the stream could be empty.
findFirst()
will return the first element.findAny()
is non-deterministic (it won’t produce the same result each time is run) and it is mainly used to maximize performance in parallel operations.
Finally, you can notice the difference when using parallel streams as shown below:
var any = Stream.of("C", "L", "H") .parallel() .findAny() .orElse(null); var first = Stream.of("C", "L", "H") .parallel() .findFirst() .orElse(null); System.out.print(any); System.out.print(first);
In this case, the result will be "LC"
.
4.9 Stream collect Methods
Stream collect
methods are used to convert the stream to another object or data structure. Stream API provides two collect
methods:
R collect(Supplier supplier,BiConsumer accumulator,BiConsumer combiner)
, which does not use the Collectors class.- <R, A> R collect(Collector<? super T, A, R> collector), which accepts a collector that we can create by calling methods of
Collectors
class.
4.9.1 Stream R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner) Example
This method accepts 3 parameters:
- A
Supplier
, which is used to choose the object or the data structure that our stream will be converted to. - A
BiConsumer
, which is the accumulator i.e. the operation that will be applied in order to fill the object or data structure (the first parameter will be the DS and the second the element to be added). - A
BiConsumer
, which is the combiner i.e. the operation that we will use to combine all the values produced by the accumulator.
Consider the example below where we convert a stream of cars to a Map
of Ids as keys and models as values:
Map<Long, String> carMap = Stream.of( new Car(1L,"Lamborghini", "Gallardo", 320, 120_000), new Car(2L, "Opel", "Astra", 105, 20_000), new Car(3L, "Mercedes", "C180", 200, 40_000), new Car(4L, "BMW", "Z4", 180, 40_000), new Car(5L, "Opel", "Mokka", 180, 40_000) ).collect( HashMap::new, (map, car) -> map.put(car.id(), car.model()), HashMap::putAll );
Of course, you can do the same with any data structure or even a StringBuilder
as shown below:
String combined = Stream.of("Code", "Learn", "Hub", "is", "the", "best.") .collect(StringBuilder::new, (sb, aString) -> sb.append(aString).append(" "), StringBuilder::append ).toString(); System.out.println(combined);
The output will be:
Code Learn Hub is the best.
4.9.2 Stream R collect(Collector collector) Examples
This method accepts a collector which you can retrieve by using methods of the Collectors class.
Some of the most used methods are the following:
Collectors.toMap()
, which allows you to convert your stream to a map. Should you like to learn about collectors to Map in-depth, take a look at our Collectors to Map in Java article.Collectors.toList()
, which converts the stream to a list and more specifically an ArrayList (We already saw an example back in section 4.4).Collectors.toSet()
, which converts the stream to a set and more specifically a HashSet.Collectors.toCollection
(), which allows you to specify exactly which implementation of theSet
orList
interface you want.Collectors.joining
(), which converts a stream of string to a string.
And many more aggregate functions that will not be analyzed in-depth in this article.
Before we begin, consider that we have the following list of cars:
List<Car> cars = Arrays.asList( new Car(1, "Opel", "Astra", 105, 20_000), new Car(2, "Lamborghini", "Gallardo", 320, 120_000), new Car(3, "Mercedes", "C180", 200, 40_000), new Car(4, "Opel", "Insignia", 120, 28_000), new Car(5, "VW", "Polo", 92, 17_000), new Car(6, "BMW", "M3", 250, 80_000) );
Now we’ll see some self-explanatory examples of various methods provided by Collectors
class.
4.9.2.1 Collectors.toList() Example
//Map cars to models and collect them to a list List<String> models = cars .stream() .map(Car::model) .collect(Collectors.toList());
4.9.2.2 Collectors.toSet() Example
//Map cars to brands and convert them to set, will remove duplicates Set<String> brandsSet = cars .stream() .map(Car::brand) .collect(Collectors.toSet());
4.9.2.3 Collectors.toCollection() Example
// Use toCollection to produce a LinkedHashSet with brands LinkedHashSet<String> brandsLinkedHashSet = cars .stream() .map(Car::brand) .collect(Collectors.toCollection(LinkedHashSet::new));
4.9.2.4 Collectors.joining() Example
// Use joining to reduce strings to one string String brandsString = cars .stream() .map(Car::brand) .distinct() .collect(Collectors.joining(",")); // Will print Opel,Lamborghini,Mercedes,VW,BMW
4.9.2.5 Collectors.averagingDouble() Example
//Get average price double averagePrice = cars .stream() .collect(Collectors.averagingDouble(Car::price));
4.9.2.6 Collectors.filtering() Example
// Filter all the cars that cost > $50k List<Car> carsMoreThan50k = cars .stream() .collect( Collectors.filtering( car -> car.price() > 50_000, Collectors.toList()) );
4.9.2.7 Collectors.groupingBy() Example
// Group cars by brand Map<String, List<Car>> carsGroup = cars .stream() .collect( Collectors.groupingBy( Car::brand, Collectors.toList() ) );
4.9.2.8 Collectors.partitioningBy() Example
//Create two partions, one with cars <=50k price and one with >50k price Map<Boolean, List<Car>> carsPartitionByPrice = cars .stream() .collect(Collectors.partitioningBy(car -> car.price() > 50_000));
5. A Visualization of Java 8 Stream
List<Car> cars = Arrays.asList( new Car(1, "Opel", "Astra", 105, 20_000), new Car(2, "Lamborghini", "Gallardo", 320, 120_000), new Car(3, "Mercedes", "C180", 200, 40_000), new Car(4, "Opel", "Insignia", 120, 28_000), new Car(5, "VW", "Polo", 92, 17_000), new Car(6, "BMW", "M3", 250, 80_000) ); String[] modelsArray = cars.stream() .filter(car -> car.price() > 50_000) .sorted(Comparator.comparing(Car::price).reversed()) .map(Car::model) .toArray(String[]::new);
We can use the stream trace provided by Intellij IDEA to visualize the following stream processing and see how the magic happens:
![Stream Trace from Intellij IDEA](https://youlearncode.com/wp-content/uploads/2022/10/Screenshot_1-1024x392.png)
6. Conclusion
By now, you should have a good grasp of how streams work in Java and how to take full advantage of all the methods offered by Java Stream API. You can find the source code on our GitHub page.