The Stream API introduced in Java 8 revolutionized how developers handle collections. This functional approach enables writing cleaner, more expressive code when processing sequences of elements from various sources such as collections, arrays, or I/O resources. Unlike traditional loops, Stream operations do not modify the underlying data source but instead produce new streams for chained operations.
Key characteristics of Streams include:
- Lazy Execution: Intermediate operations are not executed until a terminal operation is invoked, allowing for optimized performance.
- Functional Nature: Operations can be composed using lambda expressions and method references.
- Single Use: A Stream can only be consumed once and will throw an exception if reused.
- Non-storage: Elements are computed on-demand rather than stored in memory.
Creating streams can be accomplished through several approaches:
import java.util.stream.Stream;
import java.util.Arrays;
import java.util.List;
// From a collection using stream() method
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> numberStream = numbers.stream();
// From an array using Arrays.stream()
Integer[] data = {10, 20, 30};
Stream<Integer> arrayStream = Arrays.stream(data);
// Using Stream.of() factory method
Stream<String> stringStream = Stream.of("x", "y", "z");
Stream operations are categorized into two types: intermediate operations that transform streams, and terminal operations that produce results or side effects.
Intermediate Operations include filtering, mapping, sorting, and limiting. These return new streams and support method chaining:
List<String> fruits = Arrays.asList("mango", "papaya", "kiwi", "grape");
List<String> processed = fruits.stream()
.filter(f -> f.length() > 4)
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
Terminal Operations trigger the actual computation and include methods like collect(), forEach(), reduce(), count(), and various matching operations:
List<Integer> values = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);
// Counting elements meeting a condition
long evenCount = values.stream()
.filter(v -> v % 2 == 0)
.count();
// Checking conditions
boolean hasNegative = values.stream().anyMatch(v -> v < 0);
// Reducing to single value
int sum = values.stream().reduce(0, Integer::sum);
When handling large datasets, parallel streams can leverage multi-core processors for improved performence. Converting a sequential stream to parallel is as simple as calling parallelStream() instead of stream():
List<Double> prices = Arrays.asList(29.99, 49.99, 19.99, 99.99, 149.99);
double average = prices.parallelStream()
.filter(p -> p > 30.0)
.mapToDouble(p -> p * 0.9) // Apply 10% discount
.average()
.orElse(0.0);
Consider this practical example demonstrating a stream pipeline for analyzing product inventory:
import java.util.*;
import java.util.stream.Collectors;
public class InventoryDemo {
public static void main(String[] args) {
List<Product> inventory = Arrays.asList(
new Product("Laptop", 1200, 5),
new Product("Mouse", 25, 50),
new Product("Keyboard", 80, 30),
new Product("Monitor", 300, 15)
);
Map<String, Integer> highValueProducts = inventory.stream()
.filter(p -> p.getPrice() > 100)
.sorted(Comparator.comparing(Product::getPrice).reversed())
.collect(Collectors.toMap(
Product::getName,
Product::getPrice,
(existing, replacement) -> replacement,
LinkedHashMap::new
));
highValueProducts.forEach((name, price) ->
System.out.println(name + ": $" + price));
}
}
class Product {
private String name;
private double price;
private int stock;
public Product(String name, double price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
public String getName() { return name; }
public double getPrice() { return price; }
public int getStock() { return stock; }
}
The Stream API fundamentally changes how Java developers approach data processing, enabling more expressive and maintainable code through functional programming principles and parallel execution capabilities.