Understanding Java Streams
What is a Stream?
- A stream in Java is a sequence of data that takes input from Collections or IO Channels.
- Streams don’t change the original data structure.
- A Stream Pipeline is the operation (STREAM OPERATIONS) that run on a stream to produce a result.
- Each intermediate operation is lazily executed and returns a stream as a result.
- Terminal operations mark the end of the stream and return the result.
- Finite Streams have a limit.
- Infinite Streams are like sunrise/sunset cycles.
Important Notes
- Re-stream a List each time: Don’t re-use a stream because “creating” a Stream just points at the existing data structure behind the scenes; it does not copy the data.
-
Do not support indexed access:
findFirst()
can give the top element, but not the second, third, or last element. - Simple syntax to build a List or array from a Stream.
Three Common Ways to Create a Stream
-
From ArrayList
List<Student> students = new ArrayList<>(); Stream<Student> studentStream = students.stream();
-
From Array of Objects (not array of primitives)
Student[] students = {....}; Stream<Student> studentStream = Stream.of(students).map(…).filter(…).other(…); // No Terminal Operator
-
From Individual Elements
Student s1 = …; Student s2 = …; Stream<Student> studentStream = Stream.of(s1, s2, ...).map(…).filter(…).other(…); // No Terminal Operator
Outputting Streams
Getting a List out of a Stream
List<SomeClass> list = someStream.map(…).collect(Collectors.toList());
Getting an Array out of a Stream
// Fill elements into an Array from a Stream
Student[] studentArray = someStream.map(someLambda).toArray(Student[]::new);
String[] strArray = stringStream.filter(...).map(...).toArray(String[]::new);
What Cannot be Done with Streams forEach
-
Multiple Loops are not possible as
forEach
is a Terminal operation consuming a Stream. -
Local variable modification:
list.stream().forEach(e -> total += e);
- Do this with
map
andreduce
. - Or use the built-in
sum
method ofDoubleStream
orIntStream
.
- Do this with
-
Cannot use
break
orreturn
withinforEach
loop.
Stream Operations
Source
- SOURCE: Where the stream comes from.
Intermediate Operations
-
INTERMEDIATE OPERATIONS: Transforms the stream into another stream. STREAMS USE LAZY EVALUATION.
- map: Returns a stream consisting of the results of applying the given function to the elements of this stream.
- filter: Selects elements as per the Predicate passed as argument.
- sorted: Sorts the stream.
- filter()
- map()
- flatMap()
- distinct()
- limit()
- peek()
Terminal Operations
-
TERMINAL OPERATION: Actually produces a result. Stream becomes invalid after terminal operation.
- collect: Returns the result of the intermediate operations performed on the stream.
- forEach: Iterates through every element of the stream.
- reduce: Reduces the elements of a stream to a single value. Takes a BinaryOperator as a parameter.
- anyMatch()
- allMatch()
- noneMatch()
- collect()
- count()
- findAny()
- findFirst() - returns Optional
- forEach()
- min()
- max()
- reduce()
- toArray()
Map vs FlatMap
Employee with Address Object
@Data
class Address {
private String city;
}
Using map
Scenario: Transforming a list of Employee
objects to a list of their names.
class Employee {
private Address address;//One Address Object
}
public void testMain(List<Employee> employees) {
//Extract the address of every employhee
List<Address> addreses = employees.stream()
.map(Employee::address)
.collect(Collectors.toList());
}
Using flatMap
Scenario: Given a list of Employee
objects, each with a list of Address
objects within it,
return a single list of all addresses.
class Employee {
private List<Address> addresses;//Each employee has multiple addresses, including empty
}
public void testFlatMap(List<Employee> employees) {
List<Address> allAddresses = employees.stream()
.flatMap(employee -> employee.getAddresses().stream())//Flatmap needs a Stream that it can stitch together.
.collect(Collectors.toList());
}