Null Safety and Types
How does Kotlin null safety work?
Example
Types are non-null by default. String? may hold null, and the compiler forces you to handle it before use with ?. (safe call), ?: (Elvis default), !! (assert non-null) or a null check / let.
data class Address(val city: String?)
data class User(val name: String, val address: Address?)
fun main() {
val users = listOf(
User("Asha", Address("Pune")),
User("Ravi", Address(null)),
User("Meera", null),
)
for (u in users) {
val city = u.address?.city ?: "Unknown"
println("${u.name}: $city")
}
println("Meera city length = ${users[2].address?.city?.length}")
users[0].address?.city?.let { println("Ship to $it") }
}Output
Asha: Pune Ravi: Unknown Meera: Unknown Meera city length = null Ship to Pune
When can a NullPointerException still happen in Kotlin?
- Using
!!on a null value. - Calling Java code that returns null into a platform type (
String!) you treated as non-null. - Accessing a
lateinitproperty before it is set (throwsUninitializedPropertyAccessException). - Leaking
thisfrom a constructor so an open member reads a property before it is initialized. - Explicitly
throw NullPointerException().
What is the difference between lateinit and by lazy?
| lateinit var | val by lazy |
|---|---|
Mutable (var) | Read-only (val) |
Assigned later by you (DI, onCreate, test setup) | Computed on first access by the lambda, then cached |
| Non-null, non-primitive types only | Any type, including primitives and nullable |
Check with ::prop.isInitialized | Thread-safe by default (SYNCHRONIZED) |
Example: lateinit var adapter: UserAdapter | Example: val db by lazy { Room.databaseBuilder(...).build() } |
What is a platform type?
A type coming from Java code without nullability annotations, shown as String!. Kotlin does not know if it can be null, so it lets you use it as either String or String? and skips null checks. Best practice: assign it to an explicitly typed variable (val name: String? = javaUser.getName()) and annotate Java APIs with @Nullable/@NonNull.
What is a smart cast?
Example
After an is check or a null check, the compiler automatically treats the variable as the narrower type, so no explicit cast is needed. It works on vals and local variables the compiler can prove have not changed.
fun describe(x: Any?): String = when (x) {
null -> "null value"
is String -> "String of length ${x.length}"
is Int -> "Int doubled = ${x * 2}"
is List<*> -> "List with ${x.size} items"
else -> "Unknown: $x"
}
fun main() {
listOf("kotlin", 21, listOf(1, 2, 3), null, 3.5).forEach { println(describe(it)) }
}Output
String of length 6 Int doubled = 42 List with 3 items null value Unknown: 3.5
What is the difference between as and as?
Example
as is an unsafe cast and throws ClassCastException on failure. as? is a safe cast that returns null instead, usually combined with ?:.
fun main() {
val value: Any = "123"
val n: Int? = value as? Int
println("as? result: $n")
val s = value as String
println("as result: ${s.toInt() + 1}")
val safe = (value as? Int) ?: value.toString().toInt()
println("fallback: $safe")
}Output
as? result: null as result: 124 fallback: 123
How are primitive types handled in Kotlin?
Kotlin has no separate primitive types in the language: you write Int, Double, Boolean. The compiler uses JVM primitives (int) where possible and boxes them (Integer) when they are nullable (Int?) or used as generic type arguments (List<Int>). Use IntArray instead of Array<Int> to avoid boxing. There are no implicit widening conversions: val l: Long = intValue does not compile, use toLong().