The isSorted Functions That Rewired Kotlin 2.4.0 stdlib Logic
Before Kotlin 2.4.0, verifying sort order meant either writing a manual loop, abusing zipWithNext(), or mapping to a boolean list — none of which the compiler could optimize, and all of which allocated objects you didnt ask for. The April 2026 stdlib update ships five dedicated functions that close this gap with zero intermediate allocations and a clean short-circuit contract.
TL;DR: Quick Takeaways
isSorted(),isSortedDescending(),isSortedBy(),isSortedByDescending(), andisSortedWith()are now part of the Kotlin 2.4.0 stdlib for bothIterableandSequence.- All five functions exit at the first out-of-order pair — O(1) best case, O(n) worst case, O(1) space throughout.
- The compiler transforms these into a
whileloop with an iterator — no lambda objects, no intermediate collections, zero boxing overhead on primitives. zipWithNext().all { ... }— the pre-2.4.0 idiom — creates a full intermediateList<Pair<T,T>>before the predicate runs;isSorted()never does.
Kotlin 2.4.0 Stdlib Changes: The Evolution of Collections
The stdlibs collection API has always leaned heavily on transformation — map, filter, flatMap — and the assumption that you compute a result, not inspect a property. Sort-order validation never fit that model cleanly, so it fell through the cracks. Developers either wrote boilerplate or reached for zipWithNext, which is clever but wrong for this job. The Kotlin 2.4.0 stdlib changes treat sort-order checking as a first-class operation, not a composable trick.
Full list of new Kotlin 2.4.0 features for Iterable and Sequence
Five extension functions ship on both Iterable<T> and Sequence<T>. They cover natural order ascending, natural order descending, key-selector ascending, key-selector descending, and fully custom comparator. Every one of them returns true for empty collections and single-element collections by definition — there are no pairs to violate, so the invariant holds vacuously. This is consistent with how sorted() itself behaves.
// Kotlin 2.4.0 — all five signatures
fun <T : Comparable<T>> Iterable<T>.isSorted(): Boolean
fun <T : Comparable<T>> Iterable<T>.isSortedDescending(): Boolean
fun <T, R : Comparable<R>> Iterable<T>.isSortedBy(selector: (T) -> R?): Boolean
fun <T, R : Comparable<R>> Iterable<T>.isSortedByDescending(selector: (T) -> R?): Boolean
fun <T> Iterable<T>.isSortedWith(comparator: Comparator<in T>): Boolean
The same five signatures exist on Sequence<T> — the difference is in evaluation strategy, covered in the final section. All functions are inline, so the lambda in isSortedBy and isSortedWith is inlined at the call site — no Function1 object is allocated. You can verify this by running javap -c on any class that calls these; the bytecode shows a plain while iterator loop, not an invokevirtual on a lambda instance.
Understanding the Kotlin isSorted Function Mechanics
The Kotlin isSorted function follows a two-pointer walk: grab the first element, advance the iterator, compare the pair, return false immediately if order is violated, otherwise shift the window and repeat. No accumulation. The moment a descending pair appears in an ascending check — done. This is what early exit means in practice: not a compile-time optimization, but a runtime contract baked into the iteration logic itself.
Proving order with isSortedDescending: Real-world examples
For primitives and strings, the functions rely on the Comparable interface, which on the JVM compiles down to compareTo() on the boxed type for generics. For Int and Long collections this means autoboxing at each comparison — something worth knowing if youre calling these in a hot path on a List<Int> rather than IntArray. For most application-layer validation this is irrelevant noise, but worth flagging.
// Before 2.4.0 — zipWithNext creates List<Boolean> fully before any check
val prices = listOf(100, 95, 87, 60, 42)
val isDesc = prices.zipWithNext { a, b -> a >= b }.all { it }
// After 2.4.0 — exits at first violation, no intermediate list
val isDesc = prices.isSortedDescending() // true
zipWithNext { a, b -> a >= b } materializes a full List<Boolean> of size n-1 before .all{} can even start. If the list has 50,000 elements and the first pair is already out of order, you still allocate 49,999 booleans before getting your answer. isSortedDescending() returns false after one comparison in that scenario. Empty list and single-element list always return true — no special-case handling needed on the caller side.
Kotlin Pitfalls: Beyond the Syntactic Sugar Moving to Kotlin isn't just about swapping semicolons for conciseness. While the marketing says "100% interoperable" and "null-safe," the reality in a Kotlin codebase complexity environment is different....
[read more →]How to Check if a List is Sorted in Kotlin Efficiently
The practical payoff of these functions shows up most in domain-model validation: you have a list of Event objects, a Product catalog ordered by price, or an Order history sorted by timestamp, and you need to assert the invariant before handing the collection to downstream code. The old approach required either a manual loop with an index or a map-then-zipWithNext chain — neither of which communicates intent clearly.
Utilizing Kotlin isSortedBy for complex object validation
The isSortedBy selector approach is the cleanest way to check if a list is sorted in Kotlin when the sort key is a property — it reads like a spec comment rather than an algorithm. The selector is inlined, so youre not paying for a closure allocation per call.
// Before 2.4.0 — manual loop, noisy intent
fun List<Event>.isChronological(): Boolean {
for (i in 1..lastIndex)
if (get(i).timestamp < get(i - 1).timestamp) return false
return true
}
// After 2.4.0 — one line, zero boilerplate
val ok = events.isSortedBy { it.timestamp }
val byPrice = products.isSortedBy { it.price }
val byDate = orders.isSortedByDescending { it.createdAt }
The selector variant passes null-safe keys — if the selector returns null, the function treats null as less than any non-null value, consistent with compareBy semantics. In production this matters when sorting by optional fields like Product.discountedPrice. The old manual-loop approach usually got this subtly wrong unless the developer explicitly handled nulls — and most didnt.
Kotlin 2.4.0 Performance Improvements: Under the Hood
The Kotlin 2.4.0 performance improvements in the sorting validation area arent magic — theyre the result of removing allocations that were never necessary. The zipWithNext idiom was always a wrong tool for the job: it was designed to transform pairs, not to validate order. Using it for validation forced the runtime to build a structure whose only purpose was to be consumed immediately by .all{}. The new functions eliminate the intermediate structure entirely.
Benchmark analysis: isSorted vs zipWithNext performance
The memory profile is the starkest difference. zipWithNext().all{} is O(n) space — it allocates a List<R> of n-1 elements before any predicate evaluation begins. isSorted() is O(1) space — two local variables, the iterator, and nothing else. Time complexity is O(n) worst case for both, but isSorted() is O(1) best case when the first pair already fails. zipWithNext is always O(n) time because it must build the full pair list regardless.
| Approach | Time complexity | Space complexity | Early exit | Allocations |
|---|---|---|---|---|
isSorted() |
O(n) worst / O(1) best | O(1) | Yes — first bad pair | Iterator only |
zipWithNext().all{} |
O(n) always | O(n) | No | List<Pair> + booleans |
| Manual index loop | O(n) worst / O(1) best | O(1) | Yes | None |
The manual loop actually matches isSorted() in asymptotic profile — but its 4–6 lines of boilerplate that every developer writes slightly differently, doesnt communicate intent, and invites off-by-one errors. The bytecode output of isSorted() after inlining is identical to a well-written manual loop. You get the performance of the loop with the readability of a stdlib call. The compiler also has the opportunity to apply iterator-specific optimizations for ArrayList that a generic index-loop wont get, though in practice this is a minor effect.
Kotlin Bytecode Bloat: What Aggressive Inlining Does to JVM Performance There's a particular kind of performance problem that doesn't show up in unit tests, doesn't trigger alerts, and looks perfectly reasonable in code review. You're...
[read more →]Advanced Validation with Custom Sorting Logic
Natural ascending and descending order covers most use cases, but production systems regularly need ordering contracts that Comparable alone cant express: case-insensitive string ordering, locale-aware sorting, multi-field compound keys, or domain-specific total orders. This is where isSortedWith earns its place — it takes a full Comparator<T>, which means any ordering you can build with compareBy, thenBy, or Comparator.comparing is checkable with the same early-exit guarantee.
Implementing isSortedWith for custom comparator scenarios
A isSortedWith custom comparator implementation is the right move when you need to validate an ordering contract that isnt derivable from a single key. The comparator is evaluated lazily — only as many pairs as needed to find a violation or exhaust the collection.
// Before 2.4.0 — inline comparator loop, verbose and fragile
val comp = compareBy<Order>({ it.status }, { it.createdAt })
var sorted = true
for (i in 1..orders.lastIndex)
if (comp.compare(orders[i - 1], orders[i]) > 0) { sorted = false; break }
// After 2.4.0 — isSortedWith, same comparator, clean contract
val isValid = orders.isSortedWith(compareBy({ it.status }, { it.createdAt }))
For case-insensitive string validation — a common API contract — pass String.CASE_INSENSITIVE_ORDER directly: tags.isSortedWith(String.CASE_INSENSITIVE_ORDER). The comparator interface means you can also plug in java.text.Collator instances for locale-aware ordering without any glue code. This is the integration point between Kotlins stdlib and the full JVM comparator ecosystem — nothing is cut off.
Optimizing Large Data Streams with Sequences
The Kotlin iterable extension functions version of isSorted works correctly on any finite collection, but it assumes the entire collection is already in memory. For large data streams — event logs, paginated API results, sensor feeds — Sequence is the right container because it doesnt force full materialization. The isSorted family on Sequence acts as a terminal operation with the same short-circuit contract, but the elements themselves are never all resident in RAM simultaneously.
Iterable isSorted vs Sequence isSorted in Kotlin 2.4.0
The functional contract is identical between Iterable and Sequence variants — same return type, same early-exit behavior, same null semantics. The difference is structural: Iterable.isSorted() walks a collection thats already allocated; Sequence.isSorted() pulls elements one at a time through the chain, so upstream Sequence vs Iterable transformations like map or filter are fused with the sort check rather than producing an intermediate list.
// Before 2.4.0 — forces full materialization before any check
val sorted = largeSource.asSequence()
.map { it.score }.toList() // allocates entire list
.zipWithNext { a, b -> a <= b }.all { it }
// After 2.4.0 — fused pipeline, no intermediate list
val sorted = largeSource.asSequence()
.map { it.score }
.isSorted() // terminal op, exits on first violation
On a stream of 1 million Event objects where the sort violation is at position 3, the Sequence version processes 4 elements total and allocates nothing beyond the iterator chain. The toList().zipWithNext()… version allocates 1 million score values, then 999,999 booleans, then evaluates the first three. This contrast highlights a core principle of Kotlin architecture: the intentional separation between eager collection processing and lazy functional pipelines. For Kotlin sequence performance in data-pipeline contexts, this difference is not theoretical — it is the difference between a 4ms call and a GC pause. The sequence variant also composes correctly with custom Kotlin iterable extension functions in the same pipeline without breaking the lazy evaluation chain. Maintaining this structural integrity ensures that business logic remains decoupled from the underlying data-processing strategy while maximizing resource efficiency. Integrating these efficient patterns into your kotlin architecture prevents the hidden overhead that typically degrades large-scale JVM applications over time.
Mastering Contextual Abstraction with Kotlin 2.4 Stable Parameters I've been waiting for the death of -Xcontext-parameters since the first previews. Not because the feature was bad — it was always promising — but because "experimental"...
[read more →]FAQ
What exactly changed in Kotlin 2.4.0 stdlib changes for collection validation?
Kotlin 2.4.0 adds five extension functions — isSorted(), isSortedDescending(), isSortedBy(), isSortedByDescending(), and isSortedWith() — to both Iterable<T> and Sequence<T>. These are declared in the standard library under kotlin.collections and are available without any additional imports. Prior to this release, no dedicated sort-validation API existed in the stdlib; developers relied on zipWithNext, manual loops, or custom utility extensions. The KEEP proposal for this addition is tracked in the Kotlin Evolution repository.
Does the Kotlin isSorted function work correctly on empty or single-element lists?
Yes — by convention, all five functions return true for empty collections and single-element collections. This is mathematically consistent: a vacuous truth holds when there are no pairs to compare. It also aligns with the behavior of sorted() itself, which returns the input unchanged for zero- or one-element collections. Callers dont need guard conditions for empty inputs — the contract is clean and unconditional.
How does isSorted vs manual loop performance compare in Kotlin?
Asymptotically they are identical: O(n) worst case, O(1) best case (early exit on first violation), O(1) space. The practical advantage of isSorted() is that the compiler can inline and specialize it per call site, whereas a generic manual loop written with index access bypasses the iterator entirely for ArrayList but is harder for the compiler to optimize for other Iterable types. In benchmarks against sorted 10,000-element List<Int>, isSorted() and a manual for-loop produce equivalent throughput; zipWithNext().all{} consistently runs 2–3× slower due to allocation pressure.
Can isSortedWith handle multi-field sorting and locale-aware comparators?
isSortedWith accepts any Comparator<in T>, which means compareBy { }.thenBy { } chains, java.text.Collator instances, and any Comparator built via Comparator.comparing(...).thenComparing(...) all work out of the box. The comparator is called once per adjacent pair until a violation is found or the collection is exhausted. Locale-aware sort validation — common when verifying alphabetically-sorted UI lists — is a one-liner: pass a Collator.getInstance(locale) wrapped in Comparator { a, b -> collator.compare(a, b) }.
Is there a meaningful difference between check if sequence is sorted Kotlin vs checking an Iterable?
The functional contract is the same, but the memory profile diverges sharply on large inputs. Iterable.isSorted() operates on a collection already in memory — no additional allocation, just iteration. Sequence.isSorted() is a terminal operation that pulls from a lazy chain: upstream map/filter steps are fused with the sort check, so no intermediate collections are produced at any stage. For datasets that dont fit comfortably in heap — or pipelines that read from a network stream or database cursor — the Sequence variant is the correct choice. Using Iterable.isSorted() on a sequence after calling toList() first negates the laziness entirely.
Do these functions work on arrays or only on collections?
The five functions are defined on Iterable<T> and Sequence<T>. Kotlin arrays (Array<T>, IntArray, etc.) are neither — they implement neither interface directly. To use isSorted() on an array, call .asIterable() or .toList() first. For primitive arrays like IntArray, asIterable() boxes each element, so a manual loop is more efficient if youre in a performance-critical path. For reference-type arrays or any scenario where boxing is already happening, array.asIterable().isSorted() is clean and correct.
Written by: