Một số tips để tránh lỗi NullPointerException trong Java
Chào mọi người! Trong bài này mình sẽ giới thiệu với mọi người một số tips để tránh lỗi NullPointerException(NPE) trong Java.
Hãy nhớ: Không bao giờ
trả về
một giá trị null,khởi tạo
một biến null,truyền tham số
là null, và code càng đơn giản càng dễ đọc càng tốt, tránh quá phức tạp mà khó quản lý được NullPointerException, khôngchấm chấm
quá nhiều trong đối tượng có nhiều thuộc tính object lồng nhau.
1. Return giá trị
1.1 Return một empy collection hoặc empty object thay vì giá trị null
Khi thiết kế các phương thức Java, việc trả về một collection rỗng hoặc đối tượng rỗng thay vì null giúp loại bỏ được các lần kiểm tra null không cần thiết. Điều này giúp code dễ đọc, gọn gàng và tránh lỗi NullPointerException.
Dưới đây là một ví dụ minh họa về cách return một collection rỗng thay vì null:
Ví dụ: Return một Collection rỗng Giả sử chúng ta có một lớp CustomerService chứa một phương thức tìm kiếm các đơn hàng (orders) của một khách hàng. Khi không tìm thấy đơn hàng nào, thay vì trả về null, ta sẽ trả về một danh sách rỗng (Collections.emptyList()):
import java.util.Collections;
import java.util.List;
public class CustomerService {
public List<Order> getOrdersByCustomerId(String customerId) {
// Giả sử tìm kiếm đơn hàng trong database nhưng không tìm thấy đơn hàng nào
List<Order> orders = findOrdersInDatabase(customerId);
// Trả về danh sách rỗng thay vì null
return orders != null ? orders : Collections.emptyList();
}
private List<Order> findOrdersInDatabase(String customerId) {
// Thực hiện logic để tìm kiếm đơn hàng
// Giả sử không tìm thấy đơn hàng, ta return null
return null;
}
}
Sử dụng phương thức
Khi sử dụng phương thức getOrdersByCustomerId(), bạn không cần phải kiểm tra null, vì kết quả luôn là một danh sách (rỗng nếu không có đơn hàng):
public class Main {
public static void main(String[] args) {
CustomerService service = new CustomerService();
List<Order> orders = service.getOrdersByCustomerId("customer123");
// Không cần kiểm tra null, có thể sử dụng trực tiếp
if (orders.isEmpty()) {
System.out.println("No orders found for the customer.");
} else {
orders.forEach(order -> System.out.println("Order ID: " + order.getId()));
}
}
}
Lợi ích của việc trả về Collection rỗng
- Loại bỏ kiểm tra null: Không cần phải kiểm tra
null
ở mọi nơi sử dụng, giảm số dòng code. - Tránh lỗi NullPointerException: Khi trả về
null
, các thao tác như.size()
hay.get()
dễ gây NPE nếu người dùng quên kiểm tranull
. - Code dễ đọc và bảo trì: Giảm độ phức tạp và giúp người đọc dễ hiểu được mục đích của code.
Việc return một collection rỗng thay vì null
là một thực hành tốt giúp code của bạn an toàn và sạch hơn, đặc biệt là trong các ứng dụng lớn.
1.2 Return một EMPTY String
Khi làm việc với các chuỗi (String), việc trả về một chuỗi rỗng ("") thay vì null giúp tránh được các lỗi NullPointerException và loại bỏ sự cần thiết phải kiểm tra null mỗi lần sử dụng.
1.3 Return một giá trị Unknown/ Default thay vì Null
Trước: Trả về null
(dễ gây lỗi NullPointerException
)
public User getUser(UserType type) {
switch (type) {
case ADMIN: return getAdmin();
case MANAGER: return getManager();
default: return null;
}
}
Sau: Trả về UnknownUser
(Null Object)
public User getUser(UserType type) {
switch (type) {
case ADMIN: return new AdminUser();
case MANAGER: return new ManagerUser();
default: return new UnknownUser();
}
}
// Null Object
class UnknownUser extends User {
@Override
public String getName() { return "Unknown"; }
@Override
public void performAction() { System.out.println("No action for unknown user"); }
}
Sử dụng
User user = getUser(UserType.UNKNOWN);
System.out.println(user.getName()); // Output: Unknown
user.performAction(); // Output: No action for unknown user
2. Kiểm tra null trước khi sử dụng (Bảo vệ bản thân 2 lớp)
Trường hơp bất khả kháng chúng ta cần return null hoặc trong các hệ thống code cũ thì điều này nên làm để bảo vệ bản thân, đặc biệt là các service mà ta không handle trực tiếp không biết bên kia họ trả gì về cho ta.
Khi nào cần kiểm tra null?
- Khi nhận giá trị từ một nguồn bên ngoài (API, database, input của người dùng).
- Trước khi gọi phương thức hoặc truy cập thuộc tính của một đối tượng.
- Khi truyền đối tượng vào các lớp hoặc phương thức khác mà không chắc chắn giá trị tồn tại.
Ví dụ khi bạn làm việc với thrift bạn gọi một method nào đó thông qua thrift nó return về một object thì bạn phải luôn luôn check null nó trước khi sử dụng, có thể do một số lỗi nào đó của network hoặc exception bên kia khi trả về qua thrift nên bạn phải tự bảo vệ mình.
Kiểm tra Null-safe
Nếu một ArrayList
được trả về từ một service bên ngoài có thể là null
, bạn nên đảm bảo xử lý null một cách an toàn. Dưới đây là cách sử dụng Collections.emptyList()
để trả về một danh sách rỗng thay vì null, giúp tránh NullPointerException
khi truy xuất dữ liệu từ danh sách đó.
Ví dụ: Null-safe với ArrayList từ một service bên ngoài
Giả sử bạn có một phương thức getItems()
từ một service có thể trả về null
:
public List<String> getItems() {
// Giả sử service này có thể trả về null nếu không có dữ liệu
return externalService.fetchItems();
}
Thay vì kiểm tra null mỗi khi sử dụng getItems()
, bạn có thể sửa hàm để luôn trả về một danh sách rỗng nếu kết quả là null:
public List<String> getItems() {
List<String> items = externalService.fetchItems();
return items != null ? items : Collections.emptyList();
}
Sử dụng trong code mà không cần kiểm tra null:
Bây giờ, bạn có thể sử dụng getItems()
mà không cần kiểm tra null thủ công:
for (String item : getItems()) {
System.out.println(item);
}
Với cách này, nếu externalService.fetchItems()
trả về null, getItems()
sẽ trả về một danh sách rỗng, và vòng lặp for-each
sẽ không gây lỗi, chỉ đơn giản là không thực hiện in ra bất kỳ phần tử nào.
3. Khởi tạo giá trị khi sử dụng
3.1. Khởi tạo ngay khi khai báo
Bạn có thể khởi tạo giá trị mặc định cho property ngay tại thời điểm khai báo:
private List<User> users = new ArrayList<>();
Với cách này, bạn sẽ không cần lo lắng về việc kiểm tra null khi sử dụng users
, vì nó luôn được khởi tạo sẵn.
3.2. Khởi tạo trong hàm constructor
Nếu bạn cần đảm bảo rằng một property được khởi tạo trong quá trình tạo đối tượng, bạn có thể sử dụng constructor:
public class UserManager {
// Cách 1: Khởi tạo ngay khi khai báo
private List<User> users = new ArrayList<>();
// Cách 2: Khởi tạo trong constructor
public UserManager() {
// users = new ArrayList<>();
}
// Cách 3: Khởi tạo trong getter
public List<User> getUsers() {
if (users == null) {
users = new ArrayList<>();
}
return users;
}
public void addUser(User user) {
getUsers().add(user);
}
}
4. Hạn chế sử dụng multi-dot
Multi-dot syntax là khi bạn gọi chuỗi các phương thức hoặc truy cập các trường nhiều lớp liên tiếp trong một dòng, ví dụ:
public String getUserCity(User user) {
return user.getProfile().getAddress().getCity(); // Lỗi nếu getProfile() hoặc getAddress() là null
}
Nếu user.getProfile()
hoặc getAddress()
trả về null
, bạn sẽ gặp lỗi NullPointerException
.
Cách giải quyết: Kiểm tra Null ở từng bước
public String getUserCity(User user) {
if (user != null && user.getProfile() != null && user.getProfile().getAddress() != null) {
return user.getProfile().getAddress().getCity();
}
return "Unknown City"; // Giá trị mặc định
}
Sử dụng Optional (Bài viết sau nhé...)
Cảm ơn bạn đã đọc bài, bạn có thể để lại feedback để mình có thể hoàn thiện hơn trong các bài viết sau. Thanks.
All Rights Reserved