limit() and skip()
Take the first n elements or drop them, page through in-memory results, and cut infinite streams short, with takeWhile and dropWhile for condition-based cuts.
Stream<T> limit(long maxSize)
Stream<T> skip(long n)- Added in Java Java 8
Parameters
| Parameter | Type | Description |
|---|---|---|
$maxSize | long | For limit: the most elements the result may contain. Zero gives an empty stream. |
$n | long | For skip: how many leading elements to discard. Zero returns the elements unchanged. |
Return value
Stream<T>limit: a stream of at most maxSize elements, taken from the start. skip: a stream without its first n elements, or an empty stream if it has fewer than n.
Errors
IllegalArgumentException if maxSize or n is negative.
limit(n) keeps at most the first n elements. skip(n) throws away the first n and passes on the rest. Both take a long and both throw IllegalArgumentException for a negative value. Asking for more than the stream has is not an error: limit(100) on five elements gives five, and skip(100) gives an empty stream.
Pagination in memory
skip((page - 1) * size).limit(size) is the classic page. Two details matter. Do the multiplication in long or validate the inputs, because large page values overflow an int and produce a negative number. And validate the request first: a page of 0 makes the skip negative. Return 400 for bad parameters rather than letting the stream throw.
This only makes sense for data already in memory, such as a cached list or a small configuration set. For database results, push the paging into the query with LIMIT/OFFSET or, better for large tables, keyset pagination (WHERE id > :lastId ORDER BY id LIMIT :size). Loading every row and calling skip moves the whole table into the heap on each request. Always sort before paging; without a fixed order, pages are not guaranteed to be consistent.
Short-circuiting
limit is short-circuiting: once it has passed on n elements, upstream steps stop running. That is what makes infinite sources such as Stream.iterate and Stream.generate usable. It also means a pipeline like filter(...).limit(2) on a sequential stream only evaluates elements until it has two matches.
The exception is a stateful step before it. sorted().limit(10) must still sort everything first, because the smallest element could be last.
takeWhile and dropWhile
When the cut point is a condition rather than a count, Java 9 added takeWhile(predicate) and dropWhile(predicate). takeWhile stops at the first element that fails; it does not skip it and continue like filter does. They are most useful on sorted data, for example taking log entries until a timestamp is reached.
Parallel streams
On ordered parallel streams, limit and skip must honour encounter order, which forces extra buffering and can be slower than sequential. If you only want any n elements, unordered() removes that constraint.
Example
When to use it
- Page through a cached list of countries or currencies for a dropdown endpoint.
- Take the first 20 IDs from an infinite sequence generator for a test fixture.
- Skip a CSV header line before mapping the remaining lines to import records.
Frequently asked questions
How do I paginate a list with Java streams?
Use list.stream().skip((long) (page - 1) * size).limit(size).toList() after sorting the list. Validate that page and size are positive first. For database data, page in the SQL query instead of loading everything.
What is the difference between limit and takeWhile?
limit(n) keeps a fixed number of elements. takeWhile(predicate), added in Java 9, keeps elements until the first one that fails the predicate and then stops, however many that is.
Does limit throw if the stream has fewer elements?
No. limit(n) returns at most n elements, so a shorter stream comes through unchanged. Only a negative argument throws IllegalArgumentException; the same rules apply to skip.
More examples
Validating paging parameters
Checking the parameters yourself gives a clear message the client can act on and caps the page size. The stream's own exception for a negative value is a last line of defence, not an API error message.
List<String> page(List<String> all, int page, int size) {
if (page < 1 || size < 1 || size > 100) {
throw new IllegalArgumentException("page must be >= 1 and size between 1 and 100");
}
return all.stream().skip((long) (page - 1) * size).limit(size).toList();
}
void main() {
List<String> all = List.of("a", "b", "c", "d", "e");
IO.println(page(all, 1, 2));
IO.println(page(all, 3, 2));
try {
page(all, 0, 2);
} catch (IllegalArgumentException e) {
IO.println("400: " + e.getMessage());
}
try {
Stream.of(1, 2).limit(-1).toList();
} catch (IllegalArgumentException e) {
IO.println("limit(-1) threw IllegalArgumentException");
}
}
// Output:
// [a, b]
// [e]
// 400: page must be >= 1 and size between 1 and 100
// limit(-1) threw IllegalArgumentExceptionlimit stops upstream work early
On a sequential stream, elements flow through one at a time, so once limit has two matches nothing after 4 is read. peek is used here only to show which elements were pulled.
void main() {
List<Integer> looked = new ArrayList<>();
List<Integer> firstTwoEven = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
.peek(looked::add)
.filter(n -> n % 2 == 0)
.limit(2)
.toList();
IO.println(firstTwoEven);
IO.println(looked);
}
// Output:
// [2, 4]
// [1, 2, 3, 4]takeWhile and dropWhile cut on a condition
takeWhile stops for good at the first failure, so 30 and 15 are not included even though they pass the test; filter checks every element. Use the while forms on data where the condition marks a boundary.
void main() {
List<Integer> latenciesMs = List.of(12, 18, 25, 240, 30, 15);
IO.println(latenciesMs.stream().takeWhile(ms -> ms < 100).toList());
IO.println(latenciesMs.stream().dropWhile(ms -> ms < 100).toList());
IO.println(latenciesMs.stream().filter(ms -> ms < 100).toList());
IO.println(latenciesMs.stream().skip(1).limit(2).toList());
}
// Output:
// [12, 18, 25]
// [240, 30, 15]
// [12, 18, 25, 30, 15]
// [18, 25]
Discussion