Getter và setter được sử dụng rất phổ biến trong Java, 2 phương thức thoạt nhìn có vẻ đơn giản, tuy nhiên bạn có chắc mình đã hiểu và triển khai 2 phương thức này đúng cách chưa?

Trong bài viết này, chúng ta sẽ cùng tìm hiểu qua về 2 phương thức getter và setter, từ cơ bản cho tới những lỗi thường gặp và cách triển khai tốt nhất nhé

Getter và setter là gì?

Getter và setter là 2 phương thức thường dùng để truy xuất/cập nhật giá trị của một biến, ngoài ra, getter và setter còn được gọi là phương thức accessormutator trong Java. Dưới đây là một ví dụ đơn giản về getter, setter:

public class Demo {
    private int number;

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }
}

 

Tham khảo Lộ trình Java Spring Boot Full Stack 7 tháng của Techmaster. 

Link môn học Java core 16 buổi

 

Ở ví dụ trên, chúng ta có một biến privatenumber, bởi vì numberprivate, mã bên ngoài class không thể truy cập trực tiếp tới nó

public class App {
    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();

        System.out.println(demo.number); // compile error
    }
}

 

Để truy cập được tới number, mã bên ngoài sẽ phải gọi hàm getNumber()setNumber() để đọc/cập nhật giá trị. Ví dụ:

public class App {
    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();

        demo.setNumber(10); // update number
        System.out.println(demo.getNumber()); // get number
    }
}

 

Tại sao lại cần getter và setter?

Bạn hoàn toàn có thể không dùng getter và setter, tuy nhiên khi đó, mọi thứ sẽ trở nên khá phiền phức. Một biến có thể được truy cập ở bất kỳ đâu trong mã, và bạn hoàn toàn không thể kiểm soát được cách mã bên ngoài truy xuất và cập nhật giá trị cho biến (tất nhiên bạn vẫn kiểm soát được nếu bạn tự viết toàn bộ mã và tự dùng :D).

Đặc biệt hơn với những thông tin quan trọng, ví dụ số tiền trong tài khoản chẳng hạn, bạn sẽ cần phải kiểm soát cách một người có thể truy cập vào nó, và cách họ rút tiền khỏi tài khoản (bạn sẽ không muốn bất kỳ ai cũng có thể truy cập tài khoản ngân hàng của mình và rút số tiền thậm chí là lớn hơn số tiền bạn có đúng không?)

Với getter và setter, bạn có thể kiểm soát cách mã bên ngoài truy cập tới một dữ liệu quan trọng, và cách cập nhật giá trị cho dữ liệu đó. Cùng xem ví dụ dưới dây về cập nhật số tiền trong tài khoản:

public class Demo {
    private long amount;

    public long getAmount() {
        return amount;
    }

    public void setAmount(long amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("Amount can not be less than 0");
        }

        this.amount = amount;
    }
}

 

Hàm setAmount() đảm bảo giá trị của amount luôn phải lớn hơn hoặc 0, và mã bên ngoài muốn truy cập tới amount bắt buộc phải thông qua getAmount(), setAmount():

 

Trong lập trình hướng đối tượng (OOP), tính đóng gói (encapsulation) là một trong những nguyên tắc cơ bản nhất. Việc ẩn một biến khỏi mã bên ngoài và triển khai getter, setter giúp đảm bảo tính đóng gói trong Java.

Quy ước đặt tên cho phương thức getter, setter

Tên phương thức getter và setter nên tuân theo quy ước đặt tên Java bean có dạng getX(), setX() với X là tên biến. Nếu một biến có kiểu boolean, thì có getter có thể sử đặt tên dạng isX() thể hiện rõ hơn tính chất của biến. Ví dụ:

public class Demo {
    private int number;
    private String name;
    private boolean valid;

    public String getName() {
        return name;
    }

    public int getNumber() {
        return number;
    }

    public boolean isValid() {
        return valid;
    }

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

    public void setNumber(int number) {
        this.number = number;
    }
    public void setValid(boolean valid) {
        this.valid = valid;
    }
}

 

Một số sai lầm thường gặp khi sử dụng getter, setter

1. Biến được khai báo với public modifier

Bạn sử dụng getter và setter, tuy nhiên biến lại được khai báo với access modifier là public, khi đó, getter và setter trở nên vô nghĩa, bởi biến có thể dễ dàng được truy xuất trực tiếp từ bên ngoài

public class Demo {
    public long amount;

    public long getAmount() {
        return amount;
    }

    public void setAmount(long amount) {
        this.amount = amount;
    }
}

 

Đoạn code trên là một ví dụ, khi đó, amount có thể dễ dàng truy cập trực tiếp từ bên ngoài, không bắt buộc phải thông qua getAmount()setAmount()

public class App {
    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();

        demo.amount = -1000000; // okey
    }
}

 

=> Sử dụng private hoặc protected access modifier cho biến

2. Gán đối tượng (tham chiếu) trong setter

Cùng xem một ví dụ trước nhé, chúng ta khai báo một class Demo như sau:

public class Demo {
    private int[] arr;

    public void setArr(int[] arr) {
        this.arr = arr;
    }

    public int[] getArr() {
        return arr;
    }
}

 

Và class App như sau:

public class App {
    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();

        int[] arr = new int[] { 1, 2, 3 };

        demo.setArr(arr);
        arr[0] = 10;

        System.out.println(Arrays.toString(demo.getArr()));
        // [10, 2, 3]
    }
}

 

Vấn đề ở đây là gì? Chú ý hàm setArr() trong class Demo, nó gán trực tiếp tham chiếu của mảng truyền vào cho biến, khi đó, cả biến trong Demo và App đều tham chiếu tới cùng một mảng.

Việc thay đổi giá trị mảng từ code bên ngoài cũng ảnh hưởng tới giá trị trong Demo, điều này vi phạm tính đóng gói trong lập trình hướng đối tượng. Giải pháp cho vấn đề này, đó là sao chép giá trị từng phần tử giữa 2 mảng. Ví dụ:

public class Demo {
    private int[] arr;

    public void setArr(int[] arr) {
        this.arr = new int[arr.length];
        System.arraycopy(src, 0, this.arr, 0, src.length);
    }

    public int[] getArr() {
        return arr;
    }
}

 

Cùng xem lại mã phía trên sẽ thay đổi như nào nhé:

public class App {
    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();

        int[] arr = new int[] { 1, 2, 3 };

        demo.setArr(arr);
        arr[0] = 10;

        System.out.println(Arrays.toString(demo.getArr()));
        // [1, 2, 3]
    }
}

 

3. Trả về tham chiếu đối tượng trong hàm getter

Ngược lại với setArr(), cũng với ví dụ mảng bên trên, hãy chú ý tới phương thức getArr(), và cùng xem đoạn mã sau sẽ hoạt động thế nào:

public class App {
    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();

        demo.setArr(new int[] { 1, 2, 3 });

        int[] arr = demo.getArr();
        arr[0] = 10;
        System.out.println(Arrays.toString(demo.getArr()));
        // [10, 2, 3]
    }
}

 

Việc trả về một tham chiếu tới đối tượng trong phương thức getArr() cũng khiến bạn gặp vấn đề tương tự như với phương thức setArr(), nó cho phép mã bên ngoài có thể thay đổi giá trị trong Demo. Giải pháp cho vấn đề tương tự với setArr(), hãy trả về một mảng copy:

public class Demo {
    private int[] arr;

    public void setArr(int[] src) {
        this.arr = new int[src.length];
        System.arraycopy(src, 0, this.arr, 0, src.length);
    }

    public int[] getArr() {
        int[] temp = new int[this.arr.length];
        System.arraycopy(this.arr, 0, temp, 0, this.arr.length);
        return temp;
    }
}

 

Cùng thử lại nhé

public class App {
    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();

        demo.setArr(new int[] { 1, 2, 3 });

        int[] arr = demo.getArr();
        arr[0] = 10;
        System.out.println(Arrays.toString(demo.getArr()));
        // [1, 2, 3]
    }
}

 

Vấn đề đã được giải quyết!!

Best practices

Đối với các giá trị primitive, và một số kiểu immutable như String, LocalDate bạn hoàn toàn có thể gán và trả về trực tiếp giá trị của biến, bởi các giá trị primitive được sao chép giá trị trực tiếp thay vì tham chiếu, còn kiểu immutable việc thay đổi giá trị của nó tạo ra một object mới.

Đối với các kiểu object khác, bạn cần triển khai một phương thức riêng để sao chép các đối tượng và sử dụng trong phương thức getter, setter của mình

Kết luận

Trên đây là những điều các bạn cần lưu ý khi triển khai các phương thức getter và setter, 2 phương thức thoạt nhìn rất đơn giản nhưng nếu sử dụng một cách "ngây thơ" sẽ rất nguy hiểm cho dữ liệu trong ứng dụng của bạn.