Cơ chế Pass-By-value và Pass-By-Reference trong java

Có hai kiểu truyền tham số phổ biến là pass-by-valuepass-by-reference. Mỗi ngôn ngữ sẽ vận dụng chúng theo mỗi cách khác, ví dụ trong C/C++ sẽ cho phép bạn tuỳ biến theo ý muốn etc. Trong java luôn luôn là pass-by-value.

Lúc mới chuyển từ C/C++ sang Java có ai đã cố gắng viết hàm swap() chưa?

void swap(int a, int b) {
   int t = a;
   a = b;
   b = t;
}

Mình đã từng và chạy mãi không biết tại sao 2 giá trị vẫn không hoán đổi cho nhau, lạ nhỉ? Trong bài viết này chúng ta sẽ tìm hiểu xem tại sao lại như vậy nhé.

Pass-by-value và Pass-by-reference

Khi các tham số đầu vào của một method là

  • Pass-by-value: Method được gọi sẽ sao chép một bản sao của tham số truyền vào và hoạt động trên chúng. Mọi thay đổi trên bản sao này không ảnh hưởng đến giá trị ban đầu. 
  • Pass-by-reference: Method được gọi và nơi gọi chúng sẽ thao tác trên cùng một object. Điều này có nghĩa là khi method thay đổi giá trị của tham số thì giá trị của chúng tại nơi gọi cũng thay đổi theo.

Truyền tham số trong java

Trong java kiểu dữ liệu nguyên thuỷ(primitives) được lưu trữ với giá trị thật của chúng trong khi các kiểu dữ liệu non-primitives lưu trữ dưới dạng biến tham chiếu đến object thật được lưu trữ trong heap memory. Cả 2 loại (giá trị của kiểu dữ liệu nguyên thuỷ và tham chiếu) này đều được lưu trữ trong Stack memory.

Các tham số trong java luôn được truyền dưới dạng pass-by-value. Trong quá trình gọi hàm, một bản sao các tham số đầu vào sẽ được tạo ra và lưu vào stack memory cho dù chúng là giá trị primitives hay tham chiếu(reference).

Tham số primitive

Trong Java chúng ta có tổng 8 kiểu dữ liệu nguyên thuỷ như (int, long, double, float, character etc). Các biến thuộc kiểu dữ liệu nguyên thuỷ này được lưu trực tiếp trong stack memory. Khi một biến primitives được sử dụng làm tham số đầu vào thì một bản sao chép của chúng sẽ được tạo ra và lưu vào stack.

Các bản sao chép của chúng chỉ tồn tại trong quá trình method đó được thực thi, sau khi method thực thi hoàn thành chúng sẽ bị xoá ra khởi stack.

Ví dụ chúng ta có đoạn code sau

public class Main {
    public static void modify(int x, int y) {
        x = 100;
        y = 200;
        System.out.println("X - Y from modidy: " + x + " - " + y);
    }
    public static void main(String[] args) {
        int x = 1;
        int y = 2;

        System.out.println("X - Y before modidy: " + x + " - " + y);
        
        modify(x, y);

        System.out.println("X - Y after modidy: " + x + " - " + y);
    }

}

Output:

X – Y before modidy: 1 – 2
X – Y from modidy: 100 – 200
X – Y after modidy: 1 – 2

Để hiểu rõ hơn tại sao lại có kết quả trên chúng ta sẽ đi từng bước mà chúng thực thi:

  1. Biến x, y có kiểu dữ liệu nguyên thuỷ nên trong hàm main() chúng sẽ được lưu trực tiếp giá trị vào stack memory.
  2. Khi gọi method modify() một bản sao chép của x,y sẽ được tạo ra vào lưu vào stack memory và truyền vào modify() method.
  3. Tất cả những thay đổi trên tham số truyền vào x,y trong hàm modify() đều là trên bản copy vì vậy giá trị của x,y tại hàm main() vẫn giữ nguyên.

pass-by-value-pass-by-reference

 

Tham số reference object

Trong Java tất cả các object sau khi khởi tạo đều được lưu trữ trong vùng nhớ heap. Những object này sẽ được tham chiếu bởi các biến reference khác nhau. Hay nói cách khác các biến này sẽ lưu trữ địa chỉ của các object mà chúng tham chiếu. Các biến reference này được lưu trữ trong trong vùng nhớ Stack.

Khi một biến reference (địa chỉ của object) được truyền vào tham số của method thì một bản sao chép của chúng được tạo ra và lưu vào Stack. Chúng có cùng địa chỉ đến object được lưu trong heap. Kết quả là khi bạn thao tác trên biến reference thì sẽ ảnh hưởng đến object được lưu trong heap.

Tuy nhiên nếu bạn khởi tạo một object mới và gán lại cho biến reference thì sẽ không ảnh hưởng đến object ban đầu.

Ví dụ cho đoạn code sau

public class Main {
    public static void modify(Foo x, Foo y) {
        x.num += 1;
        y = new Foo(1);
        y.num++;
    }

    public static void main(String[] args) {
        Foo x = new Foo(1);
        Foo y = new Foo(1);

        modify(x, y);

        System.out.println("X: " + x.num); // 2
        System.out.println("Y: " + y.num); // 1

    }


}

class Foo {
    public int num;

    public Foo(int num) {
        this.num = num;
    }
}

Để hiểu rõ hơn thì chúng ta tiến hành phân tích từng bước.

  1. Sau khi khởi tại 2 object Foo x,y thì chúng được lưu vào Heap, và 2 biến x,y lưu trữ địa chỉ của objec tương ứng.create object in java
  2. Khi x,y được truyền vào method modify() thì một bản sao của x,y được tạo ra và lưu vào Stack, nhưng chúng vẫn có cùng địa chỉ tham chiếu đến object được lưu trong Heap.copy reference variable
  3. Sau khi modify được gọi y.num+=1 sẽ thao tác trực tiếp object được lưu trong heap và thay đổi giá trị. Trong khi đó y lại được tham chiếu một object tạo mới trong modify() nên các thao trên y sẽ không ảnh hưởng đến giá trị ban đầu.reflect on reference

Như vậy trong java cơ thế truyền tham số chỉ là pass-by-value. Thế nhưng việc các các biến reference được sao chép ra theo cơ chế pass-by-value lại có cùng địa chỉ đến object trong vùng nhớ heap làm cho các thao tác trên biến reference sẽ ảnh hưởng trực tiếp đến object ban đầu. Đây cũng là một cơ chế giúp chúng ta triển khai pass-by-reference cách không chính thức khi chúng ta cần dùng đến.

Nguồn tham khảo

https://www.baeldung.com/java-pass-by-value-or-pass-by-reference

3.2 5 votes
Article Rating
Subscribe
Notify of
guest
1 Comment
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
1
0
Would love your thoughts, please comment.x
()
x