Streams in Java

Core Concepts You Must Understand {#core-concepts}

Stream Pipeline Structure

Collection.stream()
    .intermediateOperation1()
    .intermediateOperation2()
    .terminalOperation();

Key Principles:

  1. Streams are lazy - Operations are only executed when a terminal operation is called

  2. Streams are consumed - Can only be used once

  3. Streams don't modify original data - They create new streams

  4. Operations can be chained - Fluent API design

Essential Operations {#essential-operations}

Intermediate Operations (Return Stream)

1. filter() - Keep Elements That Match Condition

// Keep even numbers
numbers.stream()
    .filter(n -> n % 2 == 0)

// Keep employees with high salary
employees.stream()
    .filter(emp -> emp.getSalary() > 75000)

2. map() - Transform Each Element

// Convert to uppercase
names.stream()
    .map(String::toUpperCase)

// Extract employee names
employees.stream()
    .map(Employee::getName)

// Square each number
numbers.stream()
    .map(n -> n * n)

3. flatMap() - Flatten Nested Structures

// Flatten list of lists
List<List<String>> listOfLists = Arrays.asList(
    Arrays.asList("a", "b"),
    Arrays.asList("c", "d")
);

List<String> flattened = listOfLists.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());
// Result: ["a", "b", "c", "d"]

// Get all skills from all employees
employees.stream()
    .flatMap(emp -> emp.getSkills().stream())
    .distinct()
    .collect(Collectors.toList());

4. distinct() - Remove Duplicates

numbers.stream()
    .distinct()
    .collect(Collectors.toList());

5. sorted() - Sort Elements

// Natural sorting
numbers.stream()
    .sorted()

// Custom sorting
employees.stream()
    .sorted(Comparator.comparing(Employee::getSalary))
    .sorted(Comparator.comparing(Employee::getName).reversed())

6. limit() and skip()

// Get first 5 elements
stream.limit(5)

// Skip first 10 elements
stream.skip(10)

// Pagination: Skip 20, take 10
stream.skip(20).limit(10)

Terminal Operations (Return Result)

1. collect() - Most Important Terminal Operation

import java.util.stream.Collectors;

// To List (Most Common)
.collect(Collectors.toList())

// To Set
.collect(Collectors.toSet())

// To Map
.collect(Collectors.toMap(Employee::getId, Employee::getName))

2. reduce() - Combine All Elements

// Sum all numbers
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);
// Or: .reduce(0, Integer::sum)

// Find product
int product = numbers.stream()
    .reduce(1, (a, b) -> a * b);

// Concatenate strings
String result = words.stream()
    .reduce("", (a, b) -> a + b);

3. forEach() - Perform Action on Each Element

employees.stream()
    .filter(emp -> emp.getSalary() > 50000)
    .forEach(System.out::println);

4. count() - Count Elements

long count = employees.stream()
    .filter(emp -> emp.getDepartment().equals("IT"))
    .count();

5. Finding Operations

// Find first element
Optional<Employee> first = employees.stream()
    .filter(emp -> emp.getSalary() > 100000)
    .findFirst();

// Find any element (useful in parallel streams)
Optional<Employee> any = employees.stream()
    .filter(emp -> emp.getDepartment().equals("HR"))
    .findAny();

6. Matching Operations

// Check if any employee earns > 100k
boolean anyHighEarner = employees.stream()
    .anyMatch(emp -> emp.getSalary() > 100000);

// Check if all employees are adults
boolean allAdults = employees.stream()
    .allMatch(emp -> emp.getAge() >= 18);

// Check if no employee is from "Finance"
boolean noFinance = employees.stream()
    .noneMatch(emp -> emp.getDepartment().equals("Finance"));

7. Min/Max Operations

// Find highest paid employee
Optional<Employee> maxSalary = employees.stream()
    .max(Comparator.comparing(Employee::getSalary));

// Find youngest employee
Optional<Employee> youngest = employees.stream()
    .min(Comparator.comparing(Employee::getAge));

Advanced Patterns and Collectors {#advanced-patterns}

1. Grouping Operations

Basic Grouping

// Group employees by department
Map<String, List<Employee>> byDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

Advanced Grouping

// Group by department and count employees in each
Map<String, Long> deptCount = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.counting()
    ));

