What do the `is` and `!is` operators do in Kotlin, and what is a smart cast?
answer
- is = runtime check + smart cast
- !is is the negation
- smart cast narrows static type, no `as` needed
- needs a STABLE value (val / unmutated local var)
- works in if, &&/||, early return, when
basics
~20 sis checks at runtime whether a value is of a given type; !is is the negation. After a successful is check, Kotlin automatically treats the variable as that type, so you can use it without an extra cast. That automatic narrowing is a smart cast.
solid answer
~40 s`is` is Kotlin's runtime type-check operator: `x is String` returns `true` if `x` is a `String` (or subtype). `!is` is the inverse. Unlike Java's `instanceof`, a successful `is` triggers a smart cast: the compiler narrows the variable's static type within the scope where the check is guaranteed to hold, so you can call `String` members directly without writing `x as String`. Smart casts work after `is`, after `!= null` (narrowing `T?` to `T`), and combine with `&&`, `||`, early returns, and `when` branches. They require the value to be stable — typically a `val`, or a `var` local the compiler can prove isn't mutated between the check and use. They do not apply to mutable properties from other modules or `var`s captured by closures.
code
kotlin · 5 linesfun area(shape: Any): Double = when (shape) {
is Circle -> Math.PI * shape.radius * shape.radius // smart-cast to Circle
is Rect -> shape.w * shape.h // smart-cast to Rect
else -> 0.0
}go deeper
Knows is/!is check the type and that a smart cast lets you use the variable without an explicit cast.
Explains the stability requirement and that smart casts also apply to null checks and inside when.
Distinguishes stable vs unstable values, negative smart casts via early return, and erasure limits on generics.
Reasons about thread-safety/getter implications behind the stability rule and guides API/property design to keep values smart-castable.
## The `is` / `!is` operators `is` is Kotlin's **runtime type check**. `value is Type` evaluates to `true` when `value`'s actual runtime class is `Type` or a subtype of it. `!is` is the negation (`value !is Type` == `!(value is Type)`). It is the rough equivalent of Java's `instanceof`, but with a key extra power. ```kotlin fun describe(x: Any): String { if (x is String) { // inside this block, x is statically a String return "string of length ${x.length}" } return "not a string" } ``` ## Smart casts A **smart cast** is the compiler automatically narrowing a variable's *static* (compile-time) type after it has proven the runtime type. After `if (x is String)`, inside the `if` body `x` is typed as `String`, so `x.length` compiles with no explicit cast (no `as`). Smart casts trigger from several conditions: - `is` checks: `if (x is Foo) { x.fooMethod() }` - null checks: `if (x != null) { x.member }` narrows `Foo?` to `Foo` - combined boolean logic with `&&` / `||` - early `return`/`throw` that exclude a type (negative smart cast) ```kotlin fun len(x: Any?): Int { if (x !is String) return 0 // x is smart-cast to String here (after the negative check returns) return x.length } fun combo(x: Any) { if (x is String && x.length > 0) { /* x is String in the 2nd operand */ } if (x !is String || x.length == 0) return } ``` ## Stability requirement The compiler only smart-casts a **stable** value it can guarantee won't change between check and use: - local `val`s — always smart-castable - local `var`s — smart-castable if not modified between check and use, and not captured by a modifying closure - private/internal `val` properties with a backing field It **cannot** smart-cast: - `var` properties (could be changed by another thread or custom getter) - `open`/overridable properties or properties with custom getters (the getter could return different values each call) - properties from other modules For those cases, capture into a local `val` first, or use `?.let { }`. ## `is` inside `when` `when` branches support `is` directly and smart-cast inside each branch: ```kotlin when (x) { is Int -> x + 1 // x: Int is String -> x.length // x: String !is Boolean -> 0 else -> -1 } ``` With a `sealed` hierarchy, the compiler can verify exhaustiveness so `else` may be unnecessary. Note: generic type arguments are erased, so `x is List<String>` is not allowed (only `x is List<*>`).
- Why can't a public `var` property be smart-cast?Between the check and the use it could be reassigned (e.g. by another thread or via a custom getter), so the compiler can't guarantee the narrowed type still holds.
- How is `is` different from Java's `instanceof`?`instanceof` only returns a boolean; you still cast manually. Kotlin's `is` additionally smart-casts the variable, removing the explicit cast.
An is check is like checking a parcel's label; once verified, you can handle it as that kind of parcel without re-checking each time.
saying these in an interview costs you the question
- Saying `is` performs a cast like `as` (it only checks; smart cast is automatic narrowing)
- Claiming smart cast works on any `var` property
- Confusing `!is` with `!=`
- Thinking `is` can check generic type arguments like `List<String>`