1. NullPointerException là gì
Từ khóa null trong Java là một giá trị nguyên bản (literal) đại diện cho một tham chiếu rỗng (null reference) — tức là nó hoàn toàn không trỏ tới bất kỳ đối tượng (object) nào trong bộ nhớ.
Ví dụ:
1
String s = null;Dòng trên có nghĩa là biến s hiện tại không tham chiếu tới object String nào cả.
Bản thân dòng code trên không có lỗi gì cả, lỗi chỉ xuất hiện khi bạn cố gắng truy cập vào một phương thức hoặc thuộc tính của biến s trong khi nó đang là null. Đây là nơi dẫn đến các lỗi runtime khó kiểm soát.
1 2 3 4 5 6
public class TestNull { public static void main(String[] args) { String str = null; System.out.println(str.length()); // throw NullPointerException }}Nếu chạy đoạn code trên, chương trình sẽ bị crash và ném ra lỗi NullPointerException (NPE) - một trong những lỗi runtime phổ biến nhất trong Java. vì bạn đang cố gắng truy cập vào phương thức length() của một biến str đang là null.
Để khắc phục chúng, cách truyền thống và phổ biến nhất là sử dụng các câu lệnh kiểm tra null trước khi thao tác với biến (if-else, ...).
1 2 3 4 5 6 7 8 9 10 11
public class TestNull { public static void main(String[] args) { String str = null; if (str != null) { System.out.println(str.length()); } else { System.out.println("String is null"); } // Output: String is null }}Xét về logic, đoạn code trên chả có gì sai cả, nhưng nó làm code trở nên dài dòng và lặp đi lặp lại (boilerplate code) nếu về sau có nhiều biến cần kiểm tra null.
Và Kotlin, một ngôn ngữ lập trình hiện đại được phát triển bởi JetBrains, tương thích hoàn toàn với Java nhưng nó cung cấp nhiều tính năng hiện đại hơn. Trong đó có cơ chế Null Safety. Thay vì để lỗi null tới runtime mới lộ ra, Kotlin bắt compiler phải xử lý nó ngay từ lúc biên dịch.
2. Null Safety
Trong Kotlin, mọi biến đều được giả định là không thể null theo mặc định. Điều này có nghĩa là bạn không thể gán giá trị null cho một biến non-nullable.
1 2 3 4 5 6 7
fun main() { var str = "hello" // str = null // Error: Null can not be a value of a non-null type String println(str) var x: String? = null // OK}Thế thì, nếu muốn một biến có thể nhận giá trị null thì sao. Ta sẽ dùng ký hiệu ? để khai báo biến có thể nhận giá trị null.
Và nhớ khai báo cụ thể kiểu dữ liệu cho biến đó trước khi sử dụng dấu ?.
Dễ hiểu thì bạn cứ hiểu là dấu chấm hỏi
?cho compiler biết rằng cái biến đó có thể nhận giá trị null
Chỗ này lưu ý một chút, bạn không nên viết như này:
1
var x = nullVì null không nói lên được x nên là String?, Int?, User? hay một kiểu nào khác. Trong một số ngữ cảnh, compiler có thể suy ra kiểu rất hẹp là Nothing?, nhưng biến đó gần như không dùng được để chứa dữ liệu thật sau này.
Cho nên hãy khai báo rõ kiểu dữ liệu:
1 2
var name: String? = nullvar age: Int? = null1 2 3 4 5
fun main() { var str: String? = "hello" str = null // OK println(str) // Output: null}Một ví dụ khác chi tiết hơn
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
fun main() { // neverNull has String type var neverNull: String = "This can't be null" // Throws a compiler error neverNull = null // nullable has nullable String type var nullable: String? = "You can keep a null here" // This is OK nullable = null // By default, null values aren't accepted var inferredNonNull = "The compiler assumes non-nullable" // Throws a compiler error inferredNonNull = null // notNull doesn't accept null values fun strLength(notNull: String): Int { return notNull.length } println(strLength(neverNull)) // 18 println(strLength(nullable)) // Throws a compiler error}Nói ngắn gọn thì String và String? là hai kiểu khác nhau.
1 2
val a: String = "hello"val b: String? = "hello"Nhìn giá trị thì có vẻ giống nhau, nhưng với compiler thì a chắc chắn không null, còn b thì có thể null.
3. Safe Call Operator
Để truy cập vào một thuộc tính của 1 object mà bạn cho rằng nó có thể null, hãy sử dụng toán tử ?.
Toán tử này sẽ trả về null nếu object đứng trước dấu ?. đang là null, thay vì tiếp tục truy cập và ném ra một ngoại lệ NullPointerException.
Ví dụ dễ hiểu
1 2 3 4 5 6 7 8 9 10 11
fun main() { var name: String? = "Tui ten Dat" println(name?.length) // 11 var testNull: String? = "Tui ten Teo" testNull = null println(testNull?.length) // null val nullString: String? = null println(nullString?.uppercase()) // null}Như ví dụ trên, khi name có giá trị, name?.length sẽ trả về độ dài của chuỗi vì name khác null. Nhưng khi testNull là null, testNull?.length sẽ trả về null ngay lập tức mà không thực hiện câu lệnh length hay là ném ra lỗi NPE.
Bạn có thể kết hợp nhiều toán tử safe call liên tiếp để tạo thành một chuỗi kiểm tra. Nếu bất kỳ phần tử nào trong chuỗi là null, toàn bộ biểu thức sẽ trả về null.
1
person?.company?.address?.country // chỉ cần một trong các phần tử là `null` thì cả biểu thức trả về `null`Đoạn trên có thể hiểu tương tự như:
1 2 3 4 5
if (person != null && person.company != null && person.company.address != null) { person.company.address.country} else { null}Nhưng thay vì viết một đống if, Kotlin cho mình toán tử ?. để code gọn và dễ đọc hơn.
4. Elvis operator
Bạn có thể cung cấp một giá trị mặc định để trả về khi biểu thức trước toán tử Elvis là null. Toán tử Elvis được ký hiệu bằng ?:.
Ví dụ:
1 2 3 4
fun main() { var name: String? = null println(name ?: "Unknown") // Output: Unknown}Như các ví dụ trước, nếu bạn sử dụng name hoặc name?.length, kết quả sẽ là null.
Đôi khi kết quả trả về của một hàm không nhất thiết phải là null mà có thể là một giá trị mặc định, một mã status (như kiểu 404, 200, ...). Lúc này, toán tử Elvis sẽ làm việc đó.
Nhìn nó giống giống như ternary operator (
? :) trong C++/Java đấy. Nhưng trong Kotlin không có ternary operator (dùng if else expression). Cái toán tử trên trong Kotlin dành cho Elvis.
Cách hoạt động của toán tử Elvis: nếu biểu thức bên trái của ?: không phải là null, nó sẽ được trả về. Ngược lại, nếu biểu thức bên trái là null, thì biểu thức bên phải sẽ được trả về.
1
println(name ?: "Unknown") // Nếu name là null, in ra "Unknown", còn không thì in ra giá trị của nameNó tương đương với:
1 2 3 4 5
if (name != null) { name} else { "Unknown"}1 2 3 4 5
fun main() { val nullString: String? = null println(nullString?.length ?: 0) // 0}5. Non-null assertion operator
Đôi khi bạn chắc chắn rằng một biến nullable không phải là null tại một điểm nào đó trong code, nhưng compiler thì không thể biết điều đó. Trong những trường hợp như vậy, bạn có thể sử dụng toán tử non-null assertion !! để ép compiler chấp nhận rằng biến đó không phải là null.
Tuy nhiên, hãy cẩn thận khi sử dụng toán tử này, vì nếu biến thực sự là null tại thời điểm bạn sử dụng !!, chương trình của bạn sẽ ném ra lỗi ở runtime.
Toán tử
!!ép cho compiler phải hiểu rằng: "Biến này của tui lúc này nó không null, cứ truy cập đi, nếu tui sai thì crash".
Ví dụ:
1 2 3 4 5 6 7 8
fun getUsername(): String? { return "Kotlin"}fun main() { val name: String? = getUsername() println(name!!.length) // 6}Ở đây, giả sử bạn biết chắc chắn rằng hàm getUsername() sẽ không trả về null. Tuy nhiên, nhìn vào kiểu trả về String?, compiler vẫn phải xem name là một biến có thể null.
Vì vậy, khi bạn dùng !!, bạn đã nói rằng là biến này chắc chắn không null tại thời điểm đó.
Tuy nhiên, nếu biến bị thay đổi trong lúc runtime như thế này, chương trình sẽ báo lỗi
1 2 3 4 5 6 7
fun main() { var name: String? = "Kotlin" println(name!!.length) // 6 name = null println(name!!.length) // Error: NullPointerException}Cho nên tốt nhất là không nên sử dụng toán tử !! trong đa số các tình huống.
"Có cách nào viết an toàn hơn không?"
Ví dụ có thể đổi thành:
1
user?.nameHoặc nếu muốn có giá trị mặc định khi user là null:
1
val username = user?.name ?: ""Vậy khi nào nên dùng !!?
Câu trả lời là: chỉ dùng khi bạn chắc chắn 100% giá trị đó không thể null, và không có cách thiết kế nào tốt hơn. Còn nếu chưa chắc thì đừng dùng, vì !! gần như là bạn đang tự tay tắt bớt Null Safety của Kotlin.
6. Smart Cast
Đây là một phần rất quan trọng khi học Null Safety trong Kotlin.
Ví dụ bạn có một biến nullable:
1
val name: String? = "Dat"Bình thường, bạn không thể gọi trực tiếp:
1
println(name.length) // ErrorVì compiler biết rằng name có kiểu String?, tức là nó có thể null.
Nhưng nếu bạn đã kiểm tra null trước đó:
1 2 3 4 5 6 7
fun main() { val name: String? = "Dat" if (name != null) { println(name.length) // OK }}Sau dòng:
1
if (name != null)Kotlin tự hiểu rằng bên trong block if, biến name chắc chắn không còn là null nữa. Vì vậy compiler tự động xem name như một biến kiểu String thay vì String?.
Cơ chế này được gọi là Smart Cast.
Nói dễ hiểu thì compiler tự suy luận giúp mình:
1 2 3 4
if (name != null) { // Trong này name được xem như String println(name.length)}Nhờ Smart Cast, bạn không cần viết:
1
println(name!!.length)Đây cũng là lý do trong Kotlin, khi đã check null rồi thì đừng vội dùng !!, vì compiler đã đủ thông minh để hiểu giúp mình.
7. let
let là một trong các scope function của Kotlin (cùng nhóm với run, apply, also, with). Bản chất, let không phải sinh ra riêng cho việc xử lý null (nó chả có cơ chế xử lý null nào cả) — nó có thể gọi trên bất kỳ object nào, không chỉ object nullable:
1 2 3 4 5 6 7
fun main() { val name: String? = "Dat" name?.let { println(it.length) }}Tuy nhiên let thường được dùng kết hợp với safe call operator ?. để xử lý null một cách gọn gàng, giúp tạo ra một phạm vi (scope) mà ở đó, biến chắc chắn khác null, loại bỏ hoàn toàn sự rườm rà của các khối lệnh if not-null truyền thống.
Ở đây:
name?.sẽ kiểm tranamecó null hay không.- Nếu
namekhác null,.let { }mới được gọi, và block bên trong mới thực thi. - Nếu
namelà null, toàn bộ biểu thứcname?.let { ... }sẽ trả về null ngay lập tức mà không chạy vào block — giống hệt cơ chế của safe call operator. itlà tên tham số ngầm định (implicit parameter name) mà Kotlin tự gán khi một lambda function chỉ có đúng một tham số.- Tại thời điểm này, Kotlin đã "cast"
nametừString?sangStringgiúp bạn, bạn có thể gọi thẳngit.lengthmà không cần lo NPE.
?.let { } chính là một dạng thay thế gọn hơn cho:
1 2 3
if (name != null) { println(name.length)}nhưng ở dạng biểu thức (expression), nên bạn có thể dùng nó ngay trong một dòng, truyền tiếp giá trị trả về, hoặc chain tiếp các thao tác khác — điều mà if thông thường không làm được gọn như vậy.
Ví dụ nếu name là null:
1 2 3 4 5 6 7 8 9
fun main() { val name: String? = null name?.let { println(it.length) } println("Done")}Kết quả:
1
DoneBlock let không chạy vì name đang là null.
Bạn cũng có thể kết hợp ?.let với Elvis operator (?:) hoặc dùng riêng lẻ tùy vào mục đích:
- Nếu chỉ muốn lấy giá trị mặc định khi null: Dùng Elvis operator
(?:).
1
val username = intent.getStringExtra("username") ?: ""- Nếu chỉ muốn chạy một block code khi khác null: Dùng
?.let.
1 2 3
intent.getStringExtra("username")?.let { username -> println(username)}- Nếu muốn vừa chạy block code khi khác null, vừa xử lý case null (thay cho if-else): Kết hợp cả
?.letvà?:.
1 2 3 4 5
intent.getStringExtra("username")?.let { username -> println(username)} ?: run { println("Username không tồn tại")}Tuỳ tình huống mà chọn cách viết. Nếu cần giá trị mặc định thì dùng ?:. Nếu cần chạy một block code khi giá trị khác null thì dùng ?.let.
8. Safe Cast
Bình thường trong Kotlin, nếu bạn ép kiểu sai bằng as, chương trình có thể crash.
Ví dụ:
1 2 3 4 5 6
fun main() { val obj: Any = 123 val str = obj as String // Error: ClassCastException println(str)}Ở đây obj thật ra là Int, nhưng mình lại ép nó thành String, nên chương trình sẽ ném ra ClassCastException.
Để ép kiểu an toàn hơn, Kotlin cung cấp as?, gọi là safe cast.
1 2 3 4 5 6
fun main() { val obj: Any = "Hello" val str = obj as? String println(str) // Hello}Nếu cast thành công, biến str sẽ nhận giá trị sau khi cast.
Còn nếu cast thất bại:
1 2 3 4 5 6
fun main() { val obj: Any = 123 val str = obj as? String println(str) // null}Thay vì crash, str sẽ là null.
Vì vậy kiểu của str lúc này sẽ là:
1
String?Safe cast rất hữu ích khi bạn làm việc với dữ liệu không chắc kiểu, ví dụ như object từ list nhiều kiểu dữ liệu, argument, bundle, hoặc dữ liệu từ một layer khác truyền qua.
9. Kotlin có loại bỏ hoàn toàn NPE không?
Câu trả lời đương nhiên là không rồi.
Kotlin không loại bỏ hoàn toàn NullPointerException. Kotlin chỉ giúp giảm cực mạnh khả năng gặp NPE bằng cách bắt lỗi ngay từ compile time.
Nhưng bạn vẫn có thể gặp lỗi liên quan tới null trong một số trường hợp.
Dùng !!
Ví dụ:
1 2 3 4
fun main() { val name: String? = null println(name!!.length)}Đoạn này vẫn crash như thường, vì mình đã ép compiler tin rằng name không null, trong khi thực tế nó đang là null.
Tuỳ Kotlin version/runtime mà bạn có thể thấy lỗi hiển thị là NullPointerException hoặc KotlinNullPointerException, nhưng ý nghĩa vẫn giống nhau: bạn dùng !! sai và chương trình crash ở runtime.
Java Interoperability
Kotlin tương thích rất tốt với Java, nhưng chính vì vậy nó cũng phải làm việc với code Java.
Ví dụ bên Java có một hàm:
1 2 3
public String getName() { return null;}Khi gọi từ Kotlin:
1 2
val name = javaObj.nameprintln(name.length)Nhìn qua thì name.length có vẻ hợp lệ, nhưng nếu Java trả về null, app vẫn có thể crash.
Lý do là vì Java không có hệ thống nullable rõ ràng như Kotlin. Khi Kotlin nhận dữ liệu từ Java, kiểu dữ liệu đó có thể trở thành Platform Type.
Bạn có thể hiểu Platform Type là kiểu mà Kotlin không chắc nó có nullable hay không.
Cho nên khi làm việc với Java code, Android SDK cũ, hoặc một số thư viện Java, bạn vẫn nên cẩn thận:
1 2
val name: String? = javaObj.nameprintln(name?.length ?: 0)lateinit
lateinit cho phép bạn khai báo một biến non-null nhưng khởi tạo sau.
Ví dụ:
1
lateinit var user: UserNhìn vào kiểu dữ liệu thì user là User, không phải User?. Nhưng nếu bạn dùng nó trước khi khởi tạo:
1
println(user.name)Chương trình sẽ ném ra:
1
UninitializedPropertyAccessExceptionLỗi này không phải NullPointerException, nhưng bản chất vấn đề vẫn khá giống: bạn đang truy cập vào một biến chưa có giá trị hợp lệ.
Trong Android, lateinit hay gặp khi khai báo view binding, adapter, view model, v.v.
Ví dụ:
1 2 3 4 5 6 7 8
private lateinit var adapter: UserAdapteroverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) adapter = UserAdapter() recyclerView.adapter = adapter}Nếu bạn dùng adapter trước dòng:
1
adapter = UserAdapter()thì app sẽ crash.
Và cuối cùng, hãy nhớ rằng Kotlin không làm NPE biến mất hoàn toàn. Nó chỉ giúp mình thiết kế code an toàn hơn, rõ ràng hơn, và bắt được nhiều lỗi hơn ngay từ lúc compile.
Bạn có thể tìm đọc thêm tại các resource mà mình đã tham khảo bên dưới
