Question
Java 8 Stream.collect Equivalents in Kotlin Standard Library
Question
In Java 8, Stream.collect(...) is commonly used to aggregate and transform data in many ways. In Kotlin, there is no single direct equivalent with the same API shape, and instead similar behavior is usually provided through standard library collection operations and extension functions.
When converting Java examples to Kotlin, especially examples from Collectors, it can be unclear which Kotlin functions match common collect use cases.
For example, what are the idiomatic Kotlin standard library equivalents for these common Java Stream.collect patterns?
- Accumulate names into a
List - Accumulate names into a
TreeSet - Convert elements to strings and concatenate them with commas
- Compute the sum of employee salaries
- Group employees by department
- Compute the sum of salaries by department
- Partition students into passing and failing
The goal is to understand how Kotlin expresses these collection operations using its standard library rather than Java 8 stream collectors.
Short Answer
By the end of this page, you will understand that Kotlin usually replaces Java's single Stream.collect(...) entry point with many focused collection functions such as toList(), toSet(), toCollection(), joinToString(), sumOf(), groupBy(), groupingBy(), fold(), and partition(). You will also see how to choose the right Kotlin function for common aggregation tasks like collecting, grouping, summing, and splitting data.
Concept
Java Streams centralize many aggregation operations through collect(...) and predefined Collectors. Kotlin takes a different approach.
Instead of one large collecting API, Kotlin provides many small, readable extension functions directly on collections and sequences. This often makes Kotlin code shorter and easier to read because the operation name describes the result directly.
For example:
-
Java:
stream.collect(toList()) -
Kotlin:
toList() -
Java:
stream.collect(joining(",")) -
Kotlin:
joinToString(",") -
Java:
stream.collect(groupingBy(...)) -
Kotlin:
groupBy { ... }
This matters because in real Kotlin code, developers usually do not look for one universal collector function. Instead, they choose the operation that matches the goal:
- Build a collection:
toList(),toSet(),toCollection(...) - Convert to text:
joinToString() - Compute totals:
sumOf() - Group items:
groupBy()orgroupingBy() - Split into two groups:
partition()
Kotlin also supports lazy processing with Sequence, which is closer to Java Stream behavior. But even with sequences, the terminal operations are still typically these Kotlin functions rather than a collect method with collectors.
A key idea is this:
- Java Streams: one terminal method, many collector objects
- Kotlin collections/sequences: many terminal operations, each purpose-built
That design is why there is no exact one-to-one collect replacement in Kotlin's standard library.
Mental Model
Think of Java collect(...) like going to a service desk and handing over a form that says what result you want.
Kotlin works more like using clearly labeled buttons:
- Want a list? Press
toList() - Want a set? Press
toSet()ortoCollection(...) - Want grouped data? Press
groupBy() - Want a total? Press
sumOf() - Want a comma-separated string? Press
joinToString()
Instead of one desk handling everything, Kotlin gives you a tool drawer where each tool has a clear purpose. That usually makes code easier to read because the operation is named directly in the call.
Syntax and Examples
Core Kotlin equivalents
Here are the most common Kotlin replacements for Java Stream.collect(...) use cases.
data class Employee(val name: String, val department: String, val salary: Int)
data class Student(val name: String, val passing: Boolean)
val employees = listOf(
Employee("Alice", "Engineering", 100_000),
Employee("Bob", "Engineering", 90_000),
Employee("Cara", "HR", 70_000)
)
val students = listOf(
Student("Mina", true),
Student("Leo", false),
Student("Nora", true)
)
Accumulate names into a List
val names = employees.map { it.name }.toList()
If employees.map { it.name } already returns a List, then toList() may be unnecessary:
val names = employees.map { it.name }
Accumulate names into a TreeSet
import java.util.TreeSet
val sortedNames = employees.map { it.name }.toCollection(TreeSet())
Use toCollection(...) when you want a specific destination collection type.
Join elements into a comma-separated string
val joined = employees.joinToString(",") { it.name }
Result:
Alice,Bob,Cara
Compute sum of salaries
val totalSalary = employees.sumOf { it.salary }
Group employees by department
val byDepartment = employees.groupBy { it.department }
This produces a Map<String, List<Employee>>.
Compute sum of salaries by department
val salaryByDepartment = employees
.groupBy { it.department }
.mapValues { (_, staff) -> staff.sumOf { it.salary } }
This produces a Map<String, Int>.
Another idiomatic option for grouped aggregation is groupingBy() with fold():
val salaryByDepartment = employees
.groupingBy { it.department }
.fold(0) { acc, employee -> acc + employee.salary }
Partition students into passing and failing
val (passing, failing) = students.partition { it.passing }
This returns a Pair<List<Student>, List<Student>>.
Why Kotlin examples often look shorter
Kotlin bakes common collector behavior into dedicated functions. That means many Java collector examples become simpler and more direct in Kotlin.
Step by Step Execution
Consider this Kotlin example:
data class Employee(val name: String, val department: String, val salary: Int)
val employees = listOf(
Employee("Alice", "Engineering", 100),
Employee("Bob", "Engineering", 80),
Employee("Cara", "HR", 70)
)
val result = employees
.groupBy { it.department }
.mapValues { (_, staff) -> staff.sumOf { it.salary } }
println(result)
Step-by-step
employeesis a list with threeEmployeeobjects.groupBy { it.department }reads each employee and groups them by department.- After
groupBy, the intermediate result is roughly:
mapOf(
"Engineering" to listOf(Employee("Alice", "Engineering", 100), Employee("Bob", "Engineering", 80)),
"HR" to listOf(Employee("Cara", "HR", 70))
)
mapValues { (_, staff) -> staff.sumOf { it.salary } }loops through each map entry.- For
Engineering, it sums100 + 80and returns180. - For
HR, it sums70and returns70. - Final result:
{Engineering=180, HR=70}
What to notice
groupBy()creates groups first.mapValues()transforms each group's list into a total.- This mirrors Java's grouping collector plus downstream summing collector, but Kotlin expresses it in two readable steps.
Real World Use Cases
Common real uses of these Kotlin operations
Building API response data
You may group database results before returning JSON:
val usersByRole = users.groupBy { it.role }
Generating CSV-like output
When exporting data or building logs:
val line = items.joinToString(",") { it.id.toString() }
Calculating totals
In shopping carts, invoices, payroll, or analytics:
val total = cartItems.sumOf { it.priceInCents * it.quantity }
Splitting valid and invalid records
Useful in import scripts and validation pipelines:
val (valid, invalid) = records.partition { it.isValid() }
Collecting into a specific collection type
If order or uniqueness matters:
val ids = rows.map { it.id }.toMutableSet()
val sortedTags = tags.toCollection(java.util.TreeSet())
Aggregating metrics by category
For reports and dashboards:
val revenueByRegion = sales.groupingBy { it.region }
.fold(0.0) { acc, sale -> acc + sale.amount }
Real Codebase Usage
In real Kotlin projects, developers usually combine small collection operations to express business logic clearly.
Common patterns
1. Mapping before collecting
val usernames = users.map { it.username }
This is often enough. If the source is already a collection, no extra terminal step is needed.
2. Collecting into a specific target
val uniqueNames = users.map { it.name }.toSet()
val sortedNames = users.map { it.name }.toCollection(java.util.TreeSet())
3. Group then transform
val ordersByCustomer = orders.groupBy { it.customerId }
val totalsByCustomer = ordersByCustomer.mapValues { (_, orders) ->
orders.sumOf { it.totalInCents }
}
4. One-pass grouped aggregation with groupingBy()
val countsByStatus = tasks.groupingBy { it.status }.eachCount()
Or:
val pointsByTeam = players.groupingBy { it.team }
.fold(0) { acc, player -> acc + player.points }
This is useful when you want aggregation behavior closer to Java collectors.
5. Guard clauses before aggregation
if (employees.isEmpty()) return emptyMap()
val totals = employees.groupBy { it.department }
.mapValues { (_, staff) -> staff.sumOf { it.salary } }
6. Validation and partitioning
val (ready, rejected) = requests.partition { it.isValid() }
This pattern is common in data import, form processing, and batch jobs.
Practical advice
- Use
groupBy()when you want actual lists in each group. - Use
groupingBy()when you want grouped aggregation like counts or sums. - Use
joinToString()for text output. - Use
toCollection(...)when the destination type matters. - Use
sumOf()for numeric totals.
Common Mistakes
1. Looking for one exact collect(...) replacement
Beginners often expect Kotlin to have a direct equivalent to Java's full collector API.
Mistake
// There is no single stdlib method exactly like Java's Stream.collect(Collectors...)
Better approach
Pick the function that matches the result:
toList()toSet()toCollection(...)joinToString()sumOf()groupBy()groupingBy()partition()
2. Using groupBy() when you only need counts or sums
Less efficient style
val countByDept = employees.groupBy { it.department }
.mapValues { (_, staff) -> staff.size }
Better style
val countByDept = employees.groupingBy { it.department }.eachCount()
groupBy() stores full lists. groupingBy() is often better for aggregate results.
3. Forgetting that map() already returns a list for collections
Unnecessary code
val names = employees.map { it.name }.toList()
Simpler
val names = employees.map { it.name }
If you start from a Sequence, then toList() is necessary at the end.
4. Confusing partition() with groupBy()
Wrong tool
val grouped = students.groupBy { it.passing }
This works, but if you specifically need two lists, partition() is clearer.
Better
val (passing, failing) = students.partition { it.passing }
5. Ignoring destination collection type
Problem
val names = employees.map { it.name }.toSet()
This gives a set, but not necessarily a TreeSet.
Correct if sorted set is required
val names = employees.map { it.name }.toCollection(java.util.TreeSet())
6. Using old Kotlin functions from outdated examples
Some older resources use APIs that have changed over time.
Prefer current stdlib functions such as
sumOf()instead of older sum patterns where appropriategroupingBy()for grouped aggregationjoinToString()for concatenation
Comparisons
Kotlin equivalents for common Java collector patterns
| Java Stream / Collector | Kotlin stdlib equivalent | Notes |
|---|---|---|
stream.collect(toList()) | toList() or just map(...) result | map() on a collection already returns a list |
stream.collect(toSet()) | toSet() | Creates a set of unique elements |
stream.collect(toCollection(TreeSet::new)) | toCollection(TreeSet()) | Use when destination type matters |
stream.collect(joining(",")) | joinToString(",") | Can transform each element with a lambda |
stream.collect(summingInt(...)) | sumOf { ... } | Works for common numeric totals |
stream.collect(groupingBy(...)) | groupBy { ... } | Produces Map<Key, List<Value>> |
stream.collect(groupingBy(..., summingInt(...))) | groupBy(...).mapValues(...) or groupingBy(...).fold(...) | groupingBy is often better for aggregation |
stream.collect(partitioningBy(...)) | partition { ... } | Returns two lists as a pair |
groupBy() vs groupingBy()
| Function | Best for | Result shape |
|---|---|---|
groupBy() | Keeping grouped items | Map<Key, List<Value>> |
groupingBy() | Aggregating by key | Grouping object used with eachCount(), fold(), reduce() |
Collections vs Sequences
| Type | Behavior | Use when |
|---|---|---|
List / Set | Eager operations | Data is already in memory and operations are simple |
Sequence | Lazy operations | You want stream-like chaining or to avoid intermediate collections |
Example with a sequence:
val names = employees.asSequence()
.map { it.name }
.filter { it.isNotBlank() }
.toList()
Cheat Sheet
Quick reference
Collect into common types
val list = items.toList()
val set = items.toSet()
val sortedSet = items.toCollection(java.util.TreeSet())
Transform then collect
val names = employees.map { it.name }
val ids = employees.map { it.id }.toSet()
Join into a string
val text = items.joinToString(",")
val names = employees.joinToString(", ") { it.name }
Sum values
val total = employees.sumOf { it.salary }
Group items
val byDept = employees.groupBy { it.department }
Group and count
val counts = employees.groupingBy { it.department }.eachCount()
Group and sum
val totals = employees.groupingBy { it.department }
.fold(0) { acc, e -> acc + e.salary }
Split into two groups
val (passing, failing) = students.partition { it.passing }
Rules of thumb
- Use
groupBy()when you need lists per key. - Use
groupingBy()for counts, sums, and reductions. - Use
joinToString()for concatenation. - Use
toCollection(...)for a specific collection type. map()on a collection already returns a list.- Add
toList()afterSequencepipelines when you need a materialized result.
FAQ
What is the Kotlin equivalent of Java Stream.collect(toList())?
Usually toList(), or no extra call at all if you already used map() on a collection, because map() returns a list.
Does Kotlin have a direct equivalent to Java Collectors?
Not as one single API. Kotlin standard library provides many separate functions like groupBy(), sumOf(), and joinToString() instead.
How do I group and sum in Kotlin?
Use either:
items.groupBy { it.key }.mapValues { (_, values) -> values.sumOf { it.amount } }
or:
items.groupingBy { it.key }.fold(0) { acc, item -> acc + item.amount }
When should I use groupBy() instead of groupingBy()?
Use groupBy() when you need the actual grouped lists. Use groupingBy() when you mainly want aggregated results like counts or sums.
How do I replace Java Collectors.joining() in Kotlin?
Use joinToString().
val result = names.joinToString(",")
How do I collect into a TreeSet in Kotlin?
Use toCollection(TreeSet()).
val sorted = items.toCollection(java.util.TreeSet())
Is Kotlin Sequence the same as Java Stream?
They are similar in spirit because both support lazy pipelines, but their APIs are different. In Kotlin, terminal operations are still usually functions like toList(), sumOf(), or groupBy().
What is the Kotlin equivalent of Java partitioningBy()?
Usually partition().
val (match, nonMatch) = items.partition { condition(it) }
Mini Project
Description
Create a small reporting utility for a company. You have a list of employees, and you want to generate several useful summaries from it using Kotlin standard library functions instead of Java stream collectors. This project demonstrates collecting into lists and sets, joining names, summing salaries, grouping by department, and partitioning employees by salary threshold.
Goal
Build a Kotlin program that produces multiple summary results from one employee list using idiomatic Kotlin collection operations.
Requirements
- Define an
Employeedata class withname,department, andsalary - Create a sample list of employees in at least two departments
- Print a comma-separated list of employee names
- Print total salary across all employees
- Print salary totals grouped by department
- Partition employees into high-paid and lower-paid groups based on a salary threshold
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.