The problem null safety solves
In many languages, any variable that holds an object can secretly hold "nothing", and the program finds out only when it tries to use that nothing and crashes. Kotlin moves that check to compile time. A plain type such as String can never hold null. When a value might be absent, you say so in the type by adding a question mark: String? is a nullable type. The compiler then refuses to let you use the value until you have dealt with the null case.
var name: String = "Kotlin"
name = null // rejected at compile time
var nickname: String? = null // fine: the ? allows null
The assignment to name fails to compile:
error: null cannot be a value of a non-null type 'String'.
The type of name says it always has a value, so assigning null breaks that promise. The ? is part of the type, so String and String? are two different types. A String can be used wherever a String? is expected, but not the other way round.
Why you can't just use a nullable value
With a nullable type, even a basic member access is rejected. For var nickname: String? = null, the expression nickname.length gives:
error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'.
That error is the point of the feature. The compiler knows the value could be absent, so it makes you choose what happens in that case.
Three ways to handle a nullable value
1. The safe call ?.
nickname?.length reads the length only when nickname holds a value. If it is null, the whole expression is null and nothing crashes. Its type is Int?, an Int that may be absent.
2. The Elvis operator ?:
nickname ?: "none" gives the left side when it has a value, and the right side when the left is null. Its type is String, never null, so you can use the result freely. It combines with the safe call: nickname?.length ?: 0 reads "the length if there is one, otherwise 0".
3. The non-null assertion !!
nickname!!.length tells the compiler "trust me, this is not null". If you are wrong, the program throws a NullPointerException at that point. It silences the compiler without making the value exist:
val nickname: String? = null
println(nickname!!.length)
This compiles with no complaint and then stops when it runs:
Exception in thread "main" java.lang.NullPointerException
That is exactly the crash the type system exists to prevent. Reach for ?. and ?: by default and use !! only when you can prove the value is present.
Checking for null yourself
A plain null check also works. After if (nickname != null), Kotlin treats a val or local variable as non-null inside that branch, so nickname.length compiles there. This is called a smart cast. The same applies to the right side of nickname != null && nickname.length > 3.
Common mistakes
- Adding
!!to make an error go away. The error disappears, but the risk moves to run time. - Expecting
?.to give a plain value.missing?.lengthhas the typeInt?. To use it as anInt, add a default with?:. - Thinking
valmeans non-null.valonly stops reassignment. Whether the value may be null depends on the?in the type. - Printing a null by surprise. In a string template, a null prints as the text
null. That is not an error, but it may not be what you want to show.
The lesson's exercise
The task is to try all three tools on a value that is present and one that is absent, and to produce exactly this output:
present length = 6
missing length = null
present or default = Kotlin
missing or default = none
forced length = 6
- Declare
presentwithval, give it the typeString?and the value"Kotlin". A nullable type can hold a normal value too. - Declare
missingas aString?set tonull. Without the?, the compiler rejects it. - Print the two lengths with a safe call inside a string template,
${present?.length}. Forpresentit gives 6. Formissingthe safe call returnsnulland the template prints that as the wordnull. - Print the two fallbacks with
${present ?: "none"}and${missing ?: "none"}. The first keepsKotlinand the second usesnone. - Print the forced length with
${present!!.length}. It is safe here becausepresentholds a value.
val present: String? = "Kotlin"
val missing: String? = null
println("present length = ${present?.length}")
println("missing length = ${missing?.length}")
println("present or default = ${present ?: "none"}")
println("missing or default = ${missing ?: "none"}")
println("forced length = ${present!!.length}")
The braces in ${...} let a string template hold a whole expression rather than just a variable name. Writing "$present?.length" would not work: the template takes only present, and ?.length stays as plain text.
Key takeaways
Stringnever holdsnull.String?may, and the compiler enforces the difference.?.returns the member ornull,?:supplies a default, and!!crashes onnull.- Prefer
?.and?:, and treat every!!as a risk you have to justify.