skip to content

Explain the relationship between the `in` operator and `ClosedRange.contains`. When does `in` dispatch to `ClosedRange.contains` versus a more specialized override, and why does it matter?

level: seniorimportance: should knowfreq 48%

answer

  1. `ClosedRange.contains` default = `value >= start && value <= endInclusive` via compareTo
  2. IntRange/CharRange override with primitive first/last comparison (no boxing)
  3. any Comparable in a ClosedRange gets `in` free (LocalDate, String, BigDecimal)
  4. empty range (start > end) contains nothing
  5. ClosedFloatingPointRange overrides contains for NaN/IEEE-754

basics

~10 s

x in range calls range.contains(x). ClosedRange provides a default contains using compareTo. Specialized ranges like IntRange override it with faster primitive comparisons.

solid answer

~40 s

`in` desugars to `contains`. `ClosedRange<T : Comparable<T>>` declares `operator fun contains(value: T): Boolean = value >= start && value <= endInclusive`, expressed via `compareTo`. This default works for any `Comparable`, e.g. a range of `String`, `BigDecimal`, or `LocalDate`. Concrete numeric ranges (`IntRange`, `LongRange`, `CharRange`) override `contains` with primitive comparisons against `first`/`last`, avoiding boxing and `compareTo` calls — they implement both `ClosedRange<Int>` and `IntProgression`. It matters because: (1) custom `Comparable` types automatically get `in` support if you build a `ClosedRange`; (2) overrides are a performance optimization (no autoboxing in hot loops); (3) the default treats an empty range (`start > endInclusive`) as containing nothing, which `ClosedRange.isEmpty()` reflects. Floating-point ranges (`ClosedFloatingPointRange`) add their own `contains` to honor IEEE-754 NaN semantics rather than naive `compareTo`.

code

kotlin · 12 lines
kotlin
import java.math.BigDecimal

// Custom Comparable gets `in` via ClosedRange default contains
val priceBand: ClosedRange<BigDecimal> = BigDecimal("9.99")..BigDecimal("99.99")
println(BigDecimal("49.50") in priceBand)   // true, default compareTo path

// Empty range contains nothing
println(5 in 10..1)                          // false
println((10..1).isEmpty())                   // true

// Floating point NaN edge case
println(Double.NaN in 1.0..10.0)             // false (IEEE-754 honored)

go deeper

for a junior

Knows in calls contains; may not know about the interface hierarchy.

for a middle

Explains the default compareTo-based contains and that any Comparable range supports in.

for a senior

Distinguishes the default vs. primitive overrides, the boxing/perf rationale, and empty-range/NaN edge cases.

for a principal

Reasons about API design of ClosedRange/ClosedFloatingPointRange, dispatch costs, and where to expose custom range membership in a library.

## The desugaring `value in range` always means `range.contains(value)`. The question is *which* `contains` runs — Kotlin uses ordinary virtual dispatch. ## The `ClosedRange` default `ClosedRange<T : Comparable<T>>` is the generic interface for inclusive ranges. Its key members are `start`, `endInclusive`, and a **default** `contains`: ```kotlin public interface ClosedRange<T : Comparable<T>> { public val start: T public val endInclusive: T public operator fun contains(value: T): Boolean = value >= start && value <= endInclusive public fun isEmpty(): Boolean = start > endInclusive } ``` The `>=`/`<=` are themselves sugar for `compareTo`. So **any** `Comparable<T>` gets `in` support for free once wrapped in a `ClosedRange`: ```kotlin val today = LocalDate.now() val q1 = LocalDate.of(2026, 1, 1)..LocalDate.of(2026, 3, 31) println(today in q1) // LocalDate is Comparable, uses default contains val s = "m" println(s in "a".."z") // String range, default compareTo ordering ``` ## Why specialized ranges override `contains` `IntRange`, `LongRange`, and `CharRange` are concrete classes that implement `ClosedRange<Int>` (etc.) **and** a progression interface (`IntProgression`). They override `contains` to use **primitive** comparisons against `first` and `last`: ```kotlin // conceptually: override operator fun contains(value: Int): Boolean = first <= value && value <= last ``` This avoids **autoboxing** the `Int` into `Integer` and skips generic `compareTo` dispatch — important inside tight loops or large data validation. ## Empty ranges contain nothing If `start > endInclusive`, the range is **empty**: `isEmpty()` is true and `contains` returns false for every value. `5 in 10..1` is `false` (and `(10..1).isEmpty()` is true). ## Floating point is special `1.0..10.0` is a `ClosedFloatingPointRange<Double>`. It overrides `contains` (and `lessThanOrEquals`) so that **NaN** behaves per IEEE-754 — `Double.NaN in 1.0..10.0` is `false`, and naive `compareTo` (which would order NaN as largest) is deliberately avoided. This is why floating ranges are a distinct type from a plain `ClosedRange<Double>`. ## Practical takeaways - Build a `ClosedRange` over your own `Comparable` type to get `in` for free. - Primitive range overrides are a performance feature; rely on them in hot paths. - Watch empty ranges and floating-point/NaN edge cases.

  • Why is `IntRange` allowed to also be an `IntProgression`?
    It implements both `ClosedRange<Int>` (for `contains`/membership) and `IntProgression` (for iteration with a step). The two interfaces serve membership and iteration respectively.
  • Does `5 in 10..1` throw or return false?
    Returns false. `10..1` is an empty range; `contains` returns false for everything and `isEmpty()` is true.
  • Why does `Double.NaN in 1.0..10.0` return false?
    `ClosedFloatingPointRange` overrides `contains` to follow IEEE-754, where NaN compares as neither <= nor >= any value, so it's never a member.

saying these in an interview costs you the question

  • Thinking every range uses the same `contains` implementation regardless of element type.
  • Not knowing the default `ClosedRange.contains` is built on `compareTo`.
  • Claiming `5 in 10..1` throws an exception.
  • Assuming `Double.NaN` falls inside a floating-point range.
  • Unaware that custom `Comparable` types can support `in` via `ClosedRange`.

context