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
NullPointerException
và RuntimeException
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.of
là null
, 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
- User không tồn tại trong
userOptional
- 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)
Vì 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