So sánh Kotlin reified generics và Java generics

Hãy xem đoạn mã sau:

/**
 * Hàm bậc cao: Phân tích cú pháp JSON an toàn
 * @param jsonMessage Chuỗi JSON
 * @param onSuccess Callback khi phân tích thành công
 * @param onError Callback khi phân tích thất bại (tùy chọn)
 */
private inline fun <reified T> safeParseJson(
    jsonMessage: String,
    onSuccess: (T) -> Unit,
    noinline onError: ((Exception) -> Unit)? = null
) {
    try {
        val result = gson.fromJson(jsonMessage, T::class.java)
        onSuccess(result)
    } catch (e: JsonSyntaxException) {
        Log.e(TAG, "Lỗi phân tích JSON - Loại: ${T::class.simpleName}, lỗi: ${e.message}")
        Log.e(TAG, "JSON có vấn đề: ${jsonMessage.take(300)}")
        onError?.invoke(e)
    } catch (e: Exception) {
        Log.e(TAG, "Lỗi ngoại lệ - Loại: ${T::class.simpleName}, lỗi: ${e.message}")
        onError?.invoke(e)
    }
}

<reified T> ở đây nghĩa là gì?

Kotlin reified

1. Hiểu vấn đề: Tại sao đôi khi không thể lấy được kiểu của generic?

Bạn biết rằng cả Kotlin và Java đều có generic, ví dụ:

fun <T> printType(item: T) {
    println(item::class.java)
}

Bạn có thể nghĩ rằng nó sẽ in ra kiểu, nhưng nếu bạn gọi như sau:

printType(listOf("abc"))

Thì trong một số trường hợp, chương trình không thể lấy được kiểu thực sự của T. Bởi vì — Kotlin (giống như Java) sẽ "xóa generic" khi chạy (gọi là Type Erasure).

Nghĩa là:

Khi bạn viết code, bạn có thông tin về kiểu, Nhưng khi chương trình chạy, kiểu này bị "xóa".

2. Vấn đề gì xảy ra khi "kiểu bị xóa"?

Ví dụ, bạn muốn viết một hàm phân tích cú pháp JSON chung:

fun <T> parse(json: String): T {
    return Gson().fromJson(json, T::class.java)
}

Dòng này sẽ báo lỗi, vì:

Bộ biên dịch Kotlin nói: "Tôi không biết T là gì, bạn phải cung cấp Class để phân tích."

3. Kotlin cung cấp giải pháp: reified

Khi bạn thêm reified vào hàm nội tuyến (inline function), Kotlin sẽ giúp bạn giữ lại kiểu khi biên dịch.

Ví dụ:

inline fun <reified T> parse(json: String): T {
    return Gson().fromJson(json, T::class.java)
}

Giờ thì có thể sử dụng!

4. Tại sao phải dùng inlinereified cùng nhau?

Bởi vì:

  • inline: Làm cho hàm "mở rộng" khi biên dịch, tức là sao chép trực tiếp nội dung hàm vào nơi gọi;
  • reified: Chỉ có thể dùng trong hàm inline, vì chỉ như vậy, bộ biên dịch mới biết được kiểu thực sự của T.

Vì vậy, reified phải gắn với inline.

5. So sánh ví dụ: Có vs Không có reified
Không có reified:
fun <T> parse(json: String): T {
    return Gson().fromJson(json, T::class.java) // ❌ Báo lỗi, không lấy được kiểu T
}
reified:
inline fun <reified T> parse(json: String): T {
    return Gson().fromJson(json, T::class.java) // ✅ Có thể sử dụng
}
6. Sử dụng trong thực tế (ví dụ như hàm safeParseJson)
private inline fun <reified T> safeParseJson(
    jsonMessage: String,
    onSuccess: (T) -> Unit,
    noinline onError: ((Exception) -> Unit)? = null
)

Ở đây, reified T cho phép bạn viết như sau:

safeParseJson<User>(json) { user ->
    println("Tên người dùng: ${user.name}")
}

Không cần truyền User::class.java thủ công, Kotlin sẽ tự động biết TUser.

7. Tóm tắt ngắn gọn
Đặc tính Tác dụng Ví dụ
inline Hàm được mở rộng khi biên dịch, tăng hiệu suất inline fun foo()
reified Làm cho generic vẫn biết được kiểu khi chạy inline fun <reified t=""> parse()
Cả hai Có thể sử dụng T::classT::class.java khi chạy Phân tích cú pháp JSON, phản chiếu, xác định kiểu, v.v.

Nếu so sánh với cuộc sống, ta có thể nhớ như sau:

Hàm generic bình thường giống như "hộp bưu kiện", nhãn bị xé, bạn không biết bên trong là gì; Thêm reified thì giống như "viết rõ tên bên ngoài hộp", bạn biết bên trong là gì trước khi mở.

Java generics

1. Java generics cũng "xóa kiểu"

Trong Java, tất cả thông tin generic đều bị "xóa" sau khi biên dịch, chỉ giữ lại kiểu gốc.

Ví dụ:

List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();

Cả hai danh sách này khi chạy thực tế cùng một kiểu, chỉ là ArrayList.

JVM không biết các tham số generic String hoặc Integer, vì:

Java generics là "kiểm tra tại thời điểm biên dịch + xóa kiểu khi chạy".

Ví dụ:

public <T> void printType(T item) {
    System.out.println(item.getClass());
}

Gọi:

printType("Hello"); // In ra class java.lang.String
printType(123);     // In ra class java.lang.Integer

Dường như không có vấn đề, nhưng thực tế hàm không biết mình đang xử lý <String> hay <Int>, nó chỉ dựa vào giá trị truyền vào để suy đoán kiểu.

Nếu bạn muốn giữ thông tin generic trong Java, bạn phải làm như sau:

public <T> T fromJson(String json, Class<T> clazz) {
    return gson.fromJson(json, clazz);
}

Phải truyền thêm Class<T>, vì Java không tự giữ lại kiểu.

Kotlin reified có thể lấy kiểu khi chạy

reified trong Kotlin là một kỹ thuật biên dịch + đường dẫn cú pháp.

inline fun <reified T> parse(json: String): T {
    return gson.fromJson(json, T::class.java)
}

Bộ biên dịch Kotlin sẽ ghi trực tiếp kiểu thực sự của T (ví dụ: User::class.java) khi biên dịch.

Vì vậy:

Kotlin với inline + reified có thể lấy kiểu trực tiếp, không cần truyền như Java.

Bảng so sánh

Đặc tính Java generics Kotlin generics thông thường Kotlin inline + reified
Thông tin generic Có tại thời điểm biên dịch, xóa khi chạy Cũng bị xóa Giữ lại kiểu khi biên dịch
Có thể dùng T::class.java? Không Không Có thể
Có thể phân tích cú pháp JSON trực tiếp? Cần truyền Class<t> Cũng cần Không cần, tự nhận diện
Cách biên dịch Biên dịch thông thường Biên dịch thông thường Mở rộng inline, giữ lại thông tin kiểu

Tóm tắt ngắn gọn:

Thông tin generic trong Java bị "mất" khi chạy; reified generic trong Kotlin có thể "nhớ" kiểu của mình khi chạy.

Ví dụ so sánh

Java:

User user = parse(json, User.class);

Kotlin không có reified:

val user = parse(json, User::class.java)

Kotlin có reified:

val user = parse<User>(json)

✅ Đơn giản, trực quan, ít tham số hơn.

Thẻ: Kotlin Java Generics reified inline

Đăng vào ngày 9 tháng 7 lúc 17:47