Java Stream Basics
Introduction to Java Streams
Java Streams provide a modern way to process collections of data in a functional style.
Streams enable developers to write concise and readable code for filtering, mapping, and reducing data.
Understanding streams is essential for writing efficient and clean Java applications.
Streams allow you to express complex data processing queries in a clear and concise way.
What is a Stream?
A Stream in Java is a sequence of elements supporting sequential and parallel aggregate operations.
Streams do not store data; instead, they convey elements from a data source such as collections, arrays, or I/O channels.
Streams allow functional-style operations on data, such as map, filter, and reduce.
- Streams are not data structures.
- They provide a pipeline to process data.
- Operations on streams are either intermediate or terminal.
Creating Streams
Streams can be created from various data sources including collections, arrays, or generated values.
Common ways to create streams include using the stream() method on collections or Stream.of() for explicit elements.
- From a Collection: collection.stream()
- From an Array: Arrays.stream(array)
- From explicit values: Stream.of(value1, value2, ...)
- From infinite generators: Stream.generate() or Stream.iterate()
Stream Operations
Stream operations are divided into intermediate and terminal operations.
Intermediate operations return a new stream and are lazy, meaning they are not executed until a terminal operation is invoked.
Terminal operations produce a result or side-effect and trigger the processing of the stream pipeline.
- Intermediate operations: filter, map, sorted, distinct, limit
- Terminal operations: forEach, collect, reduce, count, anyMatch
Intermediate Operations
These operations transform a stream into another stream.
They are lazy and do not process data until a terminal operation is called.
- filter(predicate): keeps elements matching the predicate
- map(function): transforms each element
- sorted(): sorts elements
- distinct(): removes duplicates
- limit(n): truncates the stream to n elements
Terminal Operations
Terminal operations produce a result or side-effect and close the stream.
Once a terminal operation is invoked, the stream pipeline is executed.
- forEach(consumer): performs an action for each element
Example: Using Streams to Process a List
Let's see a simple example that filters and transforms a list of strings using streams.
Examples
import java.util.*;
import java.util.stream.*;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
List<String> filteredNames = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(filteredNames);
}
}This example creates a stream from a list of names, filters names longer than 3 characters, converts them to uppercase, and collects the results into a new list.
Best Practices
- Prefer streams for readable and concise data processing.
- Avoid modifying data sources during stream operations.
- Use parallel streams only when beneficial and safe.
- Chain intermediate operations to build clear pipelines.
- Always close streams that wrap IO resources.
Common Mistakes
- Assuming streams store data; they only process data.
- Using streams multiple times after a terminal operation (streams cannot be reused).
- Neglecting to use terminal operations, resulting in no processing.
- Using parallel streams without considering thread safety.
- Modifying shared mutable state inside stream operations.
Hands-on Exercise
Filter and Collect Exercise
Create a stream from a list of integers, filter out even numbers, and collect the odd numbers into a new list.
Expected output: A list containing only odd numbers from the original list.
Hint: Use filter() with a predicate and collect() with Collectors.toList().
Interview Questions
What is the difference between intermediate and terminal operations in Java Streams?
InterviewIntermediate operations return a new stream and are lazy, meaning they do not process data until a terminal operation is invoked. Terminal operations produce a result or side-effect and trigger the processing of the stream pipeline.
Can you reuse a Java Stream after a terminal operation?
InterviewNo, once a terminal operation is performed, the stream is considered consumed and cannot be reused.
Summary
Java Streams provide a powerful and expressive way to process data collections.
Understanding how to create streams and use intermediate and terminal operations is key to leveraging the Stream API.
Streams promote functional programming concepts and can lead to more readable and maintainable code.
FAQ
Are streams in Java mutable?
No, streams themselves are not mutable data structures; they provide a pipeline to process data without modifying the source.
What happens if you don't call a terminal operation on a stream?
Without a terminal operation, the stream pipeline is not executed because intermediate operations are lazy.
