Streams in Java
Core Concepts You Must Understand {#core-concepts}
Stream Pipeline Structure
Collection.stream()
.intermediateOperation1()
.intermediateOperation2()
.terminalOperation();
Key Principles:
Streams are lazy - Operations are only executed when a terminal operation is called
Streams are consumed - Can only be used once
Streams don't modify original data - They create new streams
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:
Understand the concepts - Know the difference between intermediate and terminal operations
Practice the patterns - Filter-Map-Collect is the most common
Handle edge cases - Always deal with Optional results properly
Write clean code - Use method references and chain operations fluently
Know the common mistakes - Avoid them during your interview
Last updated