Lạm Bàn Về Tham Chiếu/Tham Trị Trong Java

Bài viết được sự cho phép của tác giả Nhựt Danh

Bài viết hôm nay chúng ta sẽ cùng tìm hiểu và làm rõ ra thế nào là truyền tham số kiểu Tham chiếu, hay Tham trị, vào phương thức trong Java. Có thực sự như lời đồn rằng Java chỉ hỗ trợ việc truyền tham số kiểu Tham trị hay không?

Thực ra cũng bởi có nhiều bạn liên hệ mong muốn mình làm rõ vấn đề này của Java, nên nhân đây mình viết ra luôn. Vâng, mình cũng nhận thấy rằng kiến thức này khá là quan trọng, nó không những nói rõ hơn về việc truyền tham số kiểu Tham chiếu và Tham trị là gì, mà nó còn giúp chúng ta mở rộng hơn trong việc nhìn nhận một biến được tổ chức và quản lý trong Java như thế nào. Và mình cũng muốn nói thêm một ý rằng, cái chủ đề về Tham chiếu/Tham trị này là một chủ đề thắc mắc của không riêng bạn đâu, nó như trở thành một cuộc tranh luận sôi nổi trên các diễn đàn, một lát sau vào bài viết bạn sẽ thấy tại sao nó lại gây nên sự tranh luận này, nếu bạn chưa tin lắm, hãy thử Google với từ khóa “Is Java pass-by-reference or pass-by-value” sẽ thấy.

Bắt Đầu Từ Việc Khai Báo Biến

Vâng đúng rồi, bạn nhớ đúng, biến đã được nói đến ở Bài 4. Ở đây chúng ta sẽ không nói lại biến là gì và khai báo ra sao. Mà sẽ phân tích sâu hơn về việc quản lý biến trong Java, lát nữa vấn đề về Tham chiếu và Tham trị sẽ được sáng tỏ hơn nhờ việc phân tích này.

Giả xử chúng ta khai báo một biến x như sau trong Java.

int x = 10;

Khi dòng code trên được thực thi, dĩ nhiên Giá trị 10 sẽ được chứa trong bộ nhớ. Tuy nhiên, có một giá trị khác cũng được hệ thống tạo ra, đó chính là Địa chỉ chỉ đến nơi chứa giá trị 10 này. Bạn cứ tưởng tượng bạn đi nhà sách, bạn cần phải để túi sách bên ngoài trước khi vào nhà sách đó, bạn sẽ đi đến các tủ chứa dành cho khách, cất túi xách vào một ngăn tủ, đóng cửa tủ lại. Bạn thấy rằng cửa tủ có ghi một con số, và khi bạn khóa cửa lại, trên chìa khóa cũng có một số tương ứng. Vậy thì túi xách của bạn tương tự như giá trị 10 mà bộ nhớ (hay cái tủ) dùng để chứa, còn con số trên chìa khóa tủ chính là địa chỉ của cái tủ, tương tự địa chỉ dẫn đến giá trị 10 của bộ nhớ. Địa chỉ trên chìa khóa giúp bạn không bị nhầm lẫn giữa tủ đồ của bạn và của người khác, và địa chỉ của hệ thống cũng giúp biến x tìm đến nơi chứa giá trị của nó trong bộ nhớ.

Việc khai báo và kịch bản lưu trữ trên đây được mình mô tả bằng sơ đồ trực quan, dễ hiểu như sau.

Sơ đồ diễn tả việc khai báo giá trị cho biến Sơ đồ diễn tả việc khai báo giá trị cho biến

Sơ đồ cho thấy khi dòng code khai báo trên được thực thi, giá trị 10 chứa trong bộ nhớ màu cam, và địa chỉ đến giá trị này (khung màu xanh dương) được tạo ra và lưu trữ như thế nào.

  Hướng dẫn tạo Gradle Project Java bằng dòng lệnh CMD

  Tổng Hợp Các Phương Thức Của Thread

Đến Truyền Biến Vào Trong Phương Thức

Thế Nào Là Truyền Kiểu Tham Trị

Bây giờ mới bắt đầu nội dung chính của bài hôm nay. Trước hết chúng ta hãy nhìn vào code sau.

public static void main(String[] args) {
    int x = 10;

    System.out.println("Before call process: " + x);
    process(x);
    System.out.println("After call process: " + x);
}

public static void process(int x) {
    x = 7;
}

