One of the most powerful features of Java Streams is the ability to chain multiple operations together, forming a clear and concise data processing pipeline. Each intermediate operation returns a new stream, enabling fluent composition of transformations without modifying the original data source.
Immutability: Streams are immutable; operations don’t change the source data but produce new streams with the applied transformations. This avoids side effects and makes code easier to reason about.
Laziness: Intermediate operations like filter()
, map()
, and sorted()
are lazy — they don’t execute immediately. Instead, they build up a pipeline of transformations that only run when a terminal operation (e.g., collect()
, forEach()
) is invoked. This allows optimization and efficient processing.
Here are two real-world examples illustrating chaining of multiple stream operations:
Example 1: Filter, Map, and Sort a List of Products
Imagine a product list where you want to find all available items priced above $20, convert their names to uppercase, and then sort alphabetically.
import java.util.*;
import java.util.stream.*;
public class ProductPipeline {
static class Product {
String name;
double price;
boolean available;
Product(String name, double price, boolean available) {
this.name = name; this.price = price; this.available = available;
}
@Override public String toString() {
return name + " ($" + price + ")";
}
}
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Laptop", 999.99, true),
new Product("Mouse", 19.99, true),
new Product("Keyboard", 29.99, false),
new Product("Monitor", 199.99, true),
new Product("USB Cable", 10.99, true)
);
List<String> result = products.stream()
.filter(p -> p.available) // Keep only available
.filter(p -> p.price > 20) // Price > 20
.map(p -> p.name.toUpperCase()) // Map to uppercase names
.sorted() // Sort alphabetically
.collect(Collectors.toList());
System.out.println(result);
}
}
Output:
[LAPTOP, MONITOR]
Example 2: FlatMapping Nested Lists and Sorting
Suppose you have a list of orders, each containing multiple items. You want to extract all unique item names, filter those starting with "B", and sort them.
import java.util.*;
import java.util.stream.*;
public class OrderPipeline {
static class Order {
List<String> items;
Order(List<String> items) {
this.items = items;
}
}
public static void main(String[] args) {
List<Order> orders = List.of(
new Order(List.of("Banana", "Apple", "Bread")),
new Order(List.of("Butter", "Orange", "Banana")),
new Order(List.of("Bread", "Blueberry", "Apple"))
);
List<String> filteredItems = orders.stream()
.flatMap(order -> order.items.stream()) // Flatten all items into one stream
.filter(item -> item.startsWith("B")) // Items starting with 'B'
.distinct() // Remove duplicates
.sorted() // Sort alphabetically
.collect(Collectors.toList());
System.out.println(filteredItems);
}
}
Output:
[Banana, Blueberry, Bread, Butter]
ClassName::method
syntax over lambdas when applicable.Chaining multiple stream operations lets you build clear, modular, and efficient data processing pipelines. Immutability and laziness ensure safe, optimized execution. By combining operations like filter()
, map()
, flatMap()
, and sorted()
, you can express complex logic in a fluent, readable style that’s easy to maintain and understand.
concat()
The Stream.concat()
method allows you to combine two separate streams into a single stream. This is useful when you have multiple data sources or intermediate streams you want to merge seamlessly and then continue processing as one.
Stream.concat()
Worksconcat()
can be null
. If there is a possibility of null
, you should handle it explicitly before concatenation.Stream.concat()
can only combine two streams at a time. To concatenate more streams, you need to chain multiple calls or use other approaches (e.g., Stream.of()
with flatMap()
).Example 1: Concatenating Numeric Streams
Combine two IntStream
s of numbers and sum all elements.
import java.util.stream.*;
public class NumericConcatExample {
public static void main(String[] args) {
IntStream first = IntStream.range(1, 4); // 1, 2, 3
IntStream second = IntStream.range(4, 7); // 4, 5, 6
IntStream combined = IntStream.concat(first, second);
int sum = combined.sum(); // 1+2+3+4+5+6 = 21
System.out.println("Sum of combined streams: " + sum);
}
}
Expected Output:
Sum of combined streams: 21
Example 2: Concatenating Streams from Lists
Merge two lists of strings into one stream, convert all to uppercase, and collect.
import java.util.*;
import java.util.stream.*;
public class ListConcatExample {
public static void main(String[] args) {
List<String> list1 = List.of("apple", "banana");
List<String> list2 = List.of("cherry", "date");
Stream<String> combined = Stream.concat(list1.stream(), list2.stream());
List<String> result = combined
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(result);
}
}
Expected Output:
[APPLE, BANANA, CHERRY, DATE]
Example 3: Concatenating Streams of Custom Objects
Suppose you have two employee streams and want to concatenate them, then filter by department.
import java.util.*;
import java.util.stream.*;
public class EmployeeConcatExample {
static class Employee {
String name;
String department;
Employee(String name, String department) {
this.name = name; this.department = department;
}
@Override
public String toString() {
return name + " (" + department + ")";
}
}
public static void main(String[] args) {
Stream<Employee> teamA = Stream.of(
new Employee("Alice", "Sales"),
new Employee("Bob", "HR")
);
Stream<Employee> teamB = Stream.of(
new Employee("Carol", "Sales"),
new Employee("Dave", "IT")
);
List<Employee> salesTeam = Stream.concat(teamA, teamB)
.filter(e -> "Sales".equals(e.department))
.collect(Collectors.toList());
System.out.println("Sales team: " + salesTeam);
}
}
Expected Output:
Sales team: [Alice (Sales), Carol (Sales)]
Stream.concat()
is a simple and effective way to merge two streams of the same type.concat()
calls or consider other composition methods.By mastering Stream.concat()
, you can flexibly merge data sources and build more expressive stream pipelines.
Java Streams offer flexible ways to create streams programmatically beyond collections and arrays. This includes Stream.builder()
, Stream.generate()
, Stream.iterate()
, and custom sources for dynamic or infinite data. Each method serves different needs, such as deferred computation, on-demand data, or constructing values programmatically.
Stream.builder()
Stream.builder()
allows you to build a stream manually by adding elements one at a time. It’s useful when the data isn’t already in a collection or needs to be constructed conditionally.
import java.util.stream.*;
public class BuilderExample {
public static void main(String[] args) {
Stream<String> names = Stream.<String>builder()
.add("Alice")
.add("Bob")
.add("Carol")
.build();
names.forEach(System.out::println);
}
}
Stream.generate()
Stream.generate()
creates an infinite stream where each element is supplied by a Supplier<T>
. It’s ideal for producing repeated or computed values, such as random numbers.
import java.util.stream.*;
import java.util.Random;
public class GenerateExample {
public static void main(String[] args) {
Random rand = new Random();
Stream.generate(() -> rand.nextInt(100)) // infinite stream of random numbers
.limit(5)
.forEach(System.out::println);
}
}
Use limit()
to safely cap infinite streams.
Stream.iterate()
Stream.iterate()
is used to produce elements by applying a function repeatedly, often for sequences.
import java.util.stream.*;
public class IterateExample {
public static void main(String[] args) {
Stream.iterate(1, n -> n + 2) // odd numbers
.limit(5)
.forEach(System.out::println);
}
}
From Java 9 onward, you can also pass a predicate to create bounded iterate()
streams.
Custom streams are useful when reading from non-standard sources like sensors, logs, or APIs. This often involves wrapping a custom Iterator
or using Spliterator
and StreamSupport
.
Example: Stream from a Custom Iterator (simulated sensor data)
import java.util.*;
import java.util.stream.*;
public class CustomIteratorExample {
public static void main(String[] args) {
Iterator<Double> sensorSimulator = new Iterator<>() {
private int count = 0;
@Override
public boolean hasNext() {
return count++ < 5; // Simulate 5 sensor readings
}
@Override
public Double next() {
return 20 + Math.random() * 10; // Random temperature between 20–30°C
}
};
Iterable<Double> iterable = () -> sensorSimulator;
Stream<Double> sensorStream = StreamSupport.stream(iterable.spliterator(), false);
sensorStream.forEach(temp -> System.out.println("Sensor: " + temp + "°C"));
}
}
Method | Best Use Case |
---|---|
Stream.builder() |
Dynamically building small or condition-based streams |
Stream.generate() |
Infinite or lazy values like UUIDs, timestamps, sensors |
Stream.iterate() |
Sequences with clear progression rules |
Custom Iterator | External/dynamic data like sensors, APIs, logs |
Java provides several flexible ways to build streams beyond collections:
Stream.builder()
for programmatic element addition,Stream.generate()
for infinite streams from suppliers,Stream.iterate()
for functional sequences,Iterator
or Spliterator
for dynamic or external input.These tools unlock the full power of streams for modeling complex and evolving data sources.