+2

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ông chấ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 tra null.
  • 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?

  1. Khi nhận giá trị từ một nguồn bên ngoài (API, database, input của người dùng).
  2. 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.
  3. 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí