Java Streams API: A Practical Tutorial
The Java Streams API, introduced in Java 8, is a powerful addition to the Java programming language. It provides a high - level and declarative way to process collections of data. Streams allow developers to perform complex operations on data sources such as lists, sets, and arrays in a concise and efficient manner. By using the Streams API, you can write more readable and maintainable code, especially when dealing with data filtering, mapping, and reduction operations. This tutorial will guide you through the fundamental concepts, usage methods, common practices, and best practices of the Java Streams API.
Table of Contents
- Fundamental Concepts
- What are Streams?
- Stream Sources
- Intermediate and Terminal Operations
- Usage Methods
- Creating Streams
- Intermediate Operations
- Terminal Operations
- Common Practices
- Filtering Data
- Mapping Data
- Reducing Data
- Sorting Data
- Best Practices
- Lazy Evaluation
- Parallel Streams
- Avoiding Side - Effects
- Conclusion
- References
Fundamental Concepts
What are Streams?
A stream in Java is a sequence of elements from a source that supports various aggregate operations. It is not a data structure itself but rather a way to process data from existing data structures like collections or arrays. Streams allow you to perform operations on the elements in a declarative way, similar to SQL queries on a database.
Stream Sources
Streams can be created from different sources, including:
- Collections: You can create a stream from any
Collectionimplementation such asList,Set, etc. - Arrays: Arrays can also be used as a source to create a stream.
- Files: You can read the lines of a file as a stream.
Intermediate and Terminal Operations
- Intermediate Operations: These operations are lazy and return a new stream. They are used to transform or filter the elements of the stream. Examples include
filter(),map(), andsorted(). - Terminal Operations: These operations are eager and produce a result or a side - effect. Once a terminal operation is called on a stream, the stream is consumed and cannot be used again. Examples include
forEach(),collect(), andreduce().
Usage Methods
Creating Streams
Here are some common ways to create streams:
From a Collection
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class StreamCreationFromCollection {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("cherry");
Stream<String> stream = list.stream();
}
}
From an Array
import java.util.stream.Stream;
public class StreamCreationFromArray {
public static void main(String[] args) {
String[] array = {"apple", "banana", "cherry"};
Stream<String> stream = Stream.of(array);
}
}
Intermediate Operations
Filtering
The filter() method is used to select elements that match a given predicate.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class FilterExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Stream<Integer> filteredStream = numbers.stream().filter(n -> n % 2 == 0);
filteredStream.forEach(System.out::println);
}
}
Mapping
The map() method is used to transform each element of the stream using a given function.
import java.util.Arrays;
import java.util.List;
public class MapExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.stream().map(n -> n * 2).forEach(System.out::println);
}
}
Terminal Operations
Collecting
The collect() method is used to accumulate the elements of the stream into a collection or other data structures.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class CollectExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers);
}
}
Reducing
The reduce() method is used to combine the elements of the stream into a single value.
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class ReduceExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
sum.ifPresent(System.out::println);
}
}
Common Practices
Filtering Data
Filtering is a common operation when working with streams. You can use the filter() method to select elements based on a certain condition. For example, to filter a list of employees based on their age:
import java.util.ArrayList;
import java.util.List;
class Employee {
private int age;
public Employee(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
public class EmployeeFiltering {
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>();
employees.add(new Employee(25));
employees.add(new Employee(30));
employees.add(new Employee(40));
List<Employee> seniorEmployees = employees.stream()
.filter(e -> e.getAge() > 30)
.collect(java.util.stream.Collectors.toList());
System.out.println(seniorEmployees.size());
}
}
Mapping Data
Mapping is useful when you need to transform the elements of a stream. For example, to convert a list of strings to a list of their lengths:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StringLengthMapping {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "cherry");
List<Integer> lengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(lengths);
}
}
Reducing Data
Reducing is used to combine the elements of a stream into a single value. For example, to find the sum of all elements in a list:
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class SumReduction {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
sum.ifPresent(System.out::println);
}
}
Sorting Data
The sorted() method is used to sort the elements of a stream.
import java.util.Arrays;
import java.util.List;
public class SortingExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(5, 3, 1, 4, 2);
numbers.stream().sorted().forEach(System.out::println);
}
}
Best Practices
Lazy Evaluation
Intermediate operations in streams are lazily evaluated. This means that they are not executed until a terminal operation is called. This can lead to significant performance improvements, especially when dealing with large data sets. For example, if you have a long stream and you apply multiple intermediate operations, the operations are combined and executed only when a terminal operation is invoked.
Parallel Streams
Java Streams API supports parallel processing through parallel streams. You can convert a sequential stream to a parallel stream using the parallel() method. Parallel streams can significantly speed up the processing of large data sets by distributing the work across multiple threads. However, you need to be careful when using parallel streams as they have some overhead and may not always result in better performance, especially for small data sets.
import java.util.Arrays;
import java.util.List;
public class ParallelStreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.parallelStream().mapToInt(Integer::intValue).sum();
System.out.println(sum);
}
}
Avoiding Side - Effects
It is recommended to avoid side - effects in stream operations. Side - effects can make the code harder to understand and debug. For example, modifying a shared variable inside a forEach() method is a side - effect and should be avoided. Instead, use pure functions that do not modify external state.
Conclusion
The Java Streams API is a powerful tool for processing collections of data in a declarative and efficient way. By understanding the fundamental concepts, usage methods, common practices, and best practices, you can write more concise, readable, and maintainable code. Whether you are working with small or large data sets, the Streams API provides a flexible and scalable solution for data processing.
References
- Oracle Java Documentation: https://docs.oracle.com/javase/8/docs/api/java/util/stream/package - summary.html
- “Effective Java” by Joshua Bloch
- Baeldung Java Streams Tutorial: https://www.baeldung.com/java - 8 - streams