skip to content

Explain `take`, `drop`, `takeLast`, `dropLast`, `takeWhile`, `dropWhile`, and `slice` in Kotlin. How do they handle out-of-range counts and what do they return?

level: seniorimportance: should knowfreq 60%

answer

  1. take/drop = count from front; takeLast/dropLast from back
  2. counts clamp; negative count throws IllegalArgumentException
  3. takeWhile/dropWhile only inspect the prefix (≠ filter)
  4. slice picks by index range or list — no clamping, throws OOB
  5. all return new lists; Sequence versions are lazy

basics

~20 s

take(n) keeps the first n elements; drop(n) skips them. takeLast/dropLast work from the end. The While versions cut at the first element that fails a condition. slice picks elements by index range or list. They return new collections and don't mutate.

solid answer

~40 s

`take(n)` returns a new `List` of the first `n` elements; `drop(n)` returns everything after the first `n`. Counts are **clamped**: `take(0)` → empty, `take(huge)` → a copy of the whole list, and a negative count throws `IllegalArgumentException` ("Requested element count is less than zero."). `takeLast(n)`/`dropLast(n)` mirror from the end (only on indexable `List`/`CharSequence`, not arbitrary `Iterable`). `takeWhile { p }` returns the leading run while `p` holds, stopping at the first failure; `dropWhile { p }` discards that leading run and keeps the rest — note both look only at the prefix, unlike `filter`. `slice(indices: IntRange)` and `slice(indices: Iterable<Int>)` return elements at those positions (the iterable form can reorder/repeat and throws on out-of-bounds indices). All are non-mutating and return fresh lists; on a `Sequence`, `take`/`drop`/`takeWhile`/`dropWhile` are intermediate and lazy.

code

kotlin · 7 lines
kotlin
val xs = listOf(10, 20, 30, 40, 50)
xs.take(3)               // [10, 20, 30]
xs.dropLast(1)           // [10, 20, 30, 40]
xs.takeWhile { it < 40 } // [10, 20, 30]
xs.dropWhile { it < 40 } // [40, 50]
xs.slice(1..3)           // [20, 30, 40]
xs.slice(listOf(4, 0))   // [50, 10]

go deeper

for a junior

Uses take/drop and takeLast/dropLast for simple front/back slicing and knows results are new lists.

for a middle

Distinguishes takeWhile/dropWhile prefix semantics from filter and uses slice with ranges.

for a senior

Knows the clamp-vs-throw rules, negative-count exception, and Sequence laziness of take/drop.

for a principal

Weighs slice (copy) vs subList (view) for memory/aliasing and guides safe use on hot paths and large lazy pipelines.

## Count-based slicing - **`take(n)`** → new `List` with the first `n` elements (in order). `n <= 0` → empty list; `n >= size` → full copy. `n < 0` throws `IllegalArgumentException`. - **`drop(n)`** → new `List` skipping the first `n`. `n >= size` → empty list; `n <= 0` → full copy; `n < 0` throws. - **`takeLast(n)` / `dropLast(n)`** → mirror from the end. Defined for `List`/`CharSequence`/arrays (need indexable size), not for a generic `Iterable`. Same clamping/negative rules. ```kotlin val xs = listOf(1, 2, 3, 4, 5) xs.take(2) // [1, 2] xs.drop(2) // [3, 4, 5] xs.takeLast(2) // [4, 5] xs.dropLast(2) // [1, 2, 3] xs.take(99) // [1, 2, 3, 4, 5] (clamped) // xs.take(-1) // IllegalArgumentException ``` ## Predicate-based prefix slicing - **`takeWhile { p }`** → the longest **leading run** for which `p` is true; stops permanently at the first element where `p` is false. - **`dropWhile { p }`** → discards that leading run, then keeps **everything after**, even elements that would fail `p`. These only inspect the prefix — they differ from `filter`, which tests every element. ```kotlin val ys = listOf(1, 2, 3, 1, 0) ys.takeWhile { it < 3 } // [1, 2] (stops at 3) ys.dropWhile { it < 3 } // [3, 1, 0] (keeps tail incl. 1, 0) ys.filter { it < 3 } // [1, 2, 1, 0] (tests all — contrast!) ``` ## slice — pick by position - **`slice(indices: IntRange)`** → elements in that inclusive range: `xs.slice(1..3)`. - **`slice(indices: Iterable<Int>)`** → elements at exactly those indices, in the given order, with repeats allowed: `xs.slice(listOf(4, 0, 0))` → `[5, 1, 1]`. - Any index outside `0 until size` throws `IndexOutOfBoundsException`. Unlike `take`/`drop`, `slice` does **not** clamp. ## Mutation and laziness - All of these return **new** read-only lists; the source is never modified. - On a `Sequence`, `take`/`drop`/`takeWhile`/`dropWhile` are **intermediate, lazy** operations producing another `Sequence`; `slice` is not available on `Sequence` (use indexing or convert). - `subList(from, to)` on a `MutableList` differs: it returns a **view** backed by the original, so it can reflect/propagate changes — not a copy like `slice`.

  • How is dropWhile different from filter with the negated predicate?
    dropWhile only removes the leading run that matches; once the predicate first fails it keeps the entire remainder, including later elements that match. filter removes matching elements everywhere.
  • What happens with take(-1) or slice(0..9) on a 3-element list?
    take(-1) throws IllegalArgumentException (negative count); slice(0..9) throws IndexOutOfBoundsException because slice does not clamp indices.
  • How does slice differ from subList?
    slice returns a new independent list; subList returns a live view backed by the original MutableList that reflects and can propagate mutations.

take/drop are scissors cutting a fixed number off the ends; takeWhile/dropWhile cut at the first element that breaks a rule; slice is a cherry-picker grabbing exact positions.

saying these in an interview costs you the question

  • Claiming take(n) throws when n > size (it clamps to the whole list)
  • Saying takeWhile behaves like filter
  • Thinking dropWhile removes all matching elements, not just the prefix
  • Believing slice clamps out-of-range indices (it throws)
  • Asserting these mutate the source list

context