Core Interfaces and Classes in Java Stream API
Stream-Related Interfaces and Classes
The Java Stream API introduces several key interfaces and classes for processing collections of data in a functional style. This section covers the most important ones:
- Stream - The main interface for sequential or parallel operations on collections
- Predicate - A functional interface that represents a boolean-valued function
- Consumer - A functional interface that represents an operation that accepts a single input argument
- Supplier - A functional interface that represents a supplier of results
- xxxOperator - Various operator interfaces like BinaryOperator, UnaryOperator
- xxxFunction - Various function interfaces like Function, BiFunction
- Collector - A mutable reduction operation that accumulates input elements
- Collectors - A utility class containing implementations of common collectors
Stream, Collector, and Collectors are located in the java.util.stream package, while the others are in java.util.function.
Stream Interface
The Stream interface extends BaseStream, which in turn implements AutoCloseable. The class hierarchy includes specialized streams like IntStream, LongStream, and DoubleStream for primitive types.
BaseStream defines behaviors related to iteration and concurrency, while Stream focuses on computational operations such as:
- Terminal operations: min(), max(), count(), distinct()
- Intermediate operations: various map() methods, filter(), etc.
Predicate Functional Interface
Predicate represents a function that takes an input and returns a boolean value. It's designed to determine whether an input meets certain criteria.
The interface defines several methods, with test() being the only abstract method that must be implemented:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
return (t) -> test(t) && other.test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
return (t) -> test(t) || other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
Consumer Functional Interface
Consumer represents an operation that accepts a single input argument and returns no result. It's designed for operations that perform side effects.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
Supplier Functional Interface
Supplier represents a supplier of results. Unlike Consumer, it doesn't take any input but provides results through its get() method.
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Operator Interfaces
Operator interfaces like UnaryOperator, BinaryOperator, etc., are specialized versions of Function interfaces that return a result of the same type as their input(s).
Bi-Paramter Interfaces
The "Bi" prefix indicates interfaces that take two parameters. For example:
@FunctionalInterface
public interface BiPredicate<T, U> {
boolean test(T t, U u);
}
@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);
default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
Objects.requireNonNull(after);
return (l, r) -> { accept(l, r); after.accept(l, r); };
}
}
BinaryOperator Interface
BinaryOperator extends BiFunction and requires all input and output types to be the same:
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
}
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}
}
Collector Interface
Collector is a complex interface that defines a mutable reduction operation:
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
}
The interface provides static factory methods for creating new collectors:
public static <T, A, R> Collector<T, A, R> of(
Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Function<A, R> finisher,
Set<Characteristics> characteristics) {
return new CollectorImpl<>(supplier, accumulator, combiner, finisher, characteristics);
}
Collectors Utility Class
Collectors provides implementations of common collectors. For example, toList() creates a collector that accumulates elements into a List:
public static <T> Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>(ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}
Collection Enhancements
Java 8 enhanced the Collection interface with stream-related methods:
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
Map Enhancements
While Map doesn't have stream methods, it was enhanced with functional programming methods:
default void forEach(BiConsumer<? super K, ? super V> action)
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
Implementing a Custom Collector
Let's implement a custom collector that filters and collects objects. In this example, we'll create a collector that processes Product objects:
public class ProductProcessor {
private static List<Product> PRODUCTS;
static {
PRODUCTS = new ArrayList<>();
PRODUCTS.add(new Product("Laptop", "Electronics", 1200.00));
PRODUCTS.add(new Product("Smartphone", "Electronics", 800.00));
PRODUCTS.add(new Product("Desk Chair", "Furniture", 300.00));
}
public static void main(String[] args) {
// Using built-in collector
List<Product> electronics = PRODUCTS.stream()
.filter(p -> "Electronics".equals(p.getCategory()))
.collect(Collectors.toList());
System.out.println("Electronics using built-in collector: " + electronics);
// Using custom collector
Collector<Product, List<Product>, List<Product>> electronicsCollector =
Collector.of(
ArrayList::new,
(list, product) -> {
if ("Electronics".equals(product.getCategory())) {
list.add(product);
}
},
(list1, list2) -> {
list1.addAll(list2);
return list1;
}
);
List<Product> electronicsCustom = PRODUCTS.stream().collect(electronicsCollector);
System.out.println("Electronics using custom collector: " + electronicsCustom);
}
}
class Product {
private String name;
private String category;
private double price;
public Product(String name, String category, double price) {
this.name = name;
this.category = category;
this.price = price;
}
public String getName() { return name; }
public String getCategory() { return category; }
public double getPrice() { return price; }
@Override
public String toString() {
return "Product{name='" + name + "', category='" + category + "', price=" + price + "}";
}
}
Key Considerations
- Mutability: Be aware of which parameters in stream operations are mutable and which are immutable
- Performance: For simple operations, traditional loops might be more efficient than streams
- Readability: Use streams when they make the code more readable and concise
- Appropriate Use Cases: Streams are ideal for data transformation, filtering, and aggregation operations