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?
answer
- `ClosedRange.contains` default = `value >= start && value <= endInclusive` via compareTo
- IntRange/CharRange override with primitive first/last comparison (no boxing)
- any Comparable in a ClosedRange gets `in` free (LocalDate, String, BigDecimal)
- empty range (start > end) contains nothing
- ClosedFloatingPointRange overrides contains for NaN/IEEE-754
basics
~10 sx 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 linesimport 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
Knows in calls contains; may not know about the interface hierarchy.
Explains the default compareTo-based contains and that any Comparable range supports in.
Distinguishes the default vs. primitive overrides, the boxing/perf rationale, and empty-range/NaN edge cases.
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`.