In this blog, we are going to take a closer look at the Java 8 Streams API. We will mainly do so by means of examples. We will cover many operations and after reading this blog, you will have a very good basic understanding of the Streams API. Enjoy reading!
1. Introduction
The Streams API has been introduced in Java 8 and has been part of the Java language specification for several years by now. Nevertheless, it might be that Streams seem a bit magical to you. Therefore, we will cover the basic concepts of Streams and explain them with some examples.
It all starts with the java.util.stream
package. The offical Java documentation for this package which contains some good reference documentation, can be found here. Some characteristics of Streams (taken from the official documentation) compared to Collections are:
- Streams do not store data. Instead, they are a source for data.
- Streams are functional in nature and this is probably why Streams are difficult to comprehend for most people. They produce a result, e.g. like a sum or a new Stream.
- Streams are lazyness seeking. Operations can be intermediate or terminal. We will cover both in the next sections. Intermediate operations are always lazy. E.g. find the first occurrence of an element in the Stream. It is not necessary to inspect all elements of the Stream. Once the first occurrence has been found, the search can be ended.
- Possibly unbounded. As long as the Stream produces data, the Stream will not end.
- The elements of a Stream can only be visited once, just like an
Iterator
.
In the next sections we will cover how to create Streams, cover some intermediate operations and finally we will cover some terminal operations. The sources are available at GitHub.
2. Create a Stream Source
There are several ways for creating Streams. They can be created from a Collection, from an Array, from a file, etc. In this section, we will create some tests which will show you how to create Streams in varying ways. We will create the Stream
and use the terminal operation collect
in order to consume the Stream
into a List
. We do not perform any other operations on the Stream
yet, we leave that for the other sections. At the end, we assert whether the List
is identical to what we expect. The tests are located in the MyJavaStreams
unit test.
2.1 Create a Stream From a Collection
Assume having a List
of Strings
named stringsList
. We can just call the stream
method of the List
and then collect the results into a new List
. Fairly simple and both lists are equal of course.
private static final List<String> stringsList = Arrays.asList("a", "b", "c");
@Test
public void createStreamsFromCollection() {
List<String> streamedStrings = stringsList.stream().collect(Collectors.toList());
assertLinesMatch(stringsList, streamedStrings);
}
2.2 Create a Stream From Arrays
We can use Arrays.stream
which takes an Array
as an argument in order to create a Stream
.
@Test
public void createStreamsFromArrays() {
List<String> streamedStrings = Arrays.stream(new String[]{"a", "b", "c"}).collect(Collectors.toList());
assertLinesMatch(stringsList, streamedStrings);
}
2.3 Create a Stream From Stream.of
We can use Stream.of
which takes the elements of the Stream
as arguments in order to create a Stream
.
@Test
public void createStreamsFromStreamOf() {
List<String> streamedStrings = Stream.of("a", "b", "c").collect(Collectors.toList());
assertLinesMatch(stringsList, streamedStrings);
}
2.4 Create a Stream From IntStream
We can use IntStream.of
which takes primitive integer values as arguments in order to create a Stream
. DoubleStream.of
and LongStream.of
also can be used. Note that we use the terminal operation toArray
in this case, this will return an array containing the elements of the Stream
.
@Test
public void createStreamsFromIntStream() {
int[] streamedInts = IntStream.of(1, 2, 3).toArray();
assertArrayEquals(new int[]{1, 2, 3}, streamedInts);
}
2.5 Create a Stream From a File
We can create a Stream
from a file inputFile
. The lines
method of the BufferedReader
class will stream the lines.
@Test
public void createStreamsFromFile() {
try {
List<String> expectedLines = Arrays.asList("line1", "line2", "line3");
BufferedReader reader = new BufferedReader(new FileReader(new File("test/com/mydeveloperplanet/inputfile.txt")));
List<String> streamedLines = reader.lines().collect(Collectors.toList());
assertLinesMatch(expectedLines, streamedLines);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
3. Intermediate operations
Streams become interesting when we can perform operations to them. The first category are intermediate operations. These operations return a new Stream
but do not return a final result. The intermediate operations can be categorized into stateless operations, retaining no information about the previously processed element, and stateful operations which may need to process all of the elements before producing an intermediate result.
The complete list of operations which can be invoked, can be found in the Streams API.
In the folowing examples, we will make use of a Car
object containing four parameters: id
, brand
, type
and color
. The Car
object also contains getters and implementations for equals
, hashCode
and toString
, which we do not list here for brevity, but it should be clear what these do.
public class Car {
private int id;
private String brand;
private String type;
private String color;
public Car (int id, String brand, String type, String color) {
this.id = id;
this.brand = brand;
this.type = type;
this.color = color;
}
...
}
In the unit test, we define four Car
objects which we will use in the next examples:
private static final Car volkswagenGolf = new Car(0, "Volkswagen", "Golf", "blue");
private static final Car skodaOctavia = new Car(1, "Skoda", "Octavia", "green");
private static final Car renaultKadjar = new Car(2, "Renault", "Kadjar", "red");
private static final Car volkswagenTiguan = new Car(3, "Volkswagen", "Tiguan", "red");
3.1 Stateless Operations
3.1.1 The Filter Operation
The filter
operation allows us to create a new Stream
based on a given Predicate
. In the example, we first create a Stream
of the 4 cars and create a new Stream
with only the Volkswagen cars.
@Test
public void filterStream() {
List<Car> expectedCars = Arrays.asList(volkswagenGolf, volkswagenTiguan);
List<Car> filteredCars = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.filter(car -> car.getBrand().equals("Volkswagen"))
.collect(Collectors.toList());
assertIterableEquals(expectedCars, filteredCars);
}
3.1.2 The map Operation
In all the previous examples, the types in the source Stream
and the resulting Stream
were always identical. Often, you want to apply a function on each element. E.g. if we want the resulting Stream
to only contain the brands instead of the Car
objects, we can use the map
operation and apply the getBrand
method to each element which results in a new Stream
with only the brand names.
@Test
public void mapStream() {
List<String> expectedBrands = Arrays.asList("Volkswagen", "Skoda", "Renault", "Volkswagen");
List<String> brands = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.map(Car::getBrand)
.collect(Collectors.toList());
assertIterableEquals(expectedBrands, brands);
}
3.1.3 Combine filter and map operation
Of course, it is perfectly valid to combine operations and chain them in a Stream
pipeline. In the next example, we filter in order to retrieve the Volkswagen cars and then use the map
operation in order to retrieve only the colors. This way, we end up with a list containing the colors of the Volkswagen cars.
@Test
public void filterMapStream() {
List<String> expectedColors = Arrays.asList("blue", "red");
List<String> volkswagenColors = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.filter(car -> car.getBrand().equals("Volkswagen"))
.map(Car::getColor)
.collect(Collectors.toList());
assertIterableEquals(expectedColors, volkswagenColors);
}
3.2 Stateful Operations
3.2.1 The distinct Operation
The distinct operation will return a Stream
containing only distinct elements based on equals
method implementation of the elements. In the next example, we first retrieve a Stream
of the brands and then perform the distinct
operation to it. This results in a list of the three distinct brands we used.
@Test
public void distinctStream() {
List<String> expectedBrands = Arrays.asList("Volkswagen", "Skoda", "Renault");
List<String> brands = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.map(Car::getBrand)
.distinct()
.collect(Collectors.toList());
assertIterableEquals(expectedBrands, brands);
}
3.2.2 The sorted Operation
The sorted
operation will sort the elements of the Stream
according to their natural order. It is also possible to use a Comparator
as an argument in order to have a more custom sorting. The next example will retrieve the brands from the Stream
and sort them alphabetically.
@Test
public void sortedStream() {
List<String> expectedSortedBrands = Arrays.asList("Renault", "Skoda", "Volkswagen", "Volkswagen");
List<String> brands = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.map(Car::getBrand)
.sorted()
.collect(Collectors.toList());
assertIterableEquals(expectedSortedBrands, brands);
}
3.3 Peek For Debugging Purposes
The last intermediate operation we will discuss is the peek
operation. This is a special one and is mainly intended to be used for debugging purposes. Peek
will execute an action on the element when it is being consumed. Let’s take the combined filter and map example and add some peek
operations to it in order to print the elements being consumed.
@Test
public void peekStream() {
List<String> expectedColors = Arrays.asList("blue", "red");
List<String> volkswagenColors = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.filter(car -> car.getBrand().equals("Volkswagen"))
.peek(e -> System.out.println("Filtered value: " + e))
.map(Car::getColor)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
assertIterableEquals(expectedColors, volkswagenColors);
}
The output of this test is:
Filtered value: Car{id=0, brand='Volkswagen', type='Golf', color='blue'}
Mapped value: blue
Filtered value: Car{id=3, brand='Volkswagen', type='Tiguan', color='red'}
Mapped value: red
4. Terminal Operations
In this section, we will cover some terminal operations.
4.1 The collect Operation
We already used the collect
terminal operation in all of the previous examples. We always used the Collectors.toList
argument in the collect
operation. There are more options to use though. We will cover some in the next examples. The complete list can be found in the JavaDoc.
With Collectors.joining
, we can concatenate the elements as String
separated by a delimiter.
@Test
public void collectJoinStream() {
String expectedBrands = "Volkswagen;Skoda;Renault;Volkswagen";
String joinedBrands = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.map(Car::getBrand)
.collect(Collectors.joining(";"));
assertEquals(expectedBrands, joinedBrands);
}
With Collectors.summingInt
, we can compute the sum of properties of elements. In our example, we just compute the sum of the id’s of the Cars.
@Test
public void collectSummingIntStream() {
int sumIds = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.collect(Collectors.summingInt(Car::getId));
assertEquals(6, sumIds);
}
With Collectors.groupingBy
, we can group elements in a Map
. In our example, we group the elements based on the brand.
@Test
public void collectGroupingByStream() {
Map<String, List<Car>> expectedCars = new HashMap<>();
expectedCars.put("Skoda", Arrays.asList(skodaOctavia));
expectedCars.put("Renault", Arrays.asList(renaultKadjar));
expectedCars.put("Volkswagen", Arrays.asList(volkswagenGolf, volkswagenTiguan));
Map<String, List<Car>> groupedCars = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.collect(Collectors.groupingBy(Car::getBrand));
assertTrue(expectedCars.equals(groupedCars));
}
4.2 The reduce Operation
The reduce
operation performs a reduction on the elements of a Stream
. It uses an identity (i.e. starting value) and an accumalator function which performs the reduction. In our example, we perform a summation over the id’s of the elements, just like we did when we used the Collectors.summingInt
collect
operation.
@Test
public void reduceStream() {
int sumIds = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.map(Car::getId)
.reduce(0, Integer::sum);
assertEquals(6, sumIds);
}
4.3 The forEach Operation
The forEach
operation performs a function to every element. It is quite similar to the intermediate peek
operation.
@Test
public void forEachStream() {
Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.forEach(System.out::println);
}
The outcome of this test is the following:
Car{id=0, brand='Volkswagen', type='Golf', color='blue'}
Car{id=1, brand='Skoda', type='Octavia', color='green'}
Car{id=2, brand='Renault', type='Kadjar', color='red'}
Car{id=3, brand='Volkswagen', type='Tiguan', color='red'}
4.4 The count operation
The count
operation counts the number of elements in the Stream
.
@Test
public void countStream() {
long count = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan).count();
assertEquals(4, count);
}
4.5 The max Operation
The max
operation returns the maximum element of the Stream
based on the given Comparator
. In our example, we create a Stream
of the id’s and retrieve the element with the highest id. Remark that this returns an Optional
. We just provided an alternative value when the highest id could not be determined.
@Test
public void maxStream() {
int maxId = Stream.of(volkswagenGolf, skodaOctavia, renaultKadjar, volkswagenTiguan)
.map(Car::getId)
.max((o1, o2) -> o1.compareTo(o2))
.orElse(-1);
assertEquals(3, maxId);
}
5. Conclusion
The Java Streams API is very powerful and not very difficult to learn. It is very readable and removes boiler plate code. If you are not yet acquainted with the Streams API, just play around with the provided examples and you will be up to speed in no time. Do check out the great IntelliJ feature for debugging Java Streams.