Ở phương thức main(), chúng ta cũng bắt đầu với việc khai báo x = 10. Sau đó chúng ta truyền biến x này dưới dạng tham số vào cho phương thức process(). Thế nhưng vào bên trong process(), tham số lúc này cũng có tên là x (hay thực ra đặt cái tên khác cũng được, mình cố tình đặt chung để dễ thấy sự liên quan với nhau hơn), và chúng ta còn gán lại giá trị mới cho x nữa, là 7. Vậy sau khi process() kết thúc, câu lệnh in x ra console “After call process: “ sẽ in ra số 10 hay số 7 vậy các bạn?

Là số… 10. Hi vọng các bạn đều thuộc team nói đúng.

Before call process: 10
After call process: 10

Trên đây là ví dụ rõ ràng nhất cho khái niệm truyền kiểu Tham trị vào phương thức. Vậy truyền theo kiểu Tham trị là gì. Đó là việc truyền tham số trong trường hợp trên đây, nó chỉ là sự sao chép giá trị khi tham số được truyền vào trong một phương thức. x trước khi truyền vào trong phương thức process() mang giá trị 10. Còn x bên trong process() không phải x ở ngoài, nó chỉ là một biến khác được sao chép ra, cũng mang tên x, cũng mang giá trị 10, nhưng không còn là x ở ngoài nữa. Do đó dù bên trong process() chúng ta có thay đổi x như thế nào thì cũng không ảnh hưởng với x bên ngoài.

Có thể vẽ lại sơ đồ trực quan như sau.

Sơ đồ diễn tả việc truyền tham số kiểu tham trịSơ đồ diễn tả việc truyền tham số kiểu tham trị

Sơ đồ cho thấy x ở trên cùng là x trước khi đưa vào trong process() mang giá trị 10, khi đưa vào trong process()x này sẽ là một x khác hoàn toàn, khác nơi chứa giá trị 10 và hiển nhiên sẽ khác luôn địa chỉ (địa chỉ của chúng khác nhau được minh họa bằng hai màu khác nhau). Việc thay đổi từ giá trị 10 sang 7 bên trong process() chỉ làm thay đổi x bên trong đó mà thôi, hoàn toàn không ảnh hưởng gì với x bên ngoài cả.

Xem thêm vị trí tuyển dụng Java lương cao hấp dẫn tại TopDev

Thế Nào Là Truyền Kiểu Tham Chiếu

Trước khi đi vào giải nghĩa, mình xin nhắc lại rằng, Java chỉ hỗ trợ chuyền tham số kiểu Tham trị mà thôi.

Nói là không hỗ trợ vì chẳng có một cách nào để kêu Java giúp chúng ta truyền kiểu Tham chiếu cả. Tuy nhiên một số ngôn ngữ khác lại “công khai” hỗ trợ điều này, như mình biết đó là C++. Mình cũng còn nhớ một ít C++ nên sẽ minh họa việc truyền theo Tham chiếu như thế nào theo code như sau (bạn nào biết code C++ mà nhận thấy mình viết sai thì giúp mình sửa nhé, lâu quá không dùng đến mình cũng quên nhiều).

#include <iostream>

int main() {
    int x = 10;

    cout << "Before call process: " << x << endl;
    process(someValue);
    cout << "After call process: " << x << endl;

    return 0;
}

void process(int& x) {
    x = 7;
}

Bạn thấy code C++ cũng dễ hiểu đúng không nào. Cũng có phương thức main(), cũng in ra console trước và sau khi gọi phương thức process(). Nhưng khi khai báo tham số truyền vào cho process(), C++ cho phép khai báo kiểu int& hoặc int. Với khai báo tham số là kiểu int thì kết quả sẽ truyền tham số kiểu Tham trị như code Java trên kia, còn khai báo int& thì tham số sẽ truyền theo kiểu Tham chiếu. Và bạn biết không, với code C++ này thì sau khi thực thi, kết quả in ra ở dòng cuối cùng sẽ cho thấy x bị thay đổi sang giá trị 7.

Before call process: 10
After call process: 7

Vậy bạn đã phần nào phân biệt giữa truyền tham số kiểu Tham trị và Tham chiếu chưa. Truyền tham số theo kiểu Tham chiếu như ví dụ ngay trên đây, đó là việc truyền tham số dựa trên việc truyền địa chỉ của biến (không sao chép giá trị như code Java trên kia). x trước khi truyền vào trong phương thức process() mang giá trị 10, khi truyền vào trong process() chính là x bên ngoài, do đó khi chúng ta thay đổi giá trị x bên trong process()x bên ngoài cũng thay đổi theo.

