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 compileNullable 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) // 0Throw 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?>? = nullFilter 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()vàmapNotNull()
Tiếp theo: Type Conversion