Bỏ qua để đến nội dung

🛡️ Null Safety

🎯 Mục tiêu: Hiểu Null Safety - tính năng quan trọng nhất của Kotlin giúp loại bỏ NullPointerException ngay từ lúc compile.


NullPointerException (NPE) là lỗi phổ biến nhất trong Java:

// Java - có thể crash
String name = null;
int length = name.length(); // ❌ NullPointerException!

Kotlin giải quyết vấn đề này ngay tại compile-time:

var name: String = null // ❌ Lỗi compile! String không thể null
var name: String? = null // ✅ OK! String? có thể null

Mọi biến trong Kotlin mặc định không thể null:

var name: String = "Kotlin"
// name = null // ❌ Lỗi compile: Null can not be a value of a non-null type
var age: Int = 25
// age = null // ❌ Lỗi compile

Thêm ? sau kiểu để cho phép null:

var name: String? = "Kotlin"
name = null // ✅ OK
var age: Int? = 25
age = null // ✅ OK

Thay vì kiểm tra null thủ công, dùng ?.:

val name: String? = "Kotlin"
// ❌ Cách cũ (verbose)
if (name != null) {
println(name.length)
}
// ✅ Safe call
println(name?.length) // 6
// Nếu null
val nullName: String? = null
println(nullName?.length) // null (không crash!)
data class Address(val city: String?)
data class User(val address: Address?)
val user: User? = User(Address("Hanoi"))
// Chuỗi safe call - null nếu bất kỳ phần nào null
val city = user?.address?.city?.uppercase()
println(city) // HANOI

Cung cấp giá trị mặc định khi null:

val name: String? = null
// Elvis operator
val displayName = name ?: "Guest"
println(displayName) // Guest
// Kết hợp với safe call
val length = name?.length ?: 0
println(length) // 0
fun getUser(id: Int): User {
val user = findUserById(id)
return user ?: throw IllegalArgumentException("User not found")
}
// Hoặc return sớm
fun processName(name: String?) {
val validName = name ?: return // Return nếu null
println(validName.uppercase())
}

!! ép Kotlin coi giá trị như non-null. Nếu null → crash:

val name: String? = "Kotlin"
val length = name!!.length // ✅ OK, length = 6
val nullName: String? = null
val length2 = nullName!!.length // ❌ KotlinNullPointerException!

Cast an toàn, trả về null nếu không thành công:

val obj: Any = "Hello"
// Safe cast
val str: String? = obj as? String // "Hello"
val num: Int? = obj as? Int // null (không crash)
// So sánh với unsafe cast
// val num2: Int = obj as Int // ❌ ClassCastException!

let rất hữu ích để xử lý nullable:

val email: String? = "user@example.com"
// Chỉ thực hiện khi không null
email?.let {
println("Sending email to: $it")
sendEmail(it)
}
// Với multiple operations
email?.let { validEmail ->
val domain = validEmail.substringAfter("@")
val username = validEmail.substringBefore("@")
println("User: $username, Domain: $domain")
}

Kotlin tự động cast sau khi kiểm tra null:

val name: String? = "Kotlin"
if (name != null) {
// Smart cast: name đã là String (non-null) trong block này
println(name.length) // Không cần name?.length
}
// Với when
when (name) {
null -> println("Name is null")
else -> println("Length: ${name.length}") // Smart cast
}

// List có thể null
val list1: List<String>? = null
// List chứa phần tử nullable
val list2: List<String?> = listOf("A", null, "B")
// Cả hai
val list3: List<String?>? = null
val mixedList: List<String?> = listOf("A", null, "B", null, "C")
val nonNullList: List<String> = mixedList.filterNotNull()
println(nonNullList) // [A, B, C]
// mapNotNull - map và lọc null cùng lúc
val numbers = listOf("1", "2", "abc", "3")
val parsed: List<Int> = numbers.mapNotNull { it.toIntOrNull() }
println(parsed) // [1, 2, 3]

