Null Safety trong Kotlin
1. Giới thiệu
Null Safety là một trong những tính năng quan trọng và đặc biệt nhất của Kotlin, giúp loại bỏ NullPointerException (NPE) - lỗi phổ biến nhất trong Java.
Trong Python, bạn có thể gặp lỗi NoneType:
name = None
print(name.upper()) # AttributeError: 'NoneType' object has no attribute 'upper'Kotlin giải quyết vấn đề này ngay tại thời điểm biên dịch!
2. Nullable vs Non-Nullable Types
2.1. Non-Nullable (Không thể null)
Mặc định, tất cả các biến trong Kotlin không thể là null:
var name: String = "Kotlin"
// name = null // Lỗi biên dịch: Null can not be a value of a non-null type String2.2. Nullable (Có thể null)
Để cho phép biến có giá trị null, thêm dấu ? sau kiểu dữ liệu:
var name: String? = "Kotlin"
name = null // OK3. Làm việc với Nullable Types
3.1. Kiểm tra null trước khi sử dụng
var name: String? = "Alice"
// Cách 1: Kiểm tra if truyền thống
if (name != null) {
println(name.length) // OK, Kotlin biết name không null
}
// Cách 2: Sử dụng if expression
val length = if (name != null) name.length else 03.2. Safe Call Operator ?.
Toán tử ?. chỉ thực thi nếu giá trị không null:
var name: String? = "Alice"
// Cách thông thường
if (name != null) {
println(name.length)
}
// Sử dụng safe call
println(name?.length) // In ra length nếu name != null, ngược lại in ra nullVí dụ phức tạp hơn:
var person: Person? = getPerson()
// Chuỗi safe call
val city = person?.address?.city?.uppercase()
// Tương đương với:
var city: String? = null
if (person != null) {
if (person.address != null) {
if (person.address.city != null) {
city = person.address.city.uppercase()
}
}
}3.3. Elvis Operator ?:
Toán tử Elvis cung cấp giá trị mặc định khi giá trị là null:
var name: String? = null
val length = name?.length ?: 0 // Nếu name null, trả về 0
println(length) // 0
// Ví dụ khác
val displayName = name ?: "Guest"
println(displayName) // "Guest"Kết hợp safe call và Elvis:
val result = person?.name?.uppercase() ?: "UNKNOWN"3.4. Not-Null Assertion Operator !!
Toán tử !! cho Kotlin biết bạn chắc chắn giá trị không null. Nếu null, sẽ throw NPE:
var name: String? = "Alice"
val length = name!!.length // OK, length = 5
name = null
val length2 = name!!.length // Ném ra KotlinNullPointerException!Cảnh báo:
- Tránh sử dụng
!!trừ khi bạn hoàn toàn chắc chắn - Đây là cách duy nhất gây ra NPE trong Kotlin
- Nên sử dụng
?.hoặc?:thay thế
3.5. Safe Cast as?
Chuyển đổi kiểu an toàn, trả về null nếu không thành công:
val obj: Any = "Hello"
val str: String? = obj as? String // OK, str = "Hello"
val num: Int? = obj as? Int // null (không throw exception)
// So sánh với cast thông thường
val num2: Int = obj as Int // ClassCastException!4. Let function với Nullable
Function let rất hữu ích khi làm việc với nullable:
var name: String? = "Alice"
name?.let {
// Code block này chỉ chạy nếu name != null
// 'it' là giá trị non-null của name
println("Name length: ${it.length}")
println("Uppercase: ${it.uppercase()}")
}Ví dụ thực tế:
fun processUser(userId: String?) {
userId?.let {
// Chỉ xử lý khi userId không null
val user = findUser(it)
user?.let { u ->
sendEmail(u.email)
updateLastLogin(u.id)
}
}
}5. Nullable Collections
5.1. Nullable vs Collection of Nullables
// List có thể null
val list1: List<String>? = null
// List không null, nhưng chứa các phần tử nullable
val list2: List<String?> = listOf("A", null, "B")
// List có thể null VÀ chứa các phần tử nullable
val list3: List<String?>? = null5.2. Filter Not Null
val list: List<String?> = listOf("A", null, "B", null, "C")
val nonNullList: List<String> = list.filterNotNull()
println(nonNullList) // [A, B, C]6. Platform Types
Khi làm việc với Java, Kotlin không biết kiểu có nullable hay không. Đây gọi là Platform Types (ký hiệu Type!):
// Code Java
public String getName() { return null; }
// Code Kotlin
val name = javaObject.getName() // Platform type String!
// Bạn phải tự quyết định xử lý null7. So sánh với Python
| Kotlin | Python | Ghi chú |
|---|---|---|
String? | Optional[str] | Nullable type |
name?.length | name.length if name else None | Safe call |
name ?: "default" | name or "default" | Elvis operator |
name!! | name (không kiểm tra) | Not-null assertion |
8. Ví dụ thực tế
data class User(
val id: Int,
val name: String,
val email: String?,
val phone: String?
)
fun main() {
val user = User(1, "Alice", "alice@example.com", null)
// Safe call với Elvis operator
val email = user.email ?: "No email"
val phone = user.phone ?: "No phone"
println("Email: $email") // Email: alice@example.com
println("Phone: $phone") // Phone: No phone
// Sử dụng let
user.email?.let {
println("Sending email to: $it")
}
// Chaining safe calls
val emailLength = user.email?.trim()?.length ?: 0
println("Email length: $emailLength")
// Filter nullable list
val users = listOf(
User(1, "Alice", "alice@email.com", null),
User(2, "Bob", null, "123456"),
User(3, "Charlie", "charlie@email.com", "789012")
)
val usersWithEmail = users.filter { it.email != null }
println("Users with email: ${usersWithEmail.size}")
}9. Best Practices
- Ưu tiên non-nullable types khi có thể
- Sử dụng
?.thay vì!!trong hầu hết trường hợp - Kết hợp
?.với?:để có giá trị mặc định - Sử dụng
letđể xử lý nullable một cách elegant - Tránh
!!trừ khi bạn 100% chắc chắn - Kiểm tra null sớm trong function với
require()hoặccheck()
fun processUser(user: User?) {
requireNotNull(user) { "User cannot be null" }
// Từ đây trở đi, user là non-null
println(user.name)
}10. Null Safety trong Function
// Return nullable
fun findUser(id: Int): User? {
return if (id > 0) User(id, "User $id") else null
}
// Parameter nullable
fun greet(name: String?) {
println("Hello, ${name ?: "Guest"}")
}
// Sử dụng
val user = findUser(1)
user?.let {
greet(it.name)
} ?: greet(null)📝 Tóm tắt
- Null Safety là tính năng đặc trưng của Kotlin
- Non-nullable (mặc định):
String - Nullable:
String?(thêm dấu?) - Safe call:
?.- gọi an toàn - Elvis operator:
?:- giá trị mặc định - Not-null assertion:
!!- ném NPE nếu null (tránh dùng) - Safe cast:
as?- chuyển kiểu an toàn - Let function:
?.let { }- xử lý khi không null - Null Safety giúp loại bỏ NPE ngay tại compile-time