Java tutorials > Modern Java Features > Java 8 and Later > What is the Stream API and how to use it?
What is the Stream API and how to use it?
Introduction to the Stream API
The Stream API, introduced in Java 8, provides a powerful and efficient way to process collections of data. It allows you to perform complex operations on data in a declarative style, making your code more readable and maintainable. Streams are not data structures; they are pipelines that operate on data sources like collections, arrays, or I/O channels. This tutorial will guide you through the basics of the Stream API, demonstrating its key features and providing practical examples.
Core Concepts of the Stream API
The Stream API revolves around several core concepts: Streams do not modify the original data source; instead, they create new streams or produce a result based on the original data.Key Concepts
filter
, map
, sorted
). These operations are lazy, meaning they are not executed until a terminal operation is invoked.forEach
, collect
, count
, reduce
).
Creating a Stream
Streams can be created from various sources: The code snippet demonstrates different ways to create streams from common data sources.Creating Streams
stream()
method of the List
interface.Arrays.stream()
method.Stream.of()
: Create a stream from individual elements.Stream.builder()
: Create a stream using builder pattern which allows more flexible element adding.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamCreationExample {
public static void main(String[] args) {
// From a List
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> nameStream = names.stream();
// From an Array
String[] colors = {"Red", "Green", "Blue"};
Stream<String> colorStream = Arrays.stream(colors);
// Using Stream.of()
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);
//Using Stream.builder()
Stream.Builder<String> builder = Stream.builder();
builder.add("Dog");
builder.add("Cat");
builder.add("Bird");
Stream<String> animalStream = builder.build();
}
}
Intermediate Operations: Filtering
The Filtering with Streams
filter()
operation allows you to select elements from a stream that match a given predicate (a boolean-valued function). In this example, we filter names that start with the letter 'A'..filter(name -> name.startsWith("A"))
keeps only the names starting with 'A'. The collect(Collectors.toList())
gathers the resulting elements into a new list.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamFilterExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Anna");
// Filter names that start with 'A'
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(filteredNames); // Output: [Alice, Anna]
}
}
Intermediate Operations: Mapping
The Mapping with Streams
map()
operation transforms each element of a stream into another element. It applies a function to each element and produces a new stream with the transformed elements. In this example, we convert names to uppercase..map(String::toUpperCase)
applies the toUpperCase()
method to each name. Again, collect(Collectors.toList())
collects the results into a list.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamMapExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Convert names to uppercase
List<String> uppercaseNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(uppercaseNames); // Output: [ALICE, BOB, CHARLIE]
}
}
Intermediate Operations: Sorting
The Sorting with Streams
sorted()
operation sorts the elements of a stream. By default, it sorts elements in natural order. You can also provide a custom comparator for more complex sorting scenarios..sorted()
sorts the names alphabetically (natural order). The result is collected into a list.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamSortExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
// Sort names alphabetically
List<String> sortedNames = names.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedNames); // Output: [Alice, Bob, Charlie]
}
}
Terminal Operations: forEach
The Iterating with
forEach()
forEach()
operation performs an action for each element of the stream. It's a terminal operation that consumes the stream..forEach(System.out::println)
prints each name to the console.
import java.util.Arrays;
import java.util.List;
public class StreamForEachExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Print each name
names.stream()
.forEach(System.out::println);
// Output:
// Alice
// Bob
// Charlie
}
}
Terminal Operations: collect
The The example shows how to collect the stream elements into a Collecting Results with
collect()
collect()
operation accumulates the elements of a stream into a collection or other data structure. It uses a Collector
interface to perform the accumulation. Common collectors are provided by the Collectors
class.List
and a Set
.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamCollectExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Collect names into a new List
List<String> nameList = names.stream()
.collect(Collectors.toList());
// Collect names into a Set
java.util.Set<String> nameSet = names.stream()
.collect(Collectors.toSet());
System.out.println("List: " + nameList);
System.out.println("Set: " + nameSet);
}
}
Terminal Operations: reduce
The Reducing a Stream with
reduce()
reduce()
operation combines the elements of a stream into a single value. It takes an identity value (initial value) and an accumulator function that combines two elements into one..reduce(0, Integer::sum)
starts with an initial value of 0 and adds each number in the stream to the running sum.
import java.util.Arrays;
import java.util.List;
public class StreamReduceExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Calculate the sum of the numbers
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("Sum: " + sum); // Output: Sum: 15
}
}
Real-Life Use Case: Processing Orders
Consider a scenario where you need to process a list of orders. You can use streams to filter orders based on customer, calculate total amounts, and perform other complex operations. The example demonstrates how to filter orders for a specific customer, map them to their amounts, and then sum the amounts to get the total spent by that customer. Also, it gets the distinct list of customers whose order is greater than 100. Processing Orders with Streams
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
class Order {
String customer;
double amount;
public Order(String customer, double amount) {
this.customer = customer;
this.amount = amount;
}
public String getCustomer() {
return customer;
}
public double getAmount() {
return amount;
}
@Override
public String toString() {
return "Order{customer='" + customer + "', amount=" + amount + "}";
}
}
public class StreamOrderProcessing {
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("Alice", 120.0),
new Order("Bob", 80.0),
new Order("Alice", 50.0),
new Order("Charlie", 200.0)
);
// Calculate the total amount spent by Alice
double totalAlice = orders.stream()
.filter(order -> order.getCustomer().equals("Alice"))
.mapToDouble(Order::getAmount)
.sum();
System.out.println("Total spent by Alice: " + totalAlice); // Output: Total spent by Alice: 170.0
//Get customers whose orders are greater than 100
List<String> highValueCustomers = orders.stream()
.filter(order -> order.getAmount() > 100)
.map(Order::getCustomer)
.distinct()
.collect(Collectors.toList());
System.out.println("High value customers: " + highValueCustomers); // Output: [Alice, Charlie]
}
}
Best Practices
Best Practices for Using Streams
collect
for creating a new collection, forEach
for performing an action on each element, reduce
for combining elements).
When to Use Streams
The Stream API is most beneficial when: However, streams may not be the best choice for simple iteration or when performance is critical and every millisecond counts. In those cases, traditional loops may be more appropriate.When to Use the Stream API
Memory Footprint
Streams, being lazy, generally have a low memory footprint. They don't store the data itself but rather operate on the data source. Intermediate operations create new streams, but these are also lazy and don't materialize the data until a terminal operation is called. However, terminal operations like Memory Considerations
collect()
can have a significant memory footprint, especially when collecting large datasets into a new collection. Be mindful of the size of the data you are collecting and choose appropriate data structures to minimize memory usage.
Alternatives to Stream API
Before Java 8, traditional loops ( The Stream API provides a good balance of ease of use, performance, and functional programming capabilities for most data processing tasks in Java.Alternatives to Stream API
for
, while
) were the primary way to process collections of data. Other alternatives include:
Pros and Cons
Pros and Cons of Using Streams
Pros:
Cons:
Interview Tip
When discussing the Stream API in an interview, be prepared to explain the core concepts, such as streams, intermediate operations, and terminal operations. Be able to provide examples of common stream operations like Interview Tip
filter
, map
, sorted
, forEach
, collect
, and reduce
. Also, be ready to discuss the benefits and drawbacks of using streams compared to traditional loops. Understanding the concept of lazy evaluation and parallel processing with streams is also beneficial.
FAQ
-
What is the difference between a Stream and a Collection?
Streams are not data structures like Collections. A Stream is a sequence of elements that supports aggregate operations, while a Collection is a data structure that stores elements. Streams are processed in a lazy manner, while Collections hold data in memory. -
Can I reuse a Stream after a terminal operation?
No, a Stream can only be used once. After a terminal operation is performed, the Stream is considered consumed, and you cannot reuse it. You'll need to create a new Stream from the data source if you want to perform another operation. -
How do I handle exceptions in a Stream pipeline?
You can handle exceptions within a Stream pipeline using try-catch blocks within the lambda expressions used in intermediate or terminal operations. However, this can make the code less readable. An alternative is to wrap the potentially exception-throwing operation in a function that returns anOptional
and then useflatMap
to handle the presence or absence of a value.