Chúng ta mô hình hóa một chút nào.

Sơ đồ diễn tả việc truyền tham số kiểu tham chiếuSơ đồ diễn tả việc truyền tham số kiểu tham chiếu

Sơ đồ cho thấy x ở trên cùng mang giá trị 10, khi đưa vào trong process(), địa chỉ của x sẽ truyền vào, và như vậy chúng cùng chỉ đến chung một bộ nhớ. Do đó, việc thay đổi x từ giá trị 10 sang 7 bên trong process() cũng làm cho x bên ngoài thay đổi từ 10 sang 7 theo.

Chốt lại là bạn đã phân biệt giữa truyền tham số dạng Tham trị và truyền tham số dạng Tham chiếu chưa nào. Ơ nhưng bài viết chưa kết thúc? Phần tranh cãi trên mạng chính là phần sau này đây, mời bạn xem tiếp.

Vậy Nếu Tham Số Là Một Đối Tượng Thì Sao

Vâng, trên đây là các ví dụ liên quan đến truyền tham số là một giá trị nguyên thủy vào trong phương thức. Bạn thấy rõ rằng Java là một ngôn ngữ chỉ cho phép truyền tham số kiểu Tham trị. Tức là giá trị bên trong phương thức truyền vào chỉ là một bản sao của giá trị bên ngoài, việc thay đổi giá trị này bên trong phương thức Java không gây ảnh hưởng hay thay đổi giá trị của biến bên ngoài phương thức.

OK. Vậy mời bạn xem ví dụ sau. Trước hết chúng ta cần khai báo một lớp cái đã, mình giả sử có lớp MyCat, lớp bạn mèo này có một thuộc tính name để dễ phân biệt.

public class MyCat {
    private String name;
    
