Java Streams - Revisions
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.
Sure! Here’s a completed and organized explanation of Intermediate Operations in Java Streams, including the ones you listed:
Intermediate Operations
INTERMEDIATE OPERATIONS: These transform a stream into another stream. Streams use lazy evaluation, meaning intermediate operations are not executed until a terminal operation is invoked.
Common Intermediate Operations:
-
filter(Predicate<T> predicate)
Selects elements that match the given predicate.
✅ Example:stream.filter(x -> x > 10)
-
map(Function<T, R> mapper)
Transforms each element using the provided function.
✅ Example:stream.map(String::toUpperCase)
-
flatMap(Function<T, Stream<R>> mapper)
Flattens nested structures by mapping each element to a stream and then flattening the result.
✅ Example:stream.flatMap(list -> list.stream())
-
distinct()
Removes duplicate elements from the stream (based onequals()
method).
✅ Example:stream.distinct()
-
limit(long maxSize)
Truncates the stream to contain no more thanmaxSize
elements.
✅ Example:stream.limit(5)
-
peek(Consumer<T> action)
Performs the given action on each element as they are consumed from the stream. Useful for debugging.
✅ Example:stream.peek(System.out::println)
-
sorted()
/sorted(Comparator<T> comparator)
Returns a stream with elements sorted in natural order or using a custom comparator.
✅ Example:stream.sorted()
orstream.sorted(Comparator.reverseOrder())
Terminal Operations
These operations produce a result or a side-effect and consume the stream, making it unusable afterward. They trigger the processing of all intermediate operations.
Common Terminal Operations:
-
collect(Collector<T, A, R> collector)
Performs a mutable reduction operation on the elements of the stream and returns a collection or another result container.
✅ Example:stream.collect(Collectors.toList())
-
forEach(Consumer<T> action)
Performs the given action for each element of the stream.
✅ Example:stream.forEach(System.out::println)
-
reduce(BinaryOperator<T> accumulator)
Reduces the stream to a single value using an associative accumulation function.
✅ Example:stream.reduce((a, b) -> a + b)
-
count()
Returns the number of elements in the stream.
✅ Example:stream.count()
-
min(Comparator<T> comparator)
Returns the minimum element of the stream according to the provided comparator, wrapped in anOptional
.
✅ Example:stream.min(Comparator.naturalOrder())
-
max(Comparator<T> comparator)
Returns the maximum element of the stream according to the provided comparator, wrapped in anOptional
.
✅ Example:stream.max(Comparator.naturalOrder())
-
toArray()
Returns an array containing the elements of the stream.
✅ Example:stream.toArray()
-
anyMatch(Predicate<T> predicate)
Returnstrue
if any elements of the stream match the provided predicate.
✅ Example:stream.anyMatch(x -> x > 10)
-
allMatch(Predicate<T> predicate)
Returnstrue
if all elements of the stream match the provided predicate.
✅ Example:stream.allMatch(x -> x > 0)
-
noneMatch(Predicate<T> predicate)
Returnstrue
if no elements of the stream match the provided predicate.
✅ Example:stream.noneMatch(x -> x < 0)
-
findFirst()
Returns the first element of the stream, if present, wrapped in anOptional
.
✅ Example:stream.findFirst()
-
findAny()
Returns any element of the stream, useful in parallel streams, wrapped in anOptional
.
✅ Example:stream.findAny()
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
merge multiple streams into one
Stream<String> stream1 = Stream.of("a", "b");
Stream<String> stream2 = Stream.of("c", "d");
Stream<Stream<String>> streams = Stream.of(stream1, stream2);
List<String> combined = streams
.flatMap(s -> s)
.collect(Collectors.toList());
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());
}
Streams with Map
The entry set returns a collection (Set) which can then be used to create the stream
Map<String, Integer> filteredMap = map.entrySet()
.stream()
.filter(entry -> entry.getValue() > 1)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));