Sorting, distinct and limit
Sort with composed comparators, remove duplicates by equals, and take, skip or stop early with limit, skip, takeWhile and dropWhile.
stream.sorted(Comparator.comparing(X::key).reversed().thenComparing(X::id))
stream.distinct()
stream.skip(page * size).limit(size)These operations shape the stream rather than change its elements. They are what you reach for when an endpoint returns "the top five", "page two" or "every distinct tag".
sorted
sorted() uses natural order and needs elements that implement Comparable. sorted(comparator) takes any order, and comparators compose:
Comparator<Product> bestSellers = Comparator
.comparingInt(Product::sold).reversed()
.thenComparing(Product::name);Always add a tie-breaker such as a name or id. Without one, rows with equal keys come out in whatever order they went in, so the API returns them in a different order whenever the query changes. The sort is stable, but that only keeps the input order, which is rarely meaningful. Use Comparator.nullsLast when a sort key can be missing, or the sort throws NullPointerException. Note that sorted has to see every element before it emits the first one.
distinct
distinct() drops elements that are equals to one already seen and keeps the first occurrence, in encounter order. It relies on equals and hashCode, which records provide. It does not normalise: "[email protected]" and "[email protected]" are different strings, so map to a canonical form first. There is no built-in distinct-by-key; a gatherer or toMap with a merge function does that.
limit, skip, takeWhile and dropWhile
limit(n)keeps at most the firstnelements and stops pulling from the source after that.skip(n)throws away the firstn. Together,skip(page * size).limit(size)pages through an in-memory list.takeWhileanddropWhile(Java 9) stop or start at the first element that fails the predicate. On sorted data that is cheaper thanfilter; on unsorted data it gives a different answer.
Mistakes people make
- Putting
limitbeforesorted. You sort the firstnelements instead of taking the topn. - Paginating in Java. Loading a whole table to call
skipandlimitis slow and memory-hungry; useORDER BYwithLIMITandOFFSET, or keyset pagination, in SQL. Stream paging is for data that is small and already in memory.
Example
When to use it
- Return the five best-selling products for a home page widget, with name as a tie-breaker for a stable order.
- Remove repeated email addresses, after lowercasing, before sending a newsletter batch.
- Read log entries sorted by time and stop at the end of an incident window with takeWhile.
More examples
distinct compares with equals
Records get equals from their components, so the repeated Email is dropped. Strings that differ only in case are not equal, so normalise them with map before distinct.
import java.util.List;
import java.util.Locale;
record Email(String address) {}
void main() {
List<Email> emails = List.of(
new Email("[email protected]"),
new Email("[email protected]"),
new Email("[email protected]"));
IO.println(emails.stream().distinct().count());
List<String> raw = List.of("[email protected]", "[email protected]", "[email protected]");
IO.println(raw.stream().distinct().toList());
IO.println(raw.stream()
.map(s -> s.toLowerCase(Locale.ROOT))
.distinct()
.toList());
}
// Output:
// 2
// [[email protected], [email protected], [email protected]]
// [[email protected], [email protected]]Where limit goes changes the answer
The first pipeline takes the first three scores and sorts those, missing 88. The second sorts everything and then takes the top three, which is what a leaderboard wants.
import java.util.Comparator;
import java.util.List;
void main() {
List<Integer> scores = List.of(40, 95, 70, 88, 12);
IO.println(scores.stream()
.limit(3)
.sorted(Comparator.reverseOrder())
.toList());
IO.println(scores.stream()
.sorted(Comparator.reverseOrder())
.limit(3)
.toList());
}
// Output:
// [95, 70, 40]
// [95, 88, 70]takeWhile versus filter
takeWhile stops at the first element that fails the test, which is efficient on sorted data such as log entries by time. On unsorted data it stops too early, so 3 is lost where filter keeps it.
import java.util.List;
void main() {
List<Integer> sortedMinutes = List.of(1, 3, 5, 8, 13, 21);
IO.println(sortedMinutes.stream().takeWhile(m -> m < 10).toList());
IO.println(sortedMinutes.stream().dropWhile(m -> m < 10).toList());
List<Integer> unsorted = List.of(1, 12, 3);
IO.println(unsorted.stream().takeWhile(m -> m < 10).toList());
IO.println(unsorted.stream().filter(m -> m < 10).toList());
}
// Output:
// [1, 3, 5, 8]
// [13, 21]
// [1]
// [1, 3]
Discussion