Understanding Streams in Java
Introduction and Stream Utility
Consider a scenario where you need to filter and process a collection of strings. Given a list of names, the task are: extract names starting with a specific prefix, further filter those by length, and finally display the results.
Example using a traditional approach:
import java.util.ArrayList;
public class TraditionalFilter {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Zhang Wuji");
names.add("Zhou Zhiruo");
names.add("Zhao Min");
names.add("Zhang Qiang");
names.add("Zhang Sanfeng");
ArrayList<String> filteredByPrefix = new ArrayList<>();
for (String name : names) {
if (name.startsWith("Zhang")) {
filteredByPrefix.add(name);
}
}
ArrayList<String> finalFilteredList = new ArrayList<>();
for (String name : filteredByPrefix) {
if (name.length() == 3) { // Assuming length refers to character count
finalFilteredList.add(name);
}
}
System.out.println(finalFilteredList);
}
}
Equivalent implementation using Streams:
import java.util.ArrayList;
public class StreamFilterExample {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Zhang Wuji");
names.add("Zhou Zhiruo");
names.add("Zhao Min");
names.add("Zhang Qiang");
names.add("Zhang Sanfeng");
names.stream()
.filter(name -> name.startsWith("Zhang"))
.filter(name -> name.length() == 3)
.forEach(System.out::println);
}
}
Purpose of Streams
Streams, when used with Lambda expressions, provide a declarative and concise way to perform operations on collections and arrays.
Stream Creation Methods
The first step is to obtain a Stream pipeline and place data on it.
1. From a Single-Column Collection (e.g., List, Set):
import java.util.ArrayList;
import java.util.Collections;
public class ListStreamExample {
public static void main(String[] args) {
ArrayList<String> itemList = new ArrayList<>();
Collections.addAll(itemList, "alpha", "beta", "gamma", "delta");
itemList.stream().forEach(elem -> System.out.println(elem));
}
}
2. From a Double-Column Collection (e.g., Map):
import java.util.HashMap;
public class MapStreamExample {
public static void main(String[] args) {
HashMap<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("Alice", 95);
scoreMap.put("Bob", 87);
scoreMap.put("Charlie", 92);
// Process keys
scoreMap.keySet().stream().forEach(key -> System.out.println(key));
// Process entries
scoreMap.entrySet().stream().forEach(entry -> System.out.println(entry));
}
}
3. From an Array:
import java.util.Arrays;
public class ArrayStreamExample {
public static void main(String[] args) {
int[] numberArray = {10, 20, 30, 40, 50};
Arrays.stream(numberArray).forEach(num -> System.out.println(num));
}
}
4. From a Set of Discrete Values:
import java.util.stream.Stream;
public class ValueStreamExample {
public static void main(String[] args) {
Stream.of("first", "second", "third").forEach(val -> System.out.println(val));
Stream.of(100, 200, 300).forEach(val -> System.out.println(val));
// Note: Stream.of() accepts a variable number of arguments or a reference type array.
// Passing a primitive array will treat the entire array as a single element.
}
}
Intermediate and Terminal Operations
Once a Stream is obtained, operations are performed using its API.
Intermediate Operations: These are methods that return a new Stream, allowing further chaining.
- Filtering (
filter,limit,skip):
import java.util.ArrayList;
import java.util.Collections;
public class IntermediateOperationsDemo {
public static void main(String[] args) {
ArrayList<String> dataList = new ArrayList<>();
Collections.addAll(dataList, "Zhang", "Zhou", "Zhao", "Zhang San", "Li");
// Filter names starting with 'Zhang' and having length 3
dataList.stream()
.filter(str -> str.startsWith("Zhang"))
.filter(str -> str.length() == 3)
.forEach(str -> System.out.println(str));
// Get first 2 elements
dataList.stream().limit(2).forEach(str -> System.out.println(str));
// Skip the first element
dataList.stream().skip(1).forEach(str -> System.out.println(str));
}
}
- Removing Duplicates (
distinct):
import java.util.ArrayList;
import java.util.Collections;
public class DistinctExample {
public static void main(String[] args) {
ArrayList<String> repeatedList = new ArrayList<>();
Collections.addAll(repeatedList, "A", "A", "B", "C", "B");
repeatedList.stream().distinct().forEach(item -> System.out.println(item));
}
}
- Transformation (
map):
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Function;
public class MapExample {
public static void main(String[] args) {
ArrayList<String> infoList = new ArrayList<>();
Collections.addAll(infoList, "John-25", "Jane-30", "Jack-28");
// Extract and print ages using anonymous class
infoList.stream().map(new Function<String, Integer>() {
@Override
public Integer apply(String record) {
String[] parts = record.split("-");
return Integer.parseInt(parts[1]);
}
}).forEach(age -> System.out.println(age));
// Equivalent using lambda
infoList.stream()
.map(record -> Integer.parseInt(record.split("-")[1]))
.forEach(age -> System.out.println(age));
}
}
Important Notes on Intermediate Operations:
- Each intermediate method returns a new Stream. The original Stream object should not be reused.
- Operations on a Stream do not modify the source collection or array.
Terminal Operations: These are final operations that consume the Stream and produce a result or side-effect. No further operations can be chained.
- Iteration (
forEach), Counting (count), and Conversion to Array (toArray):
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.function.IntFunction;
public class TerminalOperationsDemo {
public static void main(String[] args) {
ArrayList<String> sampleList = new ArrayList<>();
Collections.addAll(sampleList, "Cat", "Dog", "Bird", "Fish");
// forEach with lambda
sampleList.stream().forEach(element -> System.out.println(element));
// Count elements
long total = sampleList.stream().count();
System.out.println("Count: " + total);
// Convert to an Object array
Object[] objArray = sampleList.stream().toArray();
System.out.println(Arrays.toString(objArray));
// Convert to a typed String array using IntFunction
String[] strArray = sampleList.stream().toArray(new IntFunction<String[]>() {
@Override
public String[] apply(int size) {
return new String[size];
}
});
// Equivalent lambda expression
String[] strArrayLambda = sampleList.stream().toArray(length -> new String[length]);
System.out.println(Arrays.toString(strArrayLambda));
}
}