Vấn đề

Khi lập trình Java, có một vấn đề mà nhiều người gặp phải: Cơ chế truyền tham số vào hàm.

⚠️ Lưu ý quan trọng: Java không có Pass by Reference. Mọi tham số trong Java đều được truyền theo tham trị (Pass by Value) — kể cả object. Sự nhầm lẫn phổ biến là gọi việc truyền object là “Pass by Reference”, nhưng thực chất Java chỉ sao chép địa chỉ bộ nhớ (reference value), không phải truyền reference thật sự.

Truyền Tham Trị với Kiểu Nguyên Thủy (Primitive Types)

Khi bạn truyền một kiểu nguyên thủy (int, double, boolean, v.v.) vào hàm, Java tạo ra một bản sao của giá trị đó. Mọi thay đổi trong hàm chỉ ảnh hưởng đến bản sao, không ảnh hưởng đến biến gốc bên ngoài.

Ví dụ:

public class PassByValueExample {
    public static void main(String[] args) {
        int num = 10;
        modifyPrimitive(num);
        System.out.println("Giá trị sau khi gọi hàm: " + num);
    }

    public static void modifyPrimitive(int x) {
        x = 20;  // Thay đổi giá trị của x trong hàm
    }
}

Kết quả:

Giá trị sau khi gọi hàm: 10

Giải thích: Khi gọi modifyPrimitive(num), Java tạo ra một bản sao của giá trị num (là 10) và truyền vào hàm. Việc thay đổi x = 20 bên trong hàm không ảnh hưởng đến num bên ngoài.

Truyền Tham Trị với Kiểu Đối Tượng (Reference Types)

Khi bạn truyền một object vào hàm, Java sao chép giá trị của reference (địa chỉ bộ nhớ) và truyền vào. Điều này có nghĩa là:

  • Bạn có thể thay đổi nội dung của đối tượng mà reference đó trỏ đến.
  • Bạn không thể thay đổi reference gốc (làm nó trỏ sang đối tượng khác).

Ví dụ:

class Product {
    String name;
    double price;
}

public class PassByValueExample {
    public static void main(String[] args) {
        Product product = new Product();
        product.name = "Laptop";
        product.price = 1000.0;
        modifyProduct(product);
        System.out.println("Tên sản phẩm sau khi gọi hàm: " + product.name);
        System.out.println("Giá sản phẩm sau khi gọi hàm: " + product.price);
    }

    public static void modifyProduct(Product p) {
        p.name = "Smartphone";
        p.price = 500.0;  // Thay đổi thuộc tính của đối tượng
    }
}

Kết quả:

Tên sản phẩm sau khi gọi hàm: Smartphone
Giá sản phẩm sau khi gọi hàm: 500.0

Giải thích: Khi gọi modifyProduct(product), Java sao chép giá trị của reference product (địa chỉ bộ nhớ của đối tượng) và truyền vào hàm. Vì pproduct cùng trỏ đến một đối tượng trong Heap, thay đổi thuộc tính qua p sẽ phản ánh ra ngoài.

Tại sao Java luôn truyền tham trị?

Dù bạn nghĩ Java có thể truyền tham chiếu với đối tượng, nhưng thực chất Java luôn truyền tham trị. Sự khác biệt:

  1. Kiểu Nguyên Thủy (Primitive Types): Java sao chép giá trị của biến và truyền vào hàm. Thay đổi trong hàm không làm thay đổi biến gốc.
  2. Kiểu Đối Tượng (Reference Types): Java sao chép giá trị của reference (địa chỉ bộ nhớ) và truyền vào hàm. Bạn có thể thay đổi nội dung của đối tượng, nhưng không thể thay đổi reference gốc.

Một số ví dụ thú vị

1. Thay đổi reference trong hàm — không có tác dụng ra ngoài

class Product {
    String name;
    double price;
}

public class PassByValueExample {
    public static void main(String[] args) {
        Product product = new Product();
        product.name = "Laptop";
        product.price = 1000.0;
        changeReference(product);
        System.out.println("Tên sản phẩm sau khi gọi hàm: " + product.name);
    }

    public static void changeReference(Product p) {
        p = new Product();  // Tạo đối tượng mới, chỉ thay đổi bản sao của reference
        p.name = "Tablet";
    }
}

Kết quả:

Tên sản phẩm sau khi gọi hàm: Laptop

Giải thích: Trong hàm changeReference, bạn thay đổi bản sao của reference p để trỏ đến đối tượng mới. Tuy nhiên, reference product trong main không thay đổi — nó vẫn trỏ đến đối tượng “Laptop” ban đầu. Đây là bằng chứng rõ nhất cho thấy Java không có Pass by Reference thật sự.

2. Sử dụng từ khóa final với object

class Product {
    String name;

    Product(String name) {
        this.name = name;
    }

    void setName(String name) {
        this.name = name;
    }
}

public class PassByValueExample {
    public static void main(String[] args) {
        final Product product = new Product("Laptop");
        product.setName("Tablet");
        System.out.println("Tên sản phẩm: " + product.name);
    }
}

Kết quả:

Tên sản phẩm: Tablet

Giải thích: Biến product lưu địa chỉ bộ nhớ của đối tượng, không phải nội dung đối tượng. Khi dùng final, Java chỉ bảo vệ địa chỉ đó không bị gán sang đối tượng khác — không bảo vệ nội dung bên trong. Vì vậy, setName() vẫn thay đổi được thuộc tính name.

Tổng kết

  • Java luôn truyền tham trị (Pass by Value): Mọi tham số đều được sao chép khi truyền vào hàm.
  • Primitive types: bản sao của giá trị — thay đổi trong hàm không ảnh hưởng ra ngoài.
  • Reference types: bản sao của địa chỉ bộ nhớ — có thể thay đổi nội dung đối tượng, nhưng không thể thay đổi reference gốc.
  • final với object: chỉ khóa reference (địa chỉ), không khóa nội dung đối tượng.