// Group by department and find average salary
Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

// Group by department and get highest paid in each
Map<String, Optional<Employee>> maxByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.maxBy(Comparator.comparing(Employee::getSalary))
    ));

Multi-level Grouping

// Group by department, then by age range
Map<String, Map<String, List<Employee>>> multiLevel = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.groupingBy(emp -> emp.getAge() < 30 ? "Young" : "Senior")
    ));

2. Partitioning

// Partition employees by salary (>= 50k vs < 50k)
Map<Boolean, List<Employee>> partitioned = employees.stream()
    .collect(Collectors.partitioningBy(emp -> emp.getSalary() >= 50000));

List<Employee> highEarners = partitioned.get(true);
List<Employee> lowEarners = partitioned.get(false);

3. String Operations

// Join names with comma
String names = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.joining(", "));

// Join with custom delimiter and prefix/suffix
String formatted = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.joining(", ", "[", "]"));
// Result: "[John, Jane, Bob]"

4. Statistical Operations

// Get statistics for salaries
IntSummaryStatistics stats = employees.stream()
    .mapToInt(Employee::getSalary)
    .summaryStatistics();

System.out.println("Average: " + stats.getAverage());
System.out.println("Max: " + stats.getMax());
System.out.println("Min: " + stats.getMin());
System.out.println("Count: " + stats.getCount());
System.out.println("Sum: " + stats.getSum());

Common Interview Questions with Solutions {#interview-questions}

Question 1: Employee Filtering and Sorting

"Find all employees with salary > 50k, sort by name, return as list"

List<Employee> result = employees.stream()
    .filter(emp -> emp.getSalary() > 50000)
    .sorted(Comparator.comparing(Employee::getName))
    .collect(Collectors.toList());

Question 2: Department Analysis

"Group employees by department and count how many in each department"

Map<String, Long> deptCount = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.counting()
    ));

Question 3: Top N Elements

"Get the top 3 highest paid employees"

List<Employee> top3 = employees.stream()
    .sorted(Comparator.comparing(Employee::getSalary).reversed())
    .limit(3)
    .collect(Collectors.toList());

Question 4: Collection Transformation

"Convert list of employees to a map where key is employee ID and value is employee name"

Map<Integer, String> empMap = employees.stream()
    .collect(Collectors.toMap(
        Employee::getId,
        Employee::getName
    ));

Question 5: Complex Filtering

"Find employees from IT department with salary > 60k and age < 35"

List<Employee> result = employees.stream()
    .filter(emp -> emp.getDepartment().equals("IT"))
    .filter(emp -> emp.getSalary() > 60000)
    .filter(emp -> emp.getAge() < 35)
    .collect(Collectors.toList());

Question 6: String Manipulation

"Get all unique first names from employees, convert to uppercase, sort alphabetically"

List<String> uniqueFirstNames = employees.stream()
    .map(emp -> emp.getName().split(" ")[0])
    .map(String::toUpperCase)
    .distinct()
    .sorted()
    .collect(Collectors.toList());

Question 7: Nested Data Processing

"Get all unique skills from all employees"

List<String> allSkills = employees.stream()
    .flatMap(emp -> emp.getSkills().stream())
    .distinct()
    .sorted()
    .collect(Collectors.toList());

Question 8: Statistical Analysis

"Find the average salary of employees in each department"

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

Question 9: Conditional Operations

"Check if any employee in HR department earns more than 80k"

boolean exists = employees.stream()
    .filter(emp -> emp.getDepartment().equals("HR"))
    .anyMatch(emp -> emp.getSalary() > 80000);

Question 10: Complex Aggregation

"Get the name of the highest paid employee in each department"

Map<String, String> highestPaidByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.collectingAndThen(
            Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
            opt -> opt.map(Employee::getName).orElse("No employee")
        )
    ));

Working with Numbers Streams {#numbers-streams}

IntStream, LongStream, DoubleStream

// Create number streams
IntStream.range(1, 10)          // 1 to 9
IntStream.rangeClosed(1, 10)    // 1 to 10
IntStream.of(1, 2, 3, 4, 5)     // specific numbers

// Convert to primitive streams for performance
employees.stream()
    .mapToInt(Employee::getSalary)
    .sum();  // No boxing/unboxing

// Useful operations
int sum = numbers.stream().mapToInt(Integer::intValue).sum();
double avg = numbers.stream().mapToInt(Integer::intValue).average().orElse(0.0);
int max = numbers.stream().mapToInt(Integer::intValue).max().orElse(0);

Best Practices for Interviews {#best-practices}

1. Always Chain Operations Fluently

// Good - readable chain
List<String> result = employees.stream()
    .filter(emp -> emp.getSalary() > 50000)
    .map(Employee::getName)
    .sorted()
    .collect(Collectors.toList());

// Avoid - multiple intermediate variables
Stream<Employee> filtered = employees.stream().filter(emp -> emp.getSalary() > 50000);
Stream<String> names = filtered.map(Employee::getName);
// ... etc

2. Handle Optional Results Properly

// Good
Optional<Employee> emp = employees.stream()
    .max(Comparator.comparing(Employee::getSalary));

if (emp.isPresent()) {
    System.out.println("Highest paid: " + emp.get().getName());
} else {
    System.out.println("No employees found");
}

// Or using functional style
emp.ifPresent(e -> System.out.println("Highest paid: " + e.getName()));

// Or with default value
String name = emp.map(Employee::getName).orElse("No employee");

3. Use Method References When Possible

// Good - method references
employees.stream()
    .map(Employee::getName)
    .forEach(System.out::println);

// Acceptable but verbose - lambda expressions
employees.stream()
    .map(emp -> emp.getName())
    .forEach(name -> System.out.println(name));

4. Choose Right Collection Type

// Use Set for uniqueness
Set<String> uniqueDepts = employees.stream()
    .map(Employee::getDepartment)
    .collect(Collectors.toSet());

// Use List for ordered results
List<Employee> sortedEmps = employees.stream()
    .sorted(Comparator.comparing(Employee::getName))
    .collect(Collectors.toList());

5. Remember Proper Import Statements

import java.util.stream.Collectors;
import java.util.Comparator;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.IntStream;

Common Mistakes to Avoid {#mistakes-to-avoid}

1. Wrong Lambda Syntax in reduce()

// Wrong - reduce needs BinaryOperator (2 parameters)
numbers.stream().reduce((a) -> a * a);  // Compilation error

// Correct
numbers.stream().reduce(1, (a, b) -> a * b);  // Multiply all numbers

2. Forgetting Terminal Operation

// Wrong - this doesn't execute anything
employees.stream()
    .filter(emp -> emp.getSalary() > 50000)
    .map(Employee::getName);

// Correct - add terminal operation
List<String> names = employees.stream()
    .filter(emp -> emp.getSalary() > 50000)
    .map(Employee::getName)
    .collect(Collectors.toList());

3. Reusing Streams

// Wrong - stream can only be used once
Stream<Employee> empStream = employees.stream();
long count = empStream.count();
List<Employee> list = empStream.collect(Collectors.toList()); // IllegalStateException

// Correct - create new stream for each operation
long count = employees.stream().count();
List<Employee> list = employees.stream().collect(Collectors.toList());

4. Not Handling Empty Collections

// Good - handle empty results
Optional<Employee> maxSalary = employees.stream()
    .max(Comparator.comparing(Employee::getSalary));

if (maxSalary.isPresent()) {
    // Handle present value
} else {
    // Handle empty case
}

5. Confusing map() vs flatMap()

// Use map() for 1-to-1 transformation
List<String> names = employees.stream()
    .map(Employee::getName)  // Each employee -> one name
    .collect(Collectors.toList());

// Use flatMap() for 1-to-many transformation
List<String> skills = employees.stream()
    .flatMap(emp -> emp.getSkills().stream())  // Each employee -> multiple skills
    .collect(Collectors.toList());

Practice Problems {#practice-problems}

Beginner Level

Problem 1: Basic Filtering

// Given: List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
// Find: All even numbers

List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

Problem 2: String Transformation

// Given: List<String> words = Arrays.asList("apple", "banana", "cherry");
// Find: Uppercase words with length > 5

List<String> result = words.stream()
    .filter(word -> word.length() > 5)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

Intermediate Level

Problem 3: Grouping and Counting

// Given: List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
// Find: Group words by their length

Map<Integer, List<String>> wordsByLength = words.stream()
    .collect(Collectors.groupingBy(String::length));

Problem 4: Complex Filtering

// Given: List of Employee objects
// Find: Names of employees from "IT" department with salary > 70k, sorted alphabetically

List<String> result = employees.stream()
    .filter(emp -> emp.getDepartment().equals("IT"))
    .filter(emp -> emp.getSalary() > 70000)
    .map(Employee::getName)
    .sorted()
    .collect(Collectors.toList());

Advanced Level

Problem 5: Multi-level Grouping

// Group employees by department, then by salary range (High: >80k, Low: <=80k)
Map<String, Map<String, List<Employee>>> result = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.groupingBy(emp -> emp.getSalary() > 80000 ? "High" : "Low")
    ));

Problem 6: Custom Collector

// Find department with highest average salary
Optional<String> deptWithHighestAvgSalary = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ))
    .entrySet().stream()
    .max(Map.Entry.comparingByValue())
    .map(Map.Entry::getKey);

