Khởi tạo trễ (lazy initialization) là một cơ chế trong Kotlin mà theo đó một thuộc tính của đối tượng được khởi tạo không phải tại thời điểm tạo ra, mà khi được truy cập lần đầu tiên. Theo JetBrains, 2024, lateinit và lazy là hai công cụ tích hợp để thực hiện chiến lược này. Cả hai đều giải quyết vấn đề khởi tạo trễ, nhưng khác nhau cơ bản về cơ chế hoạt động và phạm vi ứng dụng.
Những điểm chính
Khởi tạo trễ là một mẫu trong đó một thuộc tính của lớp nhận giá trị không phải tại thời điểm xây dựng đối tượng, mà sau đó, theo yêu cầu. Trong Kotlin, mẫu này được thực hiện theo hai cách khác nhau cơ bản: bổ từ lateinit và ủy quyền lazy.
Cả hai cơ chế đều giải quyết một vấn đề chung — một thuộc tính phải tồn tại trong lớp, nhưng giá trị của nó hoặc chưa biết tại thời điểm tạo đối tượng, hoặc việc tính toán nó quá tốn kém tài nguyên để thực hiện một cách không cần thiết. Theo Google I/O 2023, có tới 40% thuộc tính trong một ứng dụng Android điển hình có thể được tối ưu hóa thông qua khởi tạo trễ, giúp giảm thời gian khởi động từ 15–25%.
Sự lựa chọn giữa lateinit và lazy được xác định bởi ba yếu tố: tính khả biến của thuộc tính (var hay val), vòng đời của nó (gán một lần hay nhiều lần) và yêu cầu về an toàn luồng (truy cập đơn luồng hay đa luồng).
Kịch bản đầu tiên và phổ biến nhất là Tiêm phụ thuộc. Framework (Dagger, Hilt, Koin) tiêm các phụ thuộc sau khi tạo đối tượng, do đó thuộc tính không thể được khởi tạo trong hàm tạo. Nếu không có lateinit, tất cả các phụ thuộc sẽ phải được khai báo là nullable và kiểm tra mỗi khi sử dụng.
Kịch bản thứ hai là tài nguyên nặng: cơ sở dữ liệu, máy khách mạng, trình quản lý tệp. Việc tạo chúng đòi hỏi thời gian và bộ nhớ, do đó chúng chỉ nên được khởi tạo khi thực sự được sử dụng. lazy là lý tưởng cho những trường hợp như vậy, đảm bảo tạo một lần.
Tình huống thứ ba là các thành phần Android (Activity, Fragment, ViewModel) có vòng đời được quản lý bởi hệ điều hành. Các thuộc tính phụ thuộc vào onCreate, onViewCreated hoặc khối init của ViewModel không thể được khởi tạo trong hàm tạo.
lateinit là một bổ từ cho thuộc tính var cho phép trình biên dịch Kotlin trì hoãn việc khởi tạo. Trình biên dịch không yêu cầu gán giá trị trong hàm tạo, nhưng tạo ra một kiểm tra thời gian chạy ở mỗi lần truy cập: nếu thuộc tính chưa được khởi tạo, nó sẽ ném UninitializedPropertyAccessException.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
Hạn chế của lateinit: thuộc tính phải được khai báo là var (không phải val), không được nullable và không phải kiểu nguyên thủy (Int, Double, Boolean, v.v.). Lý do là các kiểu nguyên thủy được biên dịch thành các kiểu nguyên thủy JVM, không có trạng thái “chưa khởi tạo”. Đối với các thuộc tính nullable, khởi tạo trễ là không cần thiết: null đã có nghĩa là không có giá trị.
Để kiểm tra trạng thái của một thuộc tính lateinit, hãy sử dụng tham chiếu tích hợp qua toán tử ::: ::propertyName.isInitialized. Đây là cách an toàn duy nhất để kiểm tra xem một thuộc tính đã được khởi tạo hay chưa mà không có rủi ro ngoại lệ. Việc kiểm tra chỉ khả dụng từ cùng một lớp hoặc lớp bên trong, không phải từ mã bên ngoài.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit không thêm chi phí sau khi khởi tạo: sau khi giá trị được gán, truy cập thuộc tính giống hệt như truy cập trực tiếp vào trường. Chi phí duy nhất là kiểm tra khởi tạo ở mỗi lần đọc trước khi gán. Sau khi khởi tạo, trình biên dịch JIT sẽ tối ưu hóa việc kiểm tra.
Một lưu ý quan trọng: các thuộc tính lateinit không thể được sử dụng trong lớp inline và không được hỗ trợ cho các thuộc tính có getter/setter tùy chỉnh. Nếu một thuộc tính yêu cầu truy cập tính toán, hãy sử dụng lazy thay vì lateinit.
lazy là một ủy quyền thuộc tính được tích hợp trong thư viện chuẩn của Kotlin. Nó tính toán giá trị khi truy cập lần đầu tiên vào thuộc tính và lưu kết quả vào bộ nhớ đệm cho tất cả các lần gọi tiếp theo. Không giống như lateinit, lazy chỉ hoạt động với val, làm cho thuộc tính trở nên bất biến sau khi khởi tạo.
class UserRepository {
private val database: Database by lazy {
Database.create("users.db")
}
fun getUser(id: String): User {
return database.query("SELECT * FROM users WHERE id = ?", id)
}
}
lazy chấp nhận một tham số tùy chọn LazyThreadSafetyMode kiểm soát cơ chế an toàn luồng. Mặc định là SYNCHRONIZED — kiểm tra kép với khóa, đảm bảo khởi tạo một lần ngay cả khi truy cập đồng thời từ nhiều luồng.
Chế độ PUBLICATION cho phép khởi tạo song song: nhiều luồng có thể đồng thời thực thi khối khởi tạo, nhưng kết quả chỉ được chấp nhận từ luồng hoàn thành đầu tiên. Chế độ này nhanh hơn SYNCHRONIZED khi có tranh chấp cao, nhưng làm tăng tiêu thụ tài nguyên.
Chế độ NONE vô hiệu hóa hoàn toàn đồng bộ hóa. Chỉ sử dụng nó cho các thuộc tính được đảm bảo truy cập từ một luồng duy nhất. Trong chế độ này, lazy hoạt động với chi phí tối thiểu — gần giống như gán trực tiếp.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy là lựa chọn đúng đắn cho các phụ thuộc được khởi tạo một lần: kho lưu trữ, máy khách mạng, bộ nhớ đệm, cơ sở dữ liệu. Ngữ nghĩa của val bảo vệ khỏi việc ghi đè vô tình, và an toàn luồng mặc định làm cho mã an toàn trong môi trường đa luồng. lazy cũng hoạt động chính xác với kiểu nguyên thủy, điều không thể với lateinit.
Trong Android, lazy thường được sử dụng để khởi tạo phụ thuộc ViewModel qua by viewModels() hoặc để tạo máy khách Retrofit. Tuy nhiên, hãy cẩn thận: nếu một khối lazy nắm bắt tham chiếu đến Activity hoặc Fragment, nó có thể dẫn đến rò rỉ bộ nhớ, vì ủy quyền giữ bao đóng trong suốt vòng đời của thuộc tính.
Sự lựa chọn giữa lateinit và lazy không phải là vấn đề sở thích, mà là một quyết định kiến trúc được xác định bởi bản chất của thuộc tính. Mỗi cơ chế giải quyết nhiệm vụ riêng của nó và các lĩnh vực ứng dụng của chúng chỉ chồng chéo một phần.
| Tiêu chí | lateinit | lazy |
|---|---|---|
| Loại thuộc tính | chỉ var | chỉ val |
| Nullable | không được phép | được phép |
| Kiểu nguyên thủy | không được phép | được phép |
| An toàn luồng | không đảm bảo | SYNCHRONIZED mặc định |
| Kiểm tra trạng thái | ::x.isInitialized | không cần |
| Ngoại lệ khi lỗi | UninitializedPropertyAccessException | lỗi trong khối init |
| Lưu đệm | không áp dụng | tính toán một lần |
| Android Binding | View Binding, Data Binding | không được sử dụng |
| Framework DI | Dagger, Hilt, Koin | tiêm thủ công |
Sử dụng lateinit khi một thuộc tính phải thay đổi sau khi khởi tạo hoặc việc tạo nó được quản lý bởi mã bên ngoài. Một ví dụ điển hình là View Binding trong Android Activity: binding được tạo trong onCreate nhưng vẫn là var vì framework không hỗ trợ val cho kịch bản này.
Sử dụng lazy khi một thuộc tính được khởi tạo một lần, việc tính toán nó tốn kém và giá trị không thay đổi trong suốt vòng đời của đối tượng. Một ví dụ kinh điển là tạo lười biếng máy khách Retrofit hoặc cơ sở dữ liệu Room khi truy cập lần đầu vào kho lưu trữ.
Cả hai cơ chế có thể được sử dụng đồng thời trong cùng một lớp. Ví dụ, lateinit cho View Binding và lazy cho kho lưu trữ. Đây là một thực hành bình thường phản ánh các yêu cầu khác nhau cho các thuộc tính khác nhau. Điều quan trọng là không nhầm lẫn ngữ nghĩa: không sử dụng lateinit ở nơi cần val, và không sử dụng lazy cho các thuộc tính cần được gán lại.
Lỗi phổ biến nhất với lateinit là truy cập thuộc tính trước khi nó được khởi tạo. Điều này dẫn đến UninitializedPropertyAccessException, không bị bắt tại thời điểm biên dịch vì Kotlin tin tưởng nhà phát triển đảm bảo thứ tự khởi tạo chính xác. Giải pháp là luôn kiểm tra trạng thái qua ::property.isInitialized trước khi truy cập trong các tình huống không rõ ràng.
Vấn đề phổ biến thứ hai là sử dụng lateinit cho các thuộc tính về mặt ngữ nghĩa là val. Nếu giá trị được đặt một lần và không bao giờ thay đổi, lazy là lựa chọn đúng đắn hơn. Nó làm cho thuộc tính bất biến, ngăn ghi đè vô tình và thêm an toàn luồng miễn phí.
Lỗi thứ ba là lazy có tác dụng phụ. Khối khởi tạo lazy không nên sửa đổi trạng thái bên ngoài hoặc phụ thuộc vào thứ tự khởi tạo của các thuộc tính lazy khác, vì trình tự tính toán phụ thuộc vào lần truy cập đầu tiên và có thể không rõ ràng. Nếu các thuộc tính lazy tham chiếu lẫn nhau, điều này dẫn đến phụ thuộc vòng tròn và StackOverflowError.
Vấn đề thứ tư là rò rỉ bộ nhớ qua lazy trong Android. Nếu một khối lazy nắm bắt tham chiếu đến Activity hoặc Fragment, ủy quyền giữ bao đóng và bộ thu gom rác không thể giải phóng thành phần ngay cả sau khi nó bị hủy. Giải pháp là chỉ sử dụng lazy với các đối tượng tồn tại ngắn hoặc truyền ngữ cảnh Application thay vì Activity.
Lỗi điển hình thứ năm là cố gắng áp dụng lateinit cho kiểu nguyên thủy. Trình biên dịch Kotlin chặn điều này ở cấp cú pháp, nhưng các nhà phát triển cố gắng vượt qua giới hạn thông qua trình bao bọc nullable. Điều này dẫn đến các kiểm tra null không cần thiết và hoàn toàn vô hiệu hóa lợi ích của khởi tạo trễ.
Câu hỏi thường gặp
lateinit là một bổ từ cho thuộc tính var, cho phép khởi tạo sau hàm tạo. lazy là một ủy quyền cho thuộc tính val, tính toán giá trị khi truy cập lần đầu và lưu vào bộ nhớ đệm. lateinit không hỗ trợ kiểu nguyên thủy và nullable, trong khi lazy an toàn luồng theo mặc định.
Có, qua tham chiếu thuộc tính tích hợp: ::propertyName.isInitialized. Phương thức trả về true nếu thuộc tính đã được khởi tạo. Đây là cách an toàn duy nhất để tránh UninitializedPropertyAccessException khi làm việc với các trường lateinit.
Các kiểu nguyên thủy — Int, Double, Boolean và các kiểu khác — được biên dịch thành các kiểu nguyên thủy JVM (int, double, boolean), không có trạng thái “chưa khởi tạo”. lateinit sử dụng null làm cờ, và các kiểu nguyên thủy không thể là null, do đó cơ chế này là không thể về mặt vật lý đối với các kiểu này.
Mặc định là LazyThreadSafetyMode.SYNCHRONIZED — kiểm tra kép với khóa, đảm bảo khởi tạo một lần khi truy cập đồng thời từ nhiều luồng. Đối với kịch bản đơn luồng, hãy sử dụng NONE; đối với tranh chấp cao, hãy sử dụng PUBLICATION.
Khi thuộc tính phải thay đổi sau khi khởi tạo hoặc việc tạo nó được quản lý bởi framework. Một ví dụ điển hình là View Binding trong Android Activity: binding được tạo trong onCreate và phải là var. Đối với các phụ thuộc val được khởi tạo một lần, hãy sử dụng lazy.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm