Skip to Content

Null Safety trong Kotlin

🎯 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.


💡 Vấn đề với Null

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

📝 Nullable vs Non-Nullable Types

Non-Nullable (Mặc định)

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

Nullable với ?

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

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

⚡ Safe Call Operator ?.

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!)

Chuỗi Safe Calls

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

🎯 Elvis Operator ?:

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

Throw exception với Elvis

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()) }

⚠️ Not-Null Assertion !!

!! é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!

[!CAUTION] Tránh dùng !! trừ khi bạn 100% chắc chắn! Đây là cách duy nhất gây NPE trong Kotlin.


🔍 Safe Cast với as?

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 function với Nullable

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

val email: String? = "[email protected]" // 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") }

🔧 Smart Cast

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 }

📋 Nullable Collections

// 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

Filter out nulls

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]

🛠️ Thực hành

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 }

📱 Trong Android

// 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() }

🧠 Best Practices

✅ Nên làm

// 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 }

❌ Không nên làm

// 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 { ... }

✅ Checklist - Tự kiểm tra

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

Last updated on