Parallel Streams (Bonus Topic)

When to Use Parallel Streams

// For large datasets and CPU-intensive operations
List<Integer> largeList = IntStream.range(1, 1000000)
    .boxed()
    .collect(Collectors.toList());

// Parallel processing
int sum = largeList.parallelStream()
    .filter(n -> n % 2 == 0)
    .mapToInt(Integer::intValue)
    .sum();

Cautions with Parallel Streams

  • Not always faster (overhead for small datasets)

  • Thread-safety concerns

  • Order may not be preserved

  • Avoid for I/O operations

Advanced Collectors (Expert Level)

Custom Collectors

// Collect to a specific collection type
Set<String> names = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.toCollection(TreeSet::new));

// Collect with downstream operations
Map<String, String> deptToHighestPaid = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.collectingAndThen(
            Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
            opt -> opt.map(Employee::getName).orElse("None")
        )
    ));

Performance Tips

1. Use Primitive Streams When Possible

// Avoid boxing/unboxing
int sum = employees.stream()
    .mapToInt(Employee::getSalary)  // IntStream
    .sum();

// Instead of
int sum = employees.stream()
    .map(Employee::getSalary)       // Stream<Integer>
    .reduce(0, Integer::sum);

2. Filter Early

// Good - filter first to reduce elements processed
employees.stream()
    .filter(emp -> emp.getSalary() > 50000)  // Reduce dataset first
    .map(Employee::getName)
    .collect(Collectors.toList());

3. Use Limit When Appropriate

// If you only need a few results
List<Employee> first5HighEarners = employees.stream()
    .filter(emp -> emp.getSalary() > 80000)
    .limit(5)  // Stop processing after finding 5
    .collect(Collectors.toList());

Final Checklist for Interviews {#final-checklist}

Must-Know Operations

  • [ ] filter() - Keep elements matching condition

  • [ ] map() - Transform each element

  • [ ] collect() - Convert to collection (with Collectors.toList(), toSet(), toMap())

  • [ ] reduce() - Combine elements (remember: needs 2 parameters)

  • [ ] sorted() - Sort elements (with Comparator.comparing())

  • [ ] groupingBy() - Group elements by key

  • [ ] flatMap() - Flatten nested structures

Must-Know Patterns

  • [ ] Filter → Map → Collect

  • [ ] GroupBy with counting/averaging

  • [ ] Finding max/min elements

  • [ ] Handling Optional results

  • [ ] Method references vs lambda expressions

Must-Avoid Mistakes

  • [ ] Wrong reduce() syntax

  • [ ] Forgetting terminal operations

  • [ ] Reusing streams

  • [ ] Not handling Optional results

  • [ ] Missing imports

Interview Confidence Boosters

  • [ ] Practice chaining operations fluently

  • [ ] Know when to use parallel streams

  • [ ] Understand lazy evaluation

  • [ ] Can explain benefits over traditional loops

  • [ ] Comfortable with complex grouping operations

Conclusion

Mastering Java Streams is crucial for modern Java interviews. The key is to:

  1. Understand the concepts - Know the difference between intermediate and terminal operations

  2. Practice the patterns - Filter-Map-Collect is the most common

  3. Handle edge cases - Always deal with Optional results properly

  4. Write clean code - Use method references and chain operations fluently

  5. Know the common mistakes - Avoid them during your interview

Last updated