> cừn.
HomeBlogNewsProjectsAbout
cừn.
~/blog
article.mdx

Null Safety in Kotlin

Exploring null safety features in Kotlin

07/09/2025cừn.12 phút2,270 từ

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ụ:

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

java
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, ...).

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

kotlin
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:

kotlin
1
var x = null

Vì 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:

kotlin
1
2
var name: String? = null
var age: Int? = null
kotlin
1
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

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

kotlin
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

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

kotlin
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ư:

kotlin
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ụ:

kotlin
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ề.

kotlin
1
println(name ?: "Unknown") // Nếu name là null, in ra "Unknown", còn không thì in ra giá trị của name

Nó tương đương với:

kotlin
1
2
3
4
5
if (name != null) {
name
} else {
"Unknown"
}
kotlin
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ụ:

kotlin
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

kotlin
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:

kotlin
1
user?.name

Hoặc nếu muốn có giá trị mặc định khi user là null:

kotlin
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:

kotlin
1
val name: String? = "Dat"

Bình thường, bạn không thể gọi trực tiếp:

kotlin
1
println(name.length) // Error

Vì 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 đó:

kotlin
1
2
3
4
5
6
7
fun main() {
val name: String? = "Dat"
if (name != null) {
println(name.length) // OK
}
}

Sau dòng:

kotlin
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:

kotlin
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:

kotlin
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:

kotlin
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 tra name có null hay không.
  • Nếu name khác null, .let { } mới được gọi, và block bên trong mới thực thi.
  • Nếu name là null, toàn bộ biểu thức name?.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.
  • it là 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" name từ String? sang String giúp bạn, bạn có thể gọi thẳng it.length mà không cần lo NPE.

?.let { } chính là một dạng thay thế gọn hơn cho:

kotlin
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:

kotlin
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ả:

text
1
Done

Block 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 (?:).
kotlin
1
val username = intent.getStringExtra("username") ?: ""
  • Nếu chỉ muốn chạy một block code khi khác null: Dùng ?.let.
kotlin
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ả ?.let và ?:.
kotlin
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ụ:

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

kotlin
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:

kotlin
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à:

kotlin
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ụ:

kotlin
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:

java
1
2
3
public String getName() {
return null;
}

Khi gọi từ Kotlin:

kotlin
1
2
val name = javaObj.name
println(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:

kotlin
1
2
val name: String? = javaObj.name
println(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ụ:

kotlin
1
lateinit var user: User

Nhì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:

kotlin
1
println(user.name)

Chương trình sẽ ném ra:

text
1
UninitializedPropertyAccessException

Lỗ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ụ:

kotlin
1
2
3
4
5
6
7
8
private lateinit var adapter: UserAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = UserAdapter()
recyclerView.adapter = adapter
}

Nếu bạn dùng adapter trước dòng:

kotlin
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

  • Null Safety in Kotlin - Official Docs
  • Kotlin Null Safety - Nullable Receiver
  • Android Codelab: Nullability in Kotlin

Thanks for your reading!

07/09/2025

Cừn.

Chia sẻ bài viết

FacebookLinkedInX
$cd ..