Bài tập 1: Xử lý user có thể null

data class User(val name: String, val email: String?)
fun main() {
val user: User? = User("Bumbii", null)
// TODO: In ra greeting an toàn
// TODO: In ra email hoặc "No email"
}

Lời giải:

data class User(val name: String, val email: String?)
fun main() {
val user: User? = User("Bumbii", null)
// Greeting
val greeting = user?.let { "Hello, ${it.name}!" } ?: "Hello, Guest!"
println(greeting) // Hello, Bumbii!
// Email
val email = user?.email ?: "No email"
println("Email: $email") // Email: No email
}

Bài tập 2: Parse config an toàn

fun main() {
val config: Map<String, String?> = mapOf(
"host" to "localhost",
"port" to "8080",
"timeout" to null
)
// TODO: Lấy port dạng Int, mặc định 80
// TODO: Lấy timeout dạng Int, mặc định 30
}

Lời giải:

fun main() {
val config: Map<String, String?> = mapOf(
"host" to "localhost",
"port" to "8080",
"timeout" to null
)
val port = config["port"]?.toIntOrNull() ?: 80
val timeout = config["timeout"]?.toIntOrNull() ?: 30
println("Port: $port") // 8080
println("Timeout: $timeout") // 30
}

Bài tập 3: Chain of nullable operations

data class Address(val city: String?, val zipCode: String?)
data class Company(val name: String, val address: Address?)
data class Employee(val name: String, val company: Company?)
fun main() {
val employee: Employee? = Employee(
"Bumbii",
Company("TechCorp", Address("Hanoi", null))
)
// TODO: Lấy city uppercase hoặc "UNKNOWN"
// TODO: Lấy zipCode hoặc "N/A"
}

Lời giải:

fun main() {
val employee: Employee? = Employee(
"Bumbii",
Company("TechCorp", Address("Hanoi", null))
)
val city = employee?.company?.address?.city?.uppercase() ?: "UNKNOWN"
val zipCode = employee?.company?.address?.zipCode ?: "N/A"
println("City: $city") // HANOI
println("Zip: $zipCode") // N/A
}

// Intent extras - có thể null
val userId = intent.getStringExtra("USER_ID")
userId?.let { loadUser(it) }
// findViewById trả về nullable
val button = findViewById<Button>(R.id.myButton)
button?.setOnClickListener { /* ... */ }
// Với ViewBinding (non-null, an toàn hơn)
binding.myButton.setOnClickListener { /* ... */ }
// Nullable trong ViewModel
class UserViewModel : ViewModel() {
private val _user = MutableLiveData<User?>()
val user: LiveData<User?> = _user
}
// Observe trong Fragment
viewModel.user.observe(viewLifecycleOwner) { user ->
user?.let { displayUser(it) } ?: showEmptyState()
}

// Dùng safe call và Elvis
val length = name?.length ?: 0
// Dùng let cho nullable objects
user?.let { processUser(it) }
// Dùng require/check cho validation
fun process(data: String?) {
requireNotNull(data) { "Data cannot be null" }
// data là non-null từ đây
}
// Tránh !! khi có thể
val length = name!!.length // ❌ Có thể crash
// Tránh kiểm tra null verbose
if (user != null) {
if (user.address != null) {
if (user.address.city != null) {
// ...
}
}
}
// ✅ Thay bằng: user?.address?.city?.let { ... }

Sau bài học này, bạn có thể:

  • Phân biệt String (non-null) và String? (nullable)
  • Sử dụng safe call ?. thay vì kiểm tra null thủ công
  • Sử dụng Elvis ?: để cung cấp giá trị mặc định
  • Hiểu tại sao nên tránh !!
  • Sử dụng let để xử lý nullable
  • Sử dụng smart cast sau khi kiểm tra null
  • Filter nulls với filterNotNull()mapNotNull()

Tiếp theo: Type Conversion