    public MyCat(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

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

Trường Hợp 1

Ở trường hợp thử nghiệm này, chúng ta xem hai anh chàng main() và process() khi này sử dụng MyCat như thế nào.

public static void main(String[] args) {
    MyCat myCat = new MyCat("Kitty");

    System.out.println("Before call process: " + myCat.getName());
    process(myCat);
    System.out.println("After call process: " + myCat.getName());
}

public static void process(MyCat myCat) {
    myCat.setName("Doraemon");
}

Bạn có đoán được hai dòng in ra màn hình sẽ in tên của chú mèo nhà ta như thế nào không. Có phải cùng là “Kitty” không. Bạn có lý khi nghĩ là cùng in ra “Kitty”, vì như đã nói, Java chỉ hỗ trợ truyền tham số kiểu Tham trịmyCat bên trong process() không phải myCat bên ngoài. Vâng, mời bạn xem kết quả.

Before call process: Kitty
After call process: Doraemon

Ơ sao kỳ vậy. myCat đã thay đổi name bên trong process() được ư. Vậy sao gọi là Tham trị? Vâng, thắc mắc trên các diễn đàn hỏi đáp là đây. Khái niệm Tham trị của Java bị lung lay ư? Thực chất thì đến giờ phút này, mình vẫn khẳng định rằng Java vẫn chỉ hỗ trợ kiểu tham số là Tham trị thôi nhé.

Trường Hợp 2

Chúng ta cùng sửa lại code trên một chút để thử nghiệm một trường hợp khác xem sao.

public static void main(String[] args) {
        MyCat myCat = new MyCat("Kitty");

        System.out.println("Before call process: " + myCat.getName());
        process(myCat);
        System.out.println("After call process: " + myCat.getName());
    }

    public static void process(MyCat myCat) {
        myCat = new MyCat("Doraemon");
    }

Kết quả in ra console ngay đây.

Before call process: Kitty
After call process: Kitty

Uhm trường hợp này hơi rõ ràng rồi đấy. Với code ở trường hợp 2 trên đây đủ thấy rằng, myCat bên ngoài được khởi tạo với cái tên “Kitty”, sau khi truyền vào process(), dù cho có được khởi tạo lại với cái tên “Doraemon” nhưng thực chất chúng không phải là một, nên myCat bên trong không làm ảnh hưởng đến myCat bên ngoài. Vậy trường hợp này có thể khẳng định, đây đúng là truyền tham số kiểu Tham trị.

Dù vậy thì trường hợp 1 là gì, tại sao truyền tham số kiểu Tham trị mà có thể làm thay đổi giá trị của đối tượng truyền vào được.

Giải Thích Các Trường Hợp

Trước khi đi vào giải thích từng trường hợp. Chúng ta hãy quay lại việc khai báo biến kiểu đối tượng. Hãy nhìn lại dòng khai báo sau.

MyCat myCat = new MyCat(“Kitty”);

Nếu dùng sơ đồ trên để miêu tả. Thì chúng ta sẽ có một nơi trong bộ nhớ lưu trữ Giá trị của biến myCat đúng không nào. Và một Địa chỉ chỉ đến Giá trị này của biến. Ồ nhưng trong MyCat có chứa thuộc tính name, vậy lại phải có một Địa chỉ chỉ đến Giá trị của name nữa. Lòng vòng như vậy cho chúng ta thấy thực ra việc cấp phát và lưu trữ một đối tượng nó sẽ hơi khác với biến nguyên thủy một chút. Bạn xem.

Sơ đồ diễn tả việc khai báo đối tượng myCatSơ đồ diễn tả việc khai báo đối tượng myCat

Ô màu xanh dương chính là Địa chỉ của biến myCat, chỉ đến Giá trị của nó là ô màu đỏ. Ô màu đỏ sẽ lại chứa các Địa chỉ chỉ đến các thuộc tính của MyCat, trong trường hợp này chỉ có mỗi thuộc tính name.

Như vậy với trường hợp 1 trên đây. myCat được truyền vào trong process(), thực chất đúng là truyền kiểu Tham trị. Có nghĩa là giá trị của biến myCat được sao chép qua. Nhưng trớ trêu thay, giá trị của một đối tượng lúc bây giờ chính là địa chỉ đến các thành phần của đối tượng đó. Do đó việc sao chép địa chỉ lúc này cũng như sao chép chìa khóa mà thôi, nó vẫn dùng để mở cùng một tủ. Bạn xem minh họa cho trường hợp 1 như sau.

Sơ đồ diễn tả việc truyền tham số kiểu tham trị cho đối tượng myCatSơ đồ diễn tả việc truyền tham số kiểu tham trị cho đối tượng myCat

Cũng giống như sơ đồ truyền biến nguyên thủy trên kia. Giá trị của myCat khi này lại là cục màu đỏ, mà như chúng ta đã biết, việc sao chép cục màu đỏ cũng chính là sao chép lại địa chỉ tham chiếu đến thuộc tính name. Tuy myCat bên ngoài và bên trong khác nhau về địa chỉ, nhưng lại cùng giá trị chính là địa chỉ đến name. Chính vì vậy mà việc thay đổi name đối với myCat trong trường hợp 1 lại làm thay đổi cả name của myCat trước khi truyền vào process(). Nhưng như bạn thấy, Java khi này vẫn tuân thủ là truyền tham số kiểu Tham trị đấy nhé.

Trường hợp 2. Khi myCat truyền vào trong process(), việc sao chép giá trị (chính là địa chỉa của name) vào trong phương thức vẫn diễn ra. Nhưng vào đây, myCat được khởi tạo lại thành một đối tượng mới thông qua toán tử new, thế là giá trị mới cũng thay đổi theo (màu đỏ thay bằng màu tím), nên việc đặt tên cho myCat bên trong process() như thế nào trong trường hợp 2 này cũng không ảnh hưởng đến bên ngoài nhé.

Sơ đồ diễn tả việc truyền tham số kiểu tham trị cho đối tượng myCatSơ đồ diễn tả việc truyền tham số kiểu tham trị cho đối tượng myCat

Kết Luận

Trên đây là tất cả thông tin liên quan đến việc phân biệt cho khái niệm truyền tham số kiểu Tham chiếu hay Tham trị, và khẳng định rằng Java chỉ hỗ trợ việc truyền tham chiếu kiểu Tham trị mà thôi, mặc dù với việc truyền dữ liệu kiểu đối tượng có phần gây tranh cãi với nhiều người. Qua bài viết này bạn đã hiểu rõ về Java hơn rồi đúng không.

Bài viết gốc được đăng tải tại yellowcodebooks.com

Có thể bạn quan tâm:

Tìm việc làm IT mọi cấp độ tại TopDev