Thao tác với biến private final trong Java thông qua Reflection

Việc sử dụng Reflection trong Java cho phép thay đổi hành vi đối tượng tại thời điểm chạy, kể cả các trường được khai báo là private final. Tuy nhiên, không phải lúc nào cũng có thể thay đổi giá trị của một trường như vậy. Bài viết này sẽ trình bày một số trường hợp cụ thể để minh họa.

  • Trường hợp với kiểu dữ liệu nguyên thủy
  • Trường hợp với String
  • Trường hợp với Integer
  • Kết luận

Xét ví dụ đầu tiên với kiểu dữ liệu nguyên thủy:

/**
 * @author Cool-Coding 2018/5/15
 */
public class FieldModifierExample {
    private final int years = 25;

    public int getYears() {
        return years;
    }
}

Khi thực hiện kiểm thử bằng Reflection:

import java.lang.reflect.Field;

/**
 * @author Cool-Coding 2018/5/15
 */
public class ReflectionDemo {
    public static void main(String[] args) {
        try {
            Class<?> clazz = Class.forName("practise.practise.FieldModifierExample");
            FieldModifierExample instance = (FieldModifierExample) clazz.newInstance();
            Field field = clazz.getDeclaredField("years");
            field.setAccessible(true);
            field.set(instance, 60);
            field.setAccessible(false);
            System.out.println(instance.getYears());
        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }
}

Kết quả in ra: 25 — tức là giá trị không thay đổi.

Nếu chuyển việc khởi tạo giá trị vào constructor thì sao?

public class FieldModifierExample {
    private final int years;

    public FieldModifierExample() {
        this.years = 25;
    }

    public int getYears() {
        return years;
    }
}

Chạy lại đoạn code kiểm thử → kết quả là 60.

Tại sao lại có sự khác biệt này? Hãy xem xét mã byte được sinh ra sau khi biên dịch.

Trường hợp khởi tạo trực tiếp:

public class FieldModifierExample {
    private final int years = 25;

    public FieldModifierExample() {}

    public int getYears() {
        return 25; // Trực tiếp trả về hằng số
    }
}

Trường hợp khởi tạo trong constructor:

public class FieldModifierExample {
    private final int years = 25;

    public FieldModifierExample() {}

    public int getYears() {
        return this.years; // Trả về biến instance
    }
}

Nhận thấy rằng khi khởi tạo trực tiếp, trình biên dịch đã thay thế mọi nơi xuất hiện years bằng hằng số 25, do đó dù dùng Reflection thay đổi giá trị trường, phương thức getYears() vẫn trả về 25 vì nó đã được "cố định" ở mức mã máy.


Tiếp theo, kiểm tra với kiểu String:

public class FieldModifierExample {
    private final String message = "Hello World";

    public String getMessage() {
        return message;
    }
}

Thử thay đổi qua Reflection:

import java.lang.reflect.Field;

public class ReflectionDemo {
    public static void main(String[] args) {
        try {
            Class<?> clazz = Class.forName("practise.practise.FieldModifierExample");
            FieldModifierExample obj = (FieldModifierExample) clazz.newInstance();
            Field field = clazz.getDeclaredField("message");
            field.setAccessible(true);
            field.set(obj, "Updated Message");
            field.setAccessible(false);
            System.out.println(obj.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Kết quả: Hello World — không thay đổi.

Tuy nhiên, nếu khởi tạo trong constructor:

public class FieldModifierExample {
    private final String message;

    public FieldModifierExample() {
        this.message = "Hello World";
    }

    public String getMessage() {
        return message;
    }
}

Chạy lại kiểm thử → kết quả: Updated Message.

So sánh mã bytecode:

Khởi tạo trực tiếp:

public String getMessage() {
    return "Hello World"; // Đã được thay thế thành hằng
}

Khởi tạo trong constructor:

public String getMessage() {
    return this.message; // Truy cập biến instance
}

→ Do trình biên dịch không thay thế các tham chiếu đến trường nếu nó nằm trong constructor.


Xét trường hợp với Integer:

public class FieldModifierExample {
    private final Integer count = 42;

    public Integer getCount() {
        return count;
    }
}

Thử thay đổi qua Reflection:

Field field = clazz.getDeclaredField("count");
field.setAccessible(true);
field.set(obj, 99);
field.setAccessible(false);
System.out.println(obj.getCount()); // In ra: 99

Kết quả: 99 — giá trị bị thay đổi.

Vì sao? Xem mã bytecode:

public Integer getCount() {
    return this.count; // Không thay thế thành hằng số
}

Lý do: Khi khởi tạo Integer count = 42, trình biên dịch thực hiện autoboxing, tạo ra đối tượng Integer.valueOf(42) — một đối tượng thực tế chứ không phải hằng số nguyên thủy. Vì vậy, giá trị vẫn được lưu trong trường và có thể thay đổi bằng Reflection.


Tương tự, nếu khởi tạo String bằng String.valueOf(...), điều gì xảy ra?

private final String text = String.valueOf("Dynamic Text");

public String getText() {
    return this.text;
}

Thử thay đổi → kết quả là Dynamic Text (vẫn có thể thay đổi).

Mã bytecode:

public String getText() {
    return this.text; // Không thay thế
}

→ Dù dùng String.valueOf, trình biên dịch không tối ưu hóa thành hằng số vì đây là lời gọi phương thức.


Cuối cùng, thử khởi tạo Integer trong constructor:

private final Integer value;

public FieldModifierExample() {
    this.value = 77;
}

public Integer getValue() {
    return this.value;
}

Kết quả thay đổi: 77 — giá trị vẫn bị thay đổi.

Mã bytecode tương tự như khởi tạo trực tiếp — vẫn giữ this.value.


Tổng kết:

  • Với các trường private final kiểu nguyên thủy hoặc String được khởi tạo trực tiếp bằng giá trị hằng, trình biên dịch sẽ thay thế mọi tham chiếu đến chúng bằng chính giá trị hằng. Do đó, không thể thay đổi bằng Reflection.
  • Nếu khởi tạo trong constructor hoặc dùng phương thức như String.valueOf() hay Integer.valueOf(), giá trị được lưu dưới dạng tham chiếu đến đối tượng, nên có thể thay đổi bằng Reflection.
  • Điều này có nghĩa rằng, nếu bạn muốn đảm bảo một hằng số không thể bị thay đổi, hãy khai báo nó như private final int hoặc private final String và khởi tạo ngay tại chỗ — điều này giúp trình biên dịch tối ưu hóa và làm cho giá trị trở thành hằng cố định.

Thẻ: Java Reflection final keyword bytecode optimization Autoboxing

Đăng vào ngày 22 tháng 9 lúc 04:19