🛡️ 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.
💡 Vấn đề với Null
Phần tiêu đề “💡 Vấn đề với Null”NullPointerException (NPE) là lỗi phổ biến nhất trong Java:
// Java - có thể crashString 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ể nullvar name: String? = null // ✅ OK! String? có thể null📝 Nullable vs Non-Nullable Types
Phần tiêu đề “📝 Nullable vs Non-Nullable Types”Non-Nullable (Mặc định)
Phần tiêu đề “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 compileNullable với ?
Phần tiêu đề “Nullable với ?”Thêm ? sau kiểu để cho phép null:
var name: String? = "Kotlin"name = null // ✅ OK
var age: Int? = 25age = null // ✅ OK⚡ Safe Call Operator ?.
Phần tiêu đề “⚡ 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 callprintln(name?.length) // 6
// Nếu nullval nullName: String? = nullprintln(nullName?.length) // null (không crash!)Chuỗi Safe Calls
Phần tiêu đề “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 nullval city = user?.address?.city?.uppercase()println(city) // HANOI🎯 Elvis Operator ?:
Phần tiêu đề “🎯 Elvis Operator ?:”Cung cấp giá trị mặc định khi null:
val name: String? = null
// Elvis operatorval displayName = name ?: "Guest"println(displayName) // Guest
// Kết hợp với safe callval length = name?.length ?: 0println(length) // 0Throw exception với Elvis
Phần tiêu đề “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ớmfun processName(name: String?) { val validName = name ?: return // Return nếu null println(validName.uppercase())}⚠️ Not-Null Assertion !!
Phần tiêu đề “⚠️ 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? = nullval length2 = nullName!!.length // ❌ KotlinNullPointerException!🔍 Safe Cast với as?
Phần tiêu đề “🔍 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 castval 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
Phần tiêu đề “🧪 Let function với Nullable”let rất hữu ích để xử lý nullable:
val email: String? = "user@example.com"
// Chỉ thực hiện khi không nullemail?.let { println("Sending email to: $it") sendEmail(it)}
// Với multiple operationsemail?.let { validEmail -> val domain = validEmail.substringAfter("@") val username = validEmail.substringBefore("@") println("User: $username, Domain: $domain")}🔧 Smart Cast
Phần tiêu đề “🔧 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 whenwhen (name) { null -> println("Name is null") else -> println("Length: ${name.length}") // Smart cast}📋 Nullable Collections
Phần tiêu đề “📋 Nullable Collections”// List có thể nullval list1: List<String>? = null
// List chứa phần tử nullableval list2: List<String?> = listOf("A", null, "B")
// Cả haival list3: List<String?>? = nullFilter out nulls
Phần tiêu đề “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úcval numbers = listOf("1", "2", "abc", "3")val parsed: List<Int> = numbers.mapNotNull { it.toIntOrNull() }println(parsed) // [1, 2, 3]🛠️ Thực hành
Phần tiêu đề “🛠️ 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
Phần tiêu đề “📱 Trong Android”// Intent extras - có thể nullval userId = intent.getStringExtra("USER_ID")userId?.let { loadUser(it) }
// findViewById trả về nullableval button = findViewById<Button>(R.id.myButton)button?.setOnClickListener { /* ... */ }
// Với ViewBinding (non-null, an toàn hơn)binding.myButton.setOnClickListener { /* ... */ }
// Nullable trong ViewModelclass UserViewModel : ViewModel() { private val _user = MutableLiveData<User?>() val user: LiveData<User?> = _user}
// Observe trong FragmentviewModel.user.observe(viewLifecycleOwner) { user -> user?.let { displayUser(it) } ?: showEmptyState()}🧠 Best Practices
Phần tiêu đề “🧠 Best Practices”✅ Nên làm
Phần tiêu đề “✅ Nên làm”// Dùng safe call và Elvisval length = name?.length ?: 0
// Dùng let cho nullable objectsuser?.let { processUser(it) }
// Dùng require/check cho validationfun process(data: String?) { requireNotNull(data) { "Data cannot be null" } // data là non-null từ đây}❌ Không nên làm
Phần tiêu đề “❌ Không nên làm”// Tránh !! khi có thểval length = name!!.length // ❌ Có thể crash
// Tránh kiểm tra null verboseif (user != null) { if (user.address != null) { if (user.address.city != null) { // ... } }}// ✅ Thay bằng: user?.address?.city?.let { ... }✅ Checklist - Tự kiểm tra
Phần tiêu đề “✅ 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()vàmapNotNull()
Tiếp theo: Type Conversion