How it works
The explanation, with examples you can run.
The operators
data class Address(val city: String?)data class User(val name: String, val address: Address?) fun cityOf(user: User?): String = user?.address?.city ?: "unknown" // safe calls, then a fallback fun requireCity(user: User): String { val city = user.address?.city ?: return "no city" // Elvis can return or throw return city.uppercase() // city is String here} fun main() { println(cityOf(User("Ada", Address("London")))) // London println(cityOf(User("Bob", null))) // unknown println(cityOf(null)) // unknown println(requireCity(User("Cy", Address(null)))) // no city}a?.bisnullifais null, otherwisea.b. A chain stops at the first null.a ?: bisaunless it is null, thenb. Becausereturnandthroware expressions, the right side can leave the function early, which keeps the rest of the code flat.a!!throws aNullPointerExceptionifais null. Use it only where a null really is a bug, and prefercheckNotNull(a) { "why" }orrequireNotNull, which explain the failure.
Smart casts
After a null check the compiler narrows the type for you:
fun describe(text: String?) { if (text == null) return println(text.length) // smart cast: text is String from here on}That only works on values that can't change between the check and the use: local vals, local vars not captured by a lambda that modifies them, and val properties that are not open, have no custom getter and are declared in the same module. For a mutable property, copy it into a local first, or write owner?.let { ... }.
Where null sneaks back in
- Platform types. A value from Java without nullability annotations has a type written
String!in the IDE: Kotlin lets you treat it as either. If you choose non-null and it is null, it fails at the point of use. Annotated Java (JSpecify, JetBrains, AndroidX annotations) gives you proper Kotlin types instead. lateinit var. A promise that a non-null property will be set before it is read; reading it early throwsUninitializedPropertyAccessException. Good for dependency injection and test setup, risky elsewhere.::prop.isInitializedchecks it.- Initialisation order. Calling an open method from a constructor can let a subclass see its own properties before they are set, so a non-null property can be null for a moment.
- Generics. A type parameter
Tcan itself be nullable (TisAny?by default). WriteT : Anyto forbid it.
Collections have their own pair of question marks: List<String?> is a list that may contain nulls, List<String>? is a list that may be missing. filterNotNull() and mapNotNull() turn the first into a List<String>.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.