The Stream API, introduced in Java 8, provides a functional approach to process collections of objects. Streams support sequential and parallel aggregate operations.
1. Stream Creation
Different Ways to Create Streams
import java.util.*; import java.util.stream.*; public class StreamCreation { public static void main(String[] args) { // From Collection List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream1 = list.stream(); // From Array String[] array = {"a", "b", "c"}; Stream<String> stream2 = Arrays.stream(array); // Using Stream.of() Stream<String> stream3 = Stream.of("a", "b", "c"); // Using Stream.builder() Stream<String> stream4 = Stream.<String>builder() .add("a") .add("b") .add("c") .build(); // Infinite streams Stream<Integer> infinite1 = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6... Stream<Double> infinite2 = Stream.generate(Math::random); // random numbers // Primitive streams IntStream intStream = IntStream.range(1, 5); // 1, 2, 3, 4 LongStream longStream = LongStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5 DoubleStream doubleStream = DoubleStream.of(1.1, 2.2, 3.3); // From String IntStream charStream = "hello".chars(); // ASCII values } } 2. Intermediate Operations
Filtering and Mapping
import java.util.*; import java.util.stream.*; public class IntermediateOperations { public static void main(String[] args) { List<String> names = Arrays.asList("John", "Alice", "Bob", "Anna", "David"); // filter() - filters elements based on predicate List<String> aNames = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList()); System.out.println("Names starting with A: " + aNames); // map() - transforms each element List<String> upperCaseNames = names.stream() .map(String::toUpperCase) .collect(Collectors.toList()); System.out.println("Uppercase names: " + upperCaseNames); // flatMap() - flattens nested structures List<List<String>> nestedList = Arrays.asList( Arrays.asList("a", "b"), Arrays.asList("c", "d") ); List<String> flatList = nestedList.stream() .flatMap(List::stream) .collect(Collectors.toList()); System.out.println("Flattened list: " + flatList); // distinct() - removes duplicates List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4); List<Integer> distinctNumbers = numbers.stream() .distinct() .collect(Collectors.toList()); System.out.println("Distinct numbers: " + distinctNumbers); // sorted() - sorts elements List<String> sortedNames = names.stream() .sorted() .collect(Collectors.toList()); System.out.println("Sorted names: " + sortedNames); // Custom sorting List<String> customSorted = names.stream() .sorted((s1, s2) -> s2.compareTo(s1)) // descending .collect(Collectors.toList()); System.out.println("Descending sorted: " + customSorted); // limit() and skip() List<Integer> limited = IntStream.range(1, 10) .boxed() .limit(5) .collect(Collectors.toList()); System.out.println("Limited to 5: " + limited); List<Integer> skipped = IntStream.range(1, 10) .boxed() .skip(5) .collect(Collectors.toList()); System.out.println("Skipped first 5: " + skipped); // peek() - for debugging List<String> peeked = names.stream() .peek(name -> System.out.println("Processing: " + name)) .map(String::toUpperCase) .peek(name -> System.out.println("After mapping: " + name)) .collect(Collectors.toList()); } } 3. Terminal Operations
Collection and Reduction
import java.util.*; import java.util.stream.*; public class TerminalOperations { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // collect() - to collection List<Integer> evenNumbers = numbers.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); System.out.println("Even numbers: " + evenNumbers); Set<Integer> numberSet = numbers.stream() .collect(Collectors.toSet()); Map<Integer, String> numberMap = numbers.stream() .collect(Collectors.toMap( n -> n, n -> "Number_" + n )); // forEach() - iterate over each element numbers.stream() .filter(n -> n > 5) .forEach(System.out::println); // reduce() - reduce to single value Optional<Integer> sum = numbers.stream() .reduce(Integer::sum); System.out.println("Sum: " + sum.orElse(0)); Integer product = numbers.stream() .reduce(1, (a, b) -> a * b); System.out.println("Product: " + product); // count() - count elements long count = numbers.stream() .filter(n -> n % 2 == 0) .count(); System.out.println("Even count: " + count); // min() and max() Optional<Integer> min = numbers.stream().min(Integer::compare); Optional<Integer> max = numbers.stream().max(Integer::compare); System.out.println("Min: " + min.orElse(0) + ", Max: " + max.orElse(0)); // anyMatch(), allMatch(), noneMatch() boolean anyEven = numbers.stream().anyMatch(n -> n % 2 == 0); boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0); boolean noneNegative = numbers.stream().noneMatch(n -> n < 0); System.out.println("Any even: " + anyEven); System.out.println("All even: " + allEven); System.out.println("None negative: " + noneNegative); // findFirst() and findAny() Optional<Integer> firstEven = numbers.stream() .filter(n -> n % 2 == 0) .findFirst(); Optional<Integer> anyEvenNumber = numbers.stream() .filter(n -> n % 2 == 0) .findAny(); System.out.println("First even: " + firstEven.orElse(0)); System.out.println("Any even: " + anyEvenNumber.orElse(0)); // toArray() Integer[] numberArray = numbers.stream() .filter(n -> n > 5) .toArray(Integer[]::new); } } 4. Collectors Utility Class
Advanced Collection Operations
import java.util.*; import java.util.stream.*; public class CollectorsExamples { public static void main(String[] args) { List<Employee> employees = Arrays.asList( new Employee("John", "IT", 5000), new Employee("Alice", "HR", 4000), new Employee("Bob", "IT", 6000), new Employee("Carol", "Finance", 4500), new Employee("David", "IT", 5500) ); // toList() List<String> names = employees.stream() .map(Employee::getName) .collect(Collectors.toList()); // toSet() Set<String> departments = employees.stream() .map(Employee::getDepartment) .collect(Collectors.toSet()); // toMap() Map<String, Double> nameToSalary = employees.stream() .collect(Collectors.toMap( Employee::getName, Employee::getSalary )); // groupingBy() - group by department Map<String, List<Employee>> byDepartment = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment)); // groupingBy with counting Map<String, Long> deptCount = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.counting() )); // groupingBy with averaging Map<String, Double> avgSalaryByDept = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.averagingDouble(Employee::getSalary) )); // partitioningBy - split by condition Map<Boolean, List<Employee>> highSalary = employees.stream() .collect(Collectors.partitioningBy( e -> e.getSalary() > 5000 )); // joining - concatenate strings String allNames = employees.stream() .map(Employee::getName) .collect(Collectors.joining(", ")); System.out.println("All names: " + allNames); // summarizing - get statistics DoubleSummaryStatistics stats = employees.stream() .collect(Collectors.summarizingDouble(Employee::getSalary)); System.out.println("Salary stats: " + stats); // reducing - custom reduction Optional<Employee> highestPaid = employees.stream() .collect(Collectors.reducing( (e1, e2) -> e1.getSalary() > e2.getSalary() ? e1 : e2 )); } } class Employee { private String name; private String department; private double salary; public Employee(String name, String department, double salary) { this.name = name; this.department = department; this.salary = salary; } // getters public String getName() { return name; } public String getDepartment() { return department; } public double getSalary() { return salary; } @Override public String toString() { return name + "(" + department + ": " + salary + ")"; } } 5. Parallel Streams
import java.util.*; import java.util.stream.*; public class ParallelStreams { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // Sequential stream long startTime = System.currentTimeMillis(); List<Integer> sequentialSquares = numbers.stream() .map(n -> { try { Thread.sleep(100); } catch (InterruptedException e) {} return n * n; }) .collect(Collectors.toList()); long sequentialTime = System.currentTimeMillis() - startTime; // Parallel stream startTime = System.currentTimeMillis(); List<Integer> parallelSquares = numbers.parallelStream() .map(n -> { try { Thread.sleep(100); } catch (InterruptedException e) {} return n * n; }) .collect(Collectors.toList()); long parallelTime = System.currentTimeMillis() - startTime; System.out.println("Sequential time: " + sequentialTime + "ms"); System.out.println("Parallel time: " + parallelTime + "ms"); // When to use parallel streams useParallelStreamsWisely(); } public static void useParallelStreamsWisely() { // Good for CPU-intensive operations long sum = LongStream.rangeClosed(1, 1_000_000) .parallel() .filter(n -> n % 2 == 0) .sum(); System.out.println("Sum: " + sum); // Be careful with stateful operations List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // This might give unexpected results List<Integer> badResult = new ArrayList<>(); numbers.parallelStream() .filter(n -> n % 2 == 0) .forEach(badResult::add); // NOT thread-safe! // Use collect instead List<Integer> goodResult = numbers.parallelStream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); } } 6. Practical Examples
Real-world Use Cases
import java.util.*; import java.util.stream.*; public class PracticalExamples { public static void main(String[] args) { // File processing processFileData(); // Data transformation dataTransformation(); // Statistical analysis statisticalAnalysis(); } public static void processFileData() { // Simulating file lines List<String> fileLines = Arrays.asList( "John,25,Engineer", "Alice,30,Manager", "Bob,22,Developer", "Carol,28,Designer" ); // Parse CSV data List<Person> people = fileLines.stream() .map(line -> line.split(",")) .map(parts -> new Person(parts[0], Integer.parseInt(parts[1]), parts[2])) .collect(Collectors.toList()); System.out.println("People: " + people); } public static void dataTransformation() { List<Transaction> transactions = Arrays.asList( new Transaction("USD", 1000.0), new Transaction("EUR", 800.0), new Transaction("USD", 1500.0), new Transaction("GBP", 1200.0), new Transaction("EUR", 900.0) ); // Group transactions by currency and sum amounts Map<String, Double> totalByCurrency = transactions.stream() .collect(Collectors.groupingBy( Transaction::getCurrency, Collectors.summingDouble(Transaction::getAmount) )); System.out.println("Total by currency: " + totalByCurrency); // Find top 3 transactions List<Transaction> topTransactions = transactions.stream() .sorted((t1, t2) -> Double.compare(t2.getAmount(), t1.getAmount())) .limit(3) .collect(Collectors.toList()); System.out.println("Top 3 transactions: " + topTransactions); } public static void statisticalAnalysis() { List<Double> data = Arrays.asList(10.5, 20.3, 15.7, 8.9, 25.1, 18.6); DoubleSummaryStatistics stats = data.stream() .mapToDouble(Double::doubleValue) .summaryStatistics(); System.out.println("Count: " + stats.getCount()); System.out.println("Min: " + stats.getMin()); System.out.println("Max: " + stats.getMax()); System.out.println("Average: " + stats.getAverage()); System.out.println("Sum: " + stats.getSum()); // Filter outliers (outside 2 standard deviations) double mean = stats.getAverage(); double stdDev = Math.sqrt( data.stream() .mapToDouble(d -> Math.pow(d - mean, 2)) .average() .orElse(0) ); List<Double> filtered = data.stream() .filter(d -> Math.abs(d - mean) <= 2 * stdDev) .collect(Collectors.toList()); System.out.println("Filtered data: " + filtered); } } class Person { private String name; private int age; private String job; public Person(String name, int age, String job) { this.name = name; this.age = age; this.job = job; } @Override public String toString() { return name + "(" + age + ", " + job + ")"; } } class Transaction { private String currency; private double amount; public Transaction(String currency, double amount) { this.currency = currency; this.amount = amount; } public String getCurrency() { return currency; } public double getAmount() { return amount; } @Override public String toString() { return currency + ": " + amount; } } 7. Best Practices and Performance
import java.util.*; import java.util.stream.*; public class BestPractices { public static void main(String[] args) { // 1. Avoid nested streams avoidNestedStreams(); // 2. Use primitive streams for better performance usePrimitiveStreams(); // 3. Prefer method references useMethodReferences(); // 4. Use lazy evaluation effectively useLazyEvaluation(); } public static void avoidNestedStreams() { List<List<String>> nestedData = Arrays.asList( Arrays.asList("a", "b", "c"), Arrays.asList("d", "e", "f") ); // BAD - nested streams List<String> badResult = nestedData.stream() .flatMap(list -> list.stream().map(String::toUpperCase)) .collect(Collectors.toList()); // GOOD - single flatMap List<String> goodResult = nestedData.stream() .flatMap(List::stream) .map(String::toUpperCase) .collect(Collectors.toList()); } public static void usePrimitiveStreams() { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // BAD - uses boxing/unboxing int badSum = numbers.stream() .mapToInt(Integer::intValue) .sum(); // GOOD - uses primitive stream directly int goodSum = numbers.stream() .mapToInt(Integer::intValue) .sum(); // Even better if you start with primitive array int[] primitiveNumbers = {1, 2, 3, 4, 5}; int bestSum = Arrays.stream(primitiveNumbers).sum(); } public static void useMethodReferences() { List<String> names = Arrays.asList("John", "Alice", "Bob"); // Lambda expression List<String> lambdaResult = names.stream() .map(name -> name.toUpperCase()) .collect(Collectors.toList()); // Method reference - cleaner List<String> methodRefResult = names.stream() .map(String::toUpperCase) .collect(Collectors.toList()); } public static void useLazyEvaluation() { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // Stream operations are lazy - no work happens until terminal operation Stream<Integer> stream = numbers.stream() .filter(n -> { System.out.println("Filtering: " + n); return n % 2 == 0; }) .map(n -> { System.out.println("Mapping: " + n); return n * n; }); // Nothing printed yet - stream not processed System.out.println("Stream created, but not processed yet"); // Now processing happens List<Integer> result = stream.collect(Collectors.toList()); System.out.println("Result: " + result); // Short-circuiting operations Optional<Integer> firstEvenSquare = numbers.stream() .filter(n -> { System.out.println("Filtering: " + n); return n % 2 == 0; }) .map(n -> { System.out.println("Mapping: " + n); return n * n; }) .findFirst(); // Stops after first match System.out.println("First even square: " + firstEvenSquare.orElse(0)); } } Key Points to Remember
- Streams don't store data - they just convey elements from a source
- Streams are functional - they don't modify the source
- Lazy evaluation - intermediate operations are lazy
- Consumable - streams can be traversed only once
- Parallel-friendly - easy to make operations parallel
- Use method references for better readability
- Prefer primitive streams for better performance
- Use Collectors for complex collection operations
The Stream API provides a declarative way to process data, making code more readable and maintainable while enabling easy parallelization.