Java8 – Optional

Nếu bạn là lập trình viên Java thì chắc chắn bạn đã được nghe hoặc gặp các trường hợp NullPointerException trong chương trình

NullPointerExceptionRuntimeException là lỗi được đưa ra bởi JVM khi đang chạy. Kiểm tra null trong chương trình thường không được xem xét kỹ bởi lập trình viên nguyên nhân gây ra những bug nghiêm trọng trong code.

Java 8 giới thiệu một kiểu mới được gọi là Optional<T> để giúp các lập trình viên đối phó với giá trị null một cách hợp lý.

Khái niệm Optional là không mới và các ngôn ngữ lập trình khác có constructor tương tự. Ví dụ, Scala có Option[T] và Haskell có kiểu Maybe

Optional là gì?

Optional là kiểu Container cho một giá trị có thể không tồn tại.

Hãy cùng xem ví dụ sau đây nhận đầu vào là userId, dùng để lấy thông tin chi tiết người dùng trong cơ sở dữ liệu và trả về.

User findUserById(String userId) { ... };

Nếu UserId không tồn tại trong DB thì hàm bên trên sẽ trả về null. Thông thường chúng ta sẽ thực hiện như sau

User user = findUserById("667290");
System.out.println("User's Name = " + user.getName());

Trong trường hợp này thì một NullPointerException sẽ được đưa ra đúng không? Các lập trình viên hay quên kiểm tra giá trị null trong code. Nếu UserId không tồn tại trong DB thì đoạn code bên trên sẽ đưa ra NullPointerException.

Chúng ta sẽ cùng nhau tìm hiểu làm thế nào Optional giúp bạn làm giảm nhẹ rủi ro khi gặp NullPointerException ở đây

Optional<User> findUserById(String userId) { ... };

Bằng việc hàm trả về Optional<User>, chúng ta đã giúp cho phía client nhận biết rõ ràng hơn hàm này có thể có hoặc không có User khi truyền vào userId. Giờ đây, khi client sử dụng hàm buộc phải xử lý thực tế này.

Client code có thể được viết lại như sau

Optional<User> optional = findUserById("667290");

optional.ifPresent(user -> {
    System.out.println("User's name = " + user.getName());    
})

Khi chúng ta có một Object Optional thì có thể sử dụng các phương thức Util để làm việc với Optional. Phương thức ifPresent trong code bên trên gọi biểu thức Lamba được cung cấp nếu user tồn tại, ngoài ra thì nó không xử lý gì

Tạo Optional object

Tạo Optional trống

Một Object Optional trống miêu tả một giá trị không tồn tại hay không có giá trị

Optional<User> user = Optional.empty();

Tạo Optional với giá trị không null

User user = new User("667290", "Rajeev Kumar Singh");
Optional<User> userOptional = Optional.of(user);

Nếu giá trị cung cấp cho Optional.ofnull, thì nó sẽ đưa ra một NullPointerException ngay lập tức và Optional object sẽ không được tạo

Tạo Optional mà có thể null hoặc không null

Optional<User> userOptional = Optional.ofNullable(user);

Nếu giá trị truyền vào là khác null thì nó sẽ trả về một Optional chứa giá trị cụ thể, ngoài ra thì nó trả về một Optional rỗng.

Kiểm tra giá trị tồn tại

isPresent()

Phương thức isPresent() trả về true nếu Optional chứa giá trị khác null, ngoài ra nó trả về false

if(optional.isPresent()) {
    // Giá trị tồn tại
    System.out.println("Value found - " + optional.get());
} else {
    // Giá trị không tồn tại
    System.out.println("Optional is empty");
}

ifPresent()

Phương thức ifPresent() cho phép truyền một hàm Consumer được chạy nếu giá trị tồn tại bên trong Optional Object

Nó sẽ không làm gì nếu Optional object là trống

optional.ifPresent(value -> {
    System.out.println("Value found - " + value);
});

Chú ý rằng sử dụng biểu thức Lamba cho phương thức ifPresent() giúp cho code trở nên dễ đọc và ngắn gọn

Nhận giá trị sử dụng phương thức get()

Phương thức get() của Optional trả về giá trị nếu tồn tại, ngoài ra đưa ra lỗi NoSuchElementException

User user = optional.get()

Nên tránh sử dụng phương thức get() trên Optional mà không thực hiện kiểm tra xem giá trị có tồn tại hay không bởi vì nó sẽ đưa ra một lỗi nếu giá trị không tồn tại.

Trả về giá trị mặc định sử dụng orElse()

orElse() được sử dụng khi bạn muốn trả về một giá trị mặc định nếu Optional là trống. Xem ví dụ sau đây

// trả về "Unknown User" nếu user null
User finalUser = (user != null) ? user : new User("0", "Unknown User");

Viết lại đoạn code trên sử dụng orElse()

// trả về "Unknown User" nếu user null
User finalUser = optionalUser.orElse(new User("0", "Unknown User"));

Trả về giá trị mặc định sử dụng orElseGet()

Không giống như orElse(), trả về trực tiếp giá trị mặc định khi Optional là trống, orElseGet() cho phép truyền vào một hàm, được gọi khi Optional là trống. Kết quả trả về của hàm sẽ là giá trị mặc định của Optional

User finalUser = optionalUser.orElseGet(() -> {
    return new User("0", "Unknown User");
});

Đưa ra một Exception nếu không có giá trị

Bạn có thể sử dụng orElseThrow() để đưa ra một Exception nếu không có giá trị. Kiểu trả về theo từng ngữ cảnh có lẽ là hữu ích – trả về một custom ResourceNotFound() Exception từ Rest API nếu object với các tham số request cụ thể không tồn tại.

@GetMapping("/users/{userId}")
public User getUser(@PathVariable("userId") String userId) {
    return userRepository.findByUserId(userId).orElseThrow(
	    () -> new ResourceNotFoundException("User not found with userId " + userId);
    );
}

Lọc giá trị sử dụng phương thức filter

Bạn có một Optional object của User và muốn kiểm tra giới tính của nó và gọi một hàm nếu nó là MALE. Đây là cách chúng ta hay xử lý

if(user != null && user.getGender().equalsIgnoreCase("MALE")) {
    // Gọi hàm
}

Hãy thử viết lại sử dụng Optional với filter.

userOptional.filter(user -> user.getGender().equalsIgnoreCase("MALE"))
.ifPresent(() -> {
    // Hàm
})

Phương thức filter nhận điều kiện xác nhận như một tham số. Nếu Optional chứa một giá trị không null và giá trị trùng với điều kiện xác nhận truyền vào thì filter trả về một Optional với giá trị đó, ngoài ra thì nó trả về một Optional rỗng.

Do đó, hàm bên trong ifPresent() bên trên sẽ được gọi khi và chỉ khi Optional chứa một User và User là MALE

Trích xuất và chuyển đổi dữ liệu sử dụng map()

Giả sử bạn muốn lấy địa chỉ của một người dùng khi nó tồn tại và in ra nếu địa chỉ từ Ấn Độ

Xem xét phương thức getAddress() sau bên trong class User

Address getAddress() {
    return this.address;
}

Đây là cách để có được kết quả như mong muốn

if(user != null) {
    Address address = user.getAddress();
    if(address != null && address.getCountry().equalsIgnoreCase("India")) {
	    System.out.println("User belongs to India");
    }
}

Hãy thử viết lại sử dụng map để nhận được kết quả tương tự

userOptional.map(User::getAddress)
.filter(address -> address.getCountry().equalsIgnoreCase("India"))
.ifPresent(() -> {
    System.out.println("User belongs to India");
});

Trông code ngắn gọn và dễ đọc hơn phải không? Hãy chia nhỏ đoạn code kia ra và cùng nhau tìm hiểu từng đoạn một

// Trích xuất địa chỉ User sử dụng phương thức map().
Optional<Address> addressOptional = userOptional.map(User::getAddress)

// Lọc địa chỉ từ India
Optional<Address> indianAddressOptional = addressOptional.filter(address -> address.getCountry().equalsIgnoreCase("India"));

// In ra nêu quốc gia là India
indianAddressOptional.ifPresent(() -> {
    System.out.println("User belongs to India");
});

Trong đoạn code trên, phương thức map trả về một Optional trống theo 2 trường hợp

  1. User không tồn tại trong userOptional
  2. User tồn tại nhưng getAddress trả về null

Ngoài ra nó sẽ trả về Optional<Address> chứa địa chỉ User

Optional phân tầng sử dụng flatMap()

Hãy cùng xem lại ví dụ bên trên sử dụng map. Bạn sẽ thắc mắc rằng nếu địa chỉ có thể là null thì tại sao bạn không trả về một Optional<Address> thay cho một Address đơn giản từ phương thức getAddress()? Bạn đã chính xác, hãy sửa đoạn code trên để getAddress trả về Optional<Address>

Hãy xem lại đoạn code bên dưới đây

Optional<Address> addressOptional = userOptional.map(User::getAddress)

getAddress trả về Optional<Address> nên kiểu trả về của userOptional.map sẽ là Optional<Optional<Address>>

Optional<Optional<Address>> addressOptional = userOptional.map(User::getAddress)

Bạn chắc chắn sẽ không muốn 2 Optional lồng nhau như vậy. Hãy sử dụng flatMap để sửa cho chính xác

Optional<Address> addressOptional = userOptional.flatMap(User::getAddress)

Nguyên tắc ở đây là nếu map trả về Optional thì nên sử dụng flatMap thay cho map để flatten kết quả của Optional

 

 

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.