CodeGym /Các khóa học /JAVA 25 SELF /Các lỗi với bộ sửa đổi truy cập

Các lỗi với bộ sửa đổi truy cập

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Trong Java, các bộ sửa đổi truy cập — giống như hệ thống khóa trong một ngôi nhà. Chúng xác định ai và từ đâu có thể “vào” phòng của bạn (hay vào trường/phương thức của lớp bạn). Nếu mọi cánh cửa đều mở — ai cũng có thể đến và thay đổi gì đó. Nếu mọi thứ đều khóa — không ai có thể làm hỏng gì, nhưng chính bạn đôi lúc cũng có thể bị “mắc kẹt” bên trong.

Nhắc lại, trong Java có bốn mức truy cập chính:

Bộ sửa đổi Khả dụng trong lớp Khả dụng trong package Khả dụng trong các lớp con Khả dụng trong các package khác
private
(package)
protected
(thông qua kế thừa)
public

(package) — là khi bộ sửa đổi không được chỉ định rõ ràng. Thành viên lớp như vậy chỉ hiển thị trong một package.

2. Các lỗi thường gặp với bộ sửa đổi truy cập

Lỗi 1: Trường và phương thức với bộ sửa đổi mặc định (package-private)

Lỗi phổ biến nhất của người mới — quên chỉ định bộ sửa đổi truy cập. Kết quả là trường hoặc phương thức trở nên khả dụng trong toàn bộ package, dù bạn không hề chủ định. Điều này có thể dẫn đến việc một lớp khác (trong cùng package nhưng không liên quan tới lớp của bạn) có thể thay đổi trạng thái bên trong của đối tượng bạn.

// Lỗi: trường name không được bảo vệ!
class User {
    String name; // package-private!
}

Kết quả là bất kỳ lớp nào trong package này cũng có thể viết:

User user = new User();
user.name = "Vasya"; // không bị hạn chế!

Lỗi 2: Vi phạm đóng gói — mở trường dữ liệu (public)

Lỗi phổ biến thứ hai — khai báo các trường của lớp là public. Điều này tiện khi bạn chỉ đang học hoặc viết ví dụ ngắn, nhưng trong dự án thực tế thì hầu như luôn tệ. Bạn mất quyền kiểm soát ai và bằng cách nào thay đổi dữ liệu của mình.

public class Account {
    public double balance; // NGUY HIỂM!
}

Bây giờ bất kỳ đoạn mã nào cũng có thể làm:

Account acc = new Account();
acc.balance = -1000000; // Vậy bây giờ ai chịu trách nhiệm?

Lỗi 3: Thiếu getter và setter

Đôi khi lập trình viên đặt các trường là private, nhưng quên thêm các phương thức để quản lý chúng. Kết quả là không thể lấy hoặc thay đổi giá trị ngay cả khi điều đó là phù hợp.

public class Product {
    private String name;
    // Không có getName() hoặc setName()
}

Lỗi 4: Cố gắng truy cập các thành viên private từ lớp khác

Nếu bạn khai báo một trường hoặc phương thức là private, thì không thể truy cập nó từ lớp khác, ngay cả khi lớp đó ở cùng package. Người mới thường ngạc nhiên vì sao “không thấy” được trường.

public class User {
    private String password;
}

public class UserService {
    public void resetPassword(User user) {
        // user.password = "123"; // Lỗi biên dịch!
    }
}

Lỗi 5: Những sai sót với protected

Nhiều người nghĩ rằng protected nghĩa là “nhìn thấy ở mọi nơi có kế thừa”. Nhưng trong Java, truy cập tới các thành viên protected ngoài package chỉ khả dụng thông qua kế thừa và chỉ cho lớp con. Đây là một điểm tinh tế rất dễ bỏ lỡ.

package animals;

public class Animal {
    protected void sleep() {}
}

package zoo;
import animals.Animal;

public class Dog extends Animal {
    public void test() {
        sleep(); // OK — lớp con
    }
}

public class NotADog {
    public void test() {
        Animal a = new Animal();
        // a.sleep(); // Lỗi: không phải lớp con!
    }
}

3. Làm đúng: Best practices

Quy tắc 1: Mặc định hãy để các trường là private

Đây là nguyên tắc chính của đóng gói. Các trường phải được ẩn với tất cả mọi thứ ngoài chính lớp đó. Nếu cần cấp quyền truy cập — hãy dùng getter/setter.

public class Book {
    private String title;
    private int pages;

    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
}

Quy tắc 2: Chỉ mở những phương thức cần thiết

Nếu một phương thức cần được truy cập từ bên ngoài — hãy đặt nó là public. Nếu chỉ cần trong nội bộ package — để package-private. Nếu phương thức chỉ dành cho lớp kế thừa — hãy dùng protected.

Quy tắc 3: Giảm thiểu phạm vi hiển thị

Phạm vi càng nhỏ thì khả năng lỗi ngẫu nhiên và “khách không mời” càng ít. Đừng đặt phương thức và trường thành public nếu không cần thiết.

Quy tắc 4: Dùng getter và setter để kiểm soát truy cập

Điều này cho phép thêm logic bổ sung khi đọc/ghi trường, ví dụ như kiểm tra hợp lệ.

public class Account {
    private double balance;

    public void setBalance(double balance) {
        if (balance < 0) {
            throw new IllegalArgumentException("Số dư không thể âm!");
        }
        this.balance = balance;
    }

    public double getBalance() {
        return balance;
    }
}

Quy tắc 5: Đừng phơi bày hiện thực bên trong

Nếu bạn có một mảng hoặc danh sách làm trường, đừng trả về nó trực tiếp qua getter — hãy trả về bản sao hoặc chỉ cung cấp những phương thức cần thiết.

public class Team {
    private List<String> members = new ArrayList<>();

    // Đúng:
    public List<String> getMembers() {
        return new ArrayList<>(members); // trả về bản sao
    }
}

4. Ví dụ thực tế

Giả sử chúng ta có lớp LibraryUser, mô tả người dùng thư viện.

Ví dụ triển khai sai

public class LibraryUser {
    public String name;
    public int borrowedBooks;
}

Ở dạng này, bất kỳ mã nào cũng có thể làm mọi thứ với đối tượng:

LibraryUser user = new LibraryUser();
user.name = null;
user.borrowedBooks = -10; // Logic? Logic gì?

Ví dụ triển khai đúng với đóng gói

public class LibraryUser {
    private String name;
    private int borrowedBooks;

    public LibraryUser(String name) {
        this.name = name;
        this.borrowedBooks = 0;
    }

    public String getName() {
        return name;
    }

    public int getBorrowedBooks() {
        return borrowedBooks;
    }

    public void borrowBook() {
        borrowedBooks++;
    }

    public void returnBook() {
        if (borrowedBooks > 0) {
            borrowedBooks--;
        }
    }
}

Bây giờ, mã bên ngoài không thể trực tiếp thay đổi số sách đã mượn hoặc tên người dùng. Mọi thứ đều được kiểm soát thông qua các phương thức của lớp.

5. Đặc điểm và lưu ý triển khai

Đôi khi có vẻ dễ dàng hơn khi đặt trường là public thay vì viết cả đống getter và setter. Nhưng đó là cái bẫy! Trường mở — giống như cửa căn hộ để ngỏ: đúng là tiện, nhưng không an toàn.

Một tinh tế nữa — không phải lúc nào cũng cần viết getter và setter cho mọi trường. Nếu giá trị trường không nên thay đổi sau khi tạo đối tượng, hãy chỉ cung cấp getter và đặt trường là final:

public class Passport {
    private final String number;

    public Passport(String number) {
        this.number = number;
    }

    public String getNumber() {
        return number;
    }
}

Cũng hãy nhớ: nếu một lớp được khai báo là public, tên tệp phải trùng với tên lớp! Điều này không hẳn là về bộ sửa đổi truy cập, nhưng lại là lỗi rất thường gặp của người mới.

6. Các lỗi thường gặp khi làm việc với bộ sửa đổi truy cập

Lỗi số 1: quên chỉ định bộ sửa đổi truy cập cho trường hoặc phương thức. Kết quả là trường hoặc phương thức trở nên khả dụng trong toàn bộ package, ngay cả khi bạn không muốn. Luôn chỉ rõ bộ sửa đổi, ngay cả khi IDE không cảnh báo.

Lỗi số 2: tất cả các trường đều được khai báo là public. Điều này phá vỡ đóng gói, làm cho mã của bạn dễ bị tổn thương và khó lường. Thói quen “cho đơn giản” trong ví dụ không nên xuất hiện trong mã sản phẩm.

Lỗi số 3: cố gắng truy cập trường private từ lớp khác. Java sẽ không cho phép — trình biên dịch sẽ bảo vệ bạn, nhưng nếu bạn chợt muốn “lách” qua reflection — hãy suy nghĩ vì sao lại cần làm vậy.

Lỗi số 4: kỳ vọng rằng các thành viên protected sẽ khả dụng ở mọi nơi có kế thừa. Thực tế, ngoài package, chỉ có thể truy cập từ lớp con và chỉ qua this hoặc qua đối tượng của lớp con.

Lỗi số 5: trả về collection nội bộ qua getter. Nếu trả về tham chiếu tới mảng hoặc danh sách nội bộ, mã bên ngoài có thể thay đổi nó, làm phá vỡ các bất biến của lớp.

Lỗi số 6: thiếu kiểm soát khi gán giá trị qua setter. Nếu không kiểm tra giá trị đầu vào, bạn có thể nhận được trạng thái không hợp lệ của đối tượng (ví dụ, số dư âm).

Lỗi số 7: phạm vi hiển thị của phương thức quá rộng. Đôi khi phương thức được đặt public, dù chỉ cần dùng trong package hoặc trong lớp. Điều này mở rộng API không cần thiết và làm phức tạp việc bảo trì.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION