Java streams are sequences of data elements that can be processed with operations.
Streams consist of 3 parts: a source, zero or more intermediate operations, and a terminal operation.
In the meanwhile there is a data in pipe, another data is waiting its turn. For instance, for a data in terminal operation there can be a data in intermediate operation.
If we remove the terminal operation nothing happens because streams don’t hold datas.
We operate on streams with something called “intermediate operations”.
Integer[] myNums = {1, 2, 3};
Stream<Integer> myStream = Arrays.stream(myNums);
long numElements = myStream
.filter((i) -> i > 1) // an intermediate operation
.count(); // a terminal operation
We can run a stream only one time but it is a lightweight object then there aren’t problem to re-create it.
With a terminal operation we can’t re-use that Stream or we’ll have an IllegalStateException.
We can assign another stream:
numElements = Arrays.stream(myNums)
.filter((i) -> i > 2)
.count();
Note that we used a Predicate to filter the array elements (with its test()
method).
In Java, a Stream is an object that gets data from a source but it doesn’t store nothing of those datas.
We’ll usually use streams to work on collections, arrays, or files, that will be our source of data. All collections have, like super class, Collection with a default method for streams:
default Stream<E> stream();
So we can directly create a stream over a list.
List<Double> aList = Arrays.asList(12.0, 18.2, 21.6);
aList.stream()
.filter(t -> t > 15.0)
.count();
Map doesn’t extends Collection so we have to convert it to a Set with entrySet()
(that extends Collection).
It creates a Set list of Entry objects with two generic objects, one like key and one like value: Entry<T, V>
.
Map<String, Integer> myMap = ne HashMap<>();
// ...add elements to myMap
myMap.entrySet()
.stream()
.filter(d -> d.getValue() > 4)
.count();
We can also build a Stream with of()
method:
static <T> Stream<T> of(T... values);
with an “on-the-fly” array.
We can create, for instance:
Stream<Integer> myStream = Stream.of(1, 2, 3);
Java Streams from a File
It’s easy to get lines from a file with streams, with this method on Files:
public static Stream<String> lines(Path path) throws IOException;
And applying a forEach()
method on it.forEach()
is a terminal operation method, has void like return (not returns a stream).
try ( Stream<String> stream = Files.lines(Path.get(filename)) ) {
stream.forEach(line -> {
list.add(line); // a generic list instance for our example.
});
} catch (IOException e) { e.printStackTrace(); }
Primitive streams
There are also streams for primitives: DoubleStream
, IntStream
, LongStream
.
They are different from, for instance, a Stream<Double>
because they haven’t to type the output. They are obviusly only for arrays or direct values in of()
.
Using Java streams
We’ve already seen filter()
that takes a Predicate.
We can use the expression so called: “map-filter-reduce” to describe most of all operations we can do with streams.
We have also the map()
method that is another intermediate operation. map()
transforms elements of a stream and takes a Function, that has how purpose to transform a value (with apply()
method of the functional interface Function).
Reduce part is both of a method reduce()
and other specific methods like count()
, sum()
, and average()
.
All reductions are terminal operations.
The basic reduce()
method takes a BiFunction.
We can think to reduce as a way to “accumulate” a result.
For instance we can take the square of a list, test whether the square is greater than 20, and count them.
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
long result = nums.stream()
.map( n -> n * n )
.peek( n -> System.out.println(n) )
.filter( n -> n > 20 )
.count();
If we want to track values in pipeline we can use the peek()
method that takes a Consumer and doesn’t modify the object. We can’t keep track of the index because it doesn’t fit the pattern.
If we have, for instance, a list of objects that have a double field we can’t use map()
method (that has a Function for objects in input) but we use mapToDouble()
that can convert to double our flow.
It takes a ToDoubleFunction, whose applyAsDouble()
method takes an object and returns a double.
Then w can filter over these double values.
class DoubleObject {
int year;
double value;
}
// ...create a doubleObjectList list with DoubleObject.
doubleObjectList.stream()
.mapToDouble( r -> r.value )
.filter( v -> v >= 3.14 && v < 3.15 )
// Do nothing, why?
At this point stream waits for a terminal operation and nothing happens. One other example of terminal operations is average()
that returns an OptionalDouble. It’s not sure to return or not a value because it can’t be sure that something arrives from the stream, maybe because all is filtered.
When w have our average: OptionalDouble avg
we can:
System.out.println(avg); // Example output: OptionalDouble[3.145]
// or get its value:
avg.getAsDouble(); // Example output: 3.145
But we must be sure that it isn’t empty or we’ll get a NoSuchElementException.
if (avg.isPresent()) { ... }
Pay attenction to some reductions that return Optional<Double>
and not OptionalDouble
, with its simple map()
and not mapToDouble()
method.
Other terminal reduce methods with OptionalDouble are min()
, max()
and sum()
but sum()
uses only Double
(or double
, with mapToDouble()
) because with an empty stream we can have a simple 0.0
value.
All these methods are natives of JDK but we can also produce our custom reduce()
method.
If we want, for instance, make a sum that returns an empty OptionalDouble we can use a reduce()
that takes a BinaryOperator:
OptionalDouble reduce( DoubleBinaryOperator op );
// Using it:
readings.stream()
.mapToDouble( r -> r.value )
.reduce( (v1, v2) -> v1 + v2 );
That returns the sum in OptionalDouble style.
There are also reduce methods that accept a default value and return a primitive.
For instance:
double reduce( double identity, DoubleBinaryOperator dbo );
// Using it:
double sum = readings.stream()
.mapToDouble( r -> r.value )
.reduce( 0.0, (v1, v2) -> v1 + v2 );
The same you can find for IntStream and LongStream.
Searching with streams
We have terminal operations to search. They take a Predicate and return a boolean.
boolean exists = dogs.stream()
.filter( ... )
.anyMatch( d -> d.getName().startsWith("c") );
Returns true
if there is at least one matches.
.allMatch( a -> a > 5 );
Returns true
if all objects match.
.noneMatch( n -> n.equals("red") );
Returns true
if none matches.
If we want to return the found object we can use findAny()
that returns an Optional<T>
.
Optional<Dog> dog = dogs.stream()
.filter( d -> d.getAge() > 5 )
.peek( System.out::println )
.findAny();
There is no guarantee there is only one result but findAny()
stops when it find a result.
All these methods stop when find the result so if you try to add peek()
you’ll print only the first result.
Sorting with streams
Streams have sorted()
method to use Comparable or Comparator for sorting.
If that class is Comparable we can simply use:
dogs.stream()
.sorted()
.forEach(System.out::println);
If we want to compare with a custom comparator we take it on sorted (like functional interface).
dogs.stream()
.sorted( (d1, d2) -> d1.getAge() - d2.getAge() )
.forEach(System.out::println);
When can also use: comparing()
, reversed()
, and thenComparing()
to use a getter with a comparator.
Comparator<Dog> byName = Comparator.comparing(Dog::getName);
// "Dog::getName" is like "d -> d.getName()"
There is also an operation to be sure there aren’t duplicates: distinct()
dogs.stream()
.map( d -> d.getName() )
.distinct()
.forEach( System.out::println );
Pay attenction to never modify the source of a stream. We can collect them in a new list.
Collecting with streams
List<Dog> heavyDogs = dogs.stream()
.filter( d -> d.getWeight() >= 50 )
.collect( Collectors.toList() );
Collectors is an helper class. With toList()
method it doesn’t guarantee that we’ll have a list of type ArrayList. If you want an ArrayList you can use Collectors.toCollection(ArrayList::new)
.
We are passing a method reference to the toCollection()
method, a constructor method reference.
ArrayList::new
// is equals to...
new ArrayList<Dog>()
The toCollection()
method takes a Supplier (a functional interface that returns only something).
Using it on Files.lines()
we can collect file lines:
try (Stream<String> stream = Files.lines(Path.get(filename))) {
List<String> data = stream.collect(Collectors.toList());
data.forEach(System.out::println);
}
We can use collect()
with Collectors to group a list to a map, based on a field of the object in the list.
Map<String, List<Person>> peopleByName = people.stream()
.collect(Collectors.groupingBy(Person::getName));
And we can also count them like values in that map:
Collectors.groupingBy(Person::getAge, Collectors.counting())
We’ll have this kind of map:
{ 32 = 1, 35 = 2, 38 = 3, 30 = 1 }
Pay attenction counting will return Long.
We can also map in map (to a field):
Collectors.groupingBy(
Person::getAge,
Collectors.mapping(
Person::getName,
Collectors.toList()
)
)
The groupingBy()
goes in a collect()
method.
{ 3 = [Pablo], 35 = [Wendi, Bill], ... }
All those method references (Person::getAge, Person::getName) are instance method references that refer to a function of this kind:p -> p.getAge()
And return an Integer for this scenario (getAge).
We can also sum map values inside groupingBy that it is inside collect()
:
Collectors.groupingBy(
Person::getName,
Collectors.summingInt(
Person::getAge
)
)
With this example we sum all ages and group them in a map. There are also summingLong()
, summingDouble()
.
We can also compute the average inside groupingBy that it is inside collect()
:
Collectors.groupingBy(
Person::getName,
Collectors.averagingInt(
Person::getAge
)
)
Pay attenction that averagingInt()
reduces ever to a Double, and we have as return: Map<String, Double>
We can also join all in one string:
people.stream()
.map(Person::getName)
.collect(Collectors.joining(","));
We can also reduce to maxBy()
or minBy()
, that get a Comparator:
people.stream()
.collect(Collectors.maxBy( (p1, p2) ->
p1.getAge() - p2.getAge() ));
It returns an Optional so you can get nothing if stream is empty.
That’s all for Java streams.
Try it at home!