LINQ to Streams
LINQ to Streams
Introduction
In this lesson, you'll learn about linq to streams in Java. Coming from C#, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In C#, you're familiar with linq to streams.
Java has its own approach to linq to streams, which we'll explore step by step.
The Java Way
Let's see how Java handles this concept. Here's a typical example:
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.sorted()
.collect(Collectors.toList());Comparing to C#
Here's how you might have written similar code in C#:
var evens = numbers
.Where(n => n % 2 == 0)
.Select(n => n * 2)
.OrderBy(n => n)
.ToList();You may be used to different syntax or behavior.
LINQ Where/Select map to Stream filter/map
You may be used to different syntax or behavior.
C# ToList() becomes .collect(Collectors.toList()) — more verbose
You may be used to different syntax or behavior.
Lambda syntax: n => body (C#) vs n -> body (Java)
You may be used to different syntax or behavior.
Both are lazy — no work happens until a terminal operation
You may be used to different syntax or behavior.
Java streams are single-use; iterating twice throws IllegalStateException
Step-by-Step Breakdown
1. LINQ Operator Mapping
Every LINQ operator has a Stream equivalent. The names differ but the concepts are identical.
list.Where(x => x > 0)
list.Select(x => x * 2)
list.OrderBy(x => x)
list.First()
list.Count()
list.Any(x => x > 0)
list.Sum()list.stream().filter(x -> x > 0)
list.stream().map(x -> x * 2)
list.stream().sorted()
list.stream().findFirst()
list.stream().count()
list.stream().anyMatch(x -> x > 0)
list.stream().mapToInt(x -> x).sum()2. Collecting Results
C#'s ToList()/ToArray() are terminal operations. Java needs .collect() with a Collector.
var list = query.ToList();
var arr = query.ToArray();
var dict = query.ToDictionary(x => x.Id);List<T> list = stream.collect(Collectors.toList());
T[] arr = stream.toArray(T[]::new);
Map<K,V> dict = stream.collect(
Collectors.toMap(x -> x.getId(), x -> x));3. GroupBy and Grouping
LINQ GroupBy returns IGrouping elements. Java Collectors.groupingBy returns a Map.
var byDept = employees
.GroupBy(e => e.Department)
.ToDictionary(g => g.Key, g => g.ToList());Map<String, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(e -> e.getDepartment()));4. Single-Use Streams
Unlike LINQ (which re-queries on each iteration), a Java Stream can only be consumed once. Store results or call stream() again.
// C# — safe to iterate query multiple times
var query = list.Where(x => x > 0);
int count = query.Count();
var first = query.First(); // works fine// Java — stream is consumed after first terminal op
Stream<Integer> s = list.stream().filter(x -> x > 0);
long count = s.count();
// s.findFirst() — THROWS IllegalStateException!
// Solution: collect first, or create fresh streams
List<Integer> filtered = list.stream().filter(x -> x > 0).toList();
long count2 = filtered.size();
Optional<Integer> first = filtered.stream().findFirst();Common Mistakes
When coming from C#, developers often make these mistakes:
- LINQ Where/Select map to Stream filter/map
- C# ToList() becomes .collect(Collectors.toList()) — more verbose
- Lambda syntax: n => body (C#) vs n -> body (Java)
Key Takeaways
- Where/Select/OrderBy → filter/map/sorted
- Terminate with .collect(Collectors.toList()) or .toList() (Java 16+)
- Both are lazy pipelines; work executes at the terminal operation
- Java streams are single-use — collect results before reusing