Stream interface supports a filter method which takes a predicate (a function returning boolean) as argument.
filter method will return a stream including all elements that match the predicate
List<Dish> vegMenu = menu.stream()
.filter(Dish::isVeg) // Dish::isVeg is a predicate which returns true for dishes that are veg
.collect(toList());
Stream interface also supports a distinct method which returns a stream with unique elements (this will depend on the implementation of hashcode and equals methods of the objects of the stream)
filter method above will need to iterate through the whole stream.
If the stream is already sorted, we can just stop iterating once the predicate condition is satisfied
takeWhile method will slice any stream using predicate by stopping iteration once an element not satisfying the predicate is found.
List<Dish> slicedMenu = specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
// will stop iterating if dish with calories above 320 is found
.collect(toList());
dropWhile method is the complement of takeWhile - it drops any element satisfying the predicate, and will return all the remaining elements once the predicate is not satisfied.
List<Dish> slicedMenu = specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
// will drop dishes that has calories lower than 320,
// and return the remaining elements if calories is greater or equal than 320
.collect(toList());
limit(n) will select only the first n elements from the stream and return those immediately.
skip(n) will return a stream skipping the first n elements
Mapping
Stream API's mapmethod takes a function as argument.
The function is applied to each element, which will map each element into a new element.
Depending on the function, it will change the data type of the elements in the stream.
List<Integer> dishNameLengths = menu.stream() // a stream of Dish
.map(Dish::getName) // now a stream of String
.map(String::length) // now a stream of Integer
.collect(toList());
We can flatten a stream using flatMap method.
In below, all the separate streams are flattened into a single stream.
List<String> uniqueCharacters = words.stream()
.map(word -> word.split("")) // each word is converted into an array
.flatMap(Arrays::stream) // flattens each stream of array into a single stream of characters
.distinct()
.collect(toList());
Modern Java in Action
Finding and Matching
Another common use case of data processing with streams is finding elements that match a certain condition.
anyMatch method will return true if at least one element in the stream satisfies the given predicate - therefore anyMatch is a terminal operation.
if(students.stream().anyMatch(Student::isMale)) {
System.out.println("We have a male student");
}
allMatch method will return true if all elements in the stream satisfy the given predicate
Unlike above findAny method, findFirst method will return the first element from the stream
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = numbers.stream()
.map(n -> n*n) // square each number
.filter(n -> n%3 == 0) // leave only those that are divisible by 3
.findFirst(); // 9
Reducing
Used to reduce the stream into Optional<T>
3 Main components: Identity, Accumulator, Combiner
Identity: the initial value of the reduction operation
Accumulator: a function that takes 2 arguments - the result is the partial result of the reduction, and will be the next element in the stream
Combiner: a function used to combine the 2 partial results - not necessarily needed if sequential streams are used (not parallel) and the types of the accumulator arguments and the types of its implementation match
Here the initial value is 0, and the accumulator is (a,b) -> a+b
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
If we do not have an initial value, the reduction operation cannot return a sum with an empty stream, so Optional<Integer> will be returned
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
Below reduction operation will reduce a list (stream) of strings into a single string
For parallel streams, we need a function (combiner) to combine the partial results of the substreams into a single one
int sum = numbers.parallelStream()
.reduce(0, (a, b) -> a + b, Integer::sum); // Integer::sum is the combiner
We also need a combiner when the types of stream objects and accumulator parameters mismatch.
int result = users.stream()
.reduce(0, (partialAge, user) -> partialAge + user.getAge(), Integer::sum);
// partialAge is int, while user is User
// Integer::sum is used as combiner to resolve this mismatch