Giảm thiểu NullPointerException với Optional trong Java

Optional class được giới thiệu trong Java 8, dùng để bao ngoài một Object có kiểu dữ liệu bất kỳ và có thể tham chiếu đến null. Nó giúp giảm thiểu các NullPointerException xảy ra trong quá trình code vì nó chỉ cho phép truy xuất các object được bao bọc bên trong Optional object (wrapped object) thông qua hàm kiểm tra khác null.

Khởi tạo Optional object

Chúng ta có rất nhiều cách để khởi tạo một Optional object, trong phần này chúng ta sẽ điểm qua một số cách thường dùng. Để khởi tạo một Optional object rỗng chúng ta có thể sử dụng empty() method.

Optional<String> empty = Optional.empty();
        
boolean isPresent = empty.isPresent(); // FALSE

isPresent() method dùng để kiểm tra wrapped object có giá trị khác null hay không. Chúng ta cũng có thể sử dụng of() method để tạo một Optional object.

Optional<String> optional = Optional.of("Shareprogramming.net");

boolean isPresent = optional.isPresent(); // TRUE

Lưu ý rằng nếu giá trị truyền vào of() method là null thì sẽ gây ra NullPointerExcetion.

Optional<String> optional = Optional.of(null);

Exception in thread "main" java.lang.NullPointerException

Tuy nhiên nếu chúng ta không chắc rằng giá trị dùng để khởi tạo Optional object có khác null hay không thì có thể sử dụng isNullable() method. Nếu giá trị khởi tạo là null thì isNullable() sẽ tạo ra một Empty Optional object. 

String name = null;
Optional<String> opt = Optional.ofNullable(name);
boolean  isPresent = opt.isPresent(); // FALSE

Conditional Action với ifPresent()

ifPresent() cho phép chúng ta thực hiện các đoạn mã code wrapped object khác null.

Cách tiếp cận này giúp giảm thiểu đáng kể các NullPointerException xảy ra. Vì khi thao tác trên một object có thể bạn sẽ quên kiểm tra notnull trước khi sử dụng. Optional chỉ cho phép sử dụng wrapped object thông qua các method mà nó cung cấp, nhờ đó nó sẽ nhắc cho các lập trình viên nhớ rằng “à object này có thể null, cẩn trọng khi sử dụng”

Như ví dụ sau xuất độ dài của string khi nó khác null.

Optional<String > opt = Optional.of("shareprogramming.net");

opt.ifPresent(s -> System.out.println(s.length())); // 20

Hoặc nếu chuỗi là null thì đoạn code bên trong sẽ không được thực thi.

Optional<String > opt = Optional.empty();

opt.ifPresent(s -> System.out.println(s.length()));

Default value với orElse()

orElse() method cho phép chúng ra khởi tạo một giá trị mặc định wrapped object khi nó null.

String nullName = null;

String name = Optional.ofNullable(nullName).orElse("john"); // john

Default value với orElseGet()

Nó cũng giống với cách sử dụng của orElse() method dùng khởi tạo giá trị mặc định cho wrapped object khi nó null. orElseGet() nhận một implement của  Supplier functional interface, và sẽ được gọi khi cần thiết.

String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(() -> "john"); // john

Sự khác nhau giữa orElse() và orElseGet()

Thoạt nhìn có vẽ 2 thằng này giống nhau, thật ra là chúng giống nhau thiệt về cách sử dụng đều nhầm mục đích cung cấp một giá trị mặc định khi wrapped object null.

Tuy nhiên có một điểm khác biệt rất quan trọng mà chúng ta cần phải tinh tế để nhận ra, nó có thể ảnh hưởng lớn đến hiệu suất của chương trình.

Mình sẽ khởi tạo một getMyDefault() method và sử dụng nó trong orElse()orElseGet() method.

import java.util.Optional;
class test {
    public static void main(String[] agrs) {
        String text = null;

        String defaultText = Optional.ofNullable(text).orElseGet(test::getMyDefault);
        System.out.println( defaultText);

        defaultText = Optional.ofNullable(text).orElse(getMyDefault());
        System.out.println( defaultText);
    }

    public static String getMyDefault() {
        System.out.println("Getting Default Value");
        return "Default Value";
    }

}

Output: 

Getting Default Value
Default Value
Getting Default Value
Default Value

Ở ví dụ trên, cả 2 method orElse()orElseGet() đều hoạt động như nhau khi text null. Vậy khi text notnull thì chúng sẽ có một chút khác biệt, chúng ta sẽ xem qua ví dụ sau:

import java.util.Optional;
class test {
    public static void main(String[] agrs) {
        String text = "not null";

        String defaultText = Optional.ofNullable(text).orElseGet(test::getMyDefault);
        System.out.println( defaultText);

        defaultText = Optional.ofNullable(text).orElse(getMyDefault());
        System.out.println( defaultText);
    }

    public static String getMyDefault() {
        System.out.println("Getting Default Value");
        return "Default Value";
    }

}

Output: 

not null
Getting Default Value
not null

Như vậy, khi text notnull thì orElseGet() method sẽ không thực hiện lời gọi đến getMyDefault() và vì thế string object “Default Value” sẽ không được khởi tạo. Trong khi orElse() method sẽ luôn gọi getMyDefault() method bất kể text null hay notnull

Như vậy sử dụng orElseGet() sẽ tối ưu hoá bộ nhớ hơn khi các default object chỉ được tạo khi thật sự cần thiết. 

Exception với orElseThrow()

orElseThrow() method giống cách tiếp cận của orElseGet() method, nó sẽ ném ra một exception khi thay vì trả về một giá trị mặc định khi wrapped object null.

import java.util.Optional;


class test {
    public static void main(String[] agrs) {
        try {
            String nullName = null;
            String name = Optional.ofNullable(nullName).orElseThrow(
                    IllegalArgumentException::new);
        } catch (Exception e) {
            System.out.println(e);
        }
    }

}

Output: java.lang.IllegalArgumentException

get() method

get() method được sử dụng để lấy wrapped object.

Optional<String> opt = Optional.of("shareprogramming.net");
String name = opt.get(); // shareprogramming.net

Tuy nhiên đây là cách thường không được khuyến khích sử dụng vì nó sẽ không thực hiện bất kỳ một bài kiểm tra nào trước khi trả về giá trị cho chúng ta. Nếu wrapped object có giá trị null nó sẽ ném ra NoSuchElementException

try {
    Optional<String> opt = Optional.ofNullable(null);
    String name = opt.get();
} catch (Exception e) {
    System.out.println(e);
}

Output:  java.util.NoSuchElementException: No value present

Lọc giá trị với filter

Chúng ta có thể đặt ra các điều kiện cho wrapped object trước khi lấy kết quả trả về với filter() method. filter() method nhận một implement của Predicate functional interface. Nếu wrapped object vượt qua được bài kiểm tra từ Predicate, filter() trả về một Optional object chứa wrapped object ngược lại trả về Empty Optional.

Integer year = 2016;
Optional<Integer> yearOptional = Optional.of(year);
boolean is2016 = yearOptional.filter(y -> y == 2016).isPresent(); // TRUE

boolean is2017 = yearOptional.filter(y -> y == 2017).isPresent(); // FALSE

Transforming Value với map() và flatMap()

Ở phần trước chúng ta đã biết cách kiểm tra wrapped object với filter() method.  map() cho phép chúng ta chuyển một Optional value sang Optional value khác. 

import java.util.Arrays;
import java.util.List;
import java.util.Optional;


class test {
    public static void main(String[] agrs) {
        List<String> companyNames = Arrays.asList(
                "paypal", "oracle", "", "microsoft", "", "apple");

        Optional<List<String>> optional = Optional.of(companyNames);

        Optional<Integer> size = optional.map(List::size);
        size.ifPresent(System.out::println);

    }

}

flatMap() cũng tương tự như map(), điểm khác biệt duy nhất giữa chúng là map() chuyển đổi wrapped object còn flatMap() chuyển đổi một Optional wrapped object. 

import java.util.Arrays;
import java.util.List;
import java.util.Optional;


class test {
    public static void main(String[] agrs) {
        List<Optional<String>> companyNames = Arrays.asList(
                Optional.of("paypal"), Optional.of("oracle"), Optional.of("microsoft"), Optional.of("apple"));

        Optional<List<Optional<String>>> optional = Optional.of(companyNames);

        Optional<Integer> size = optional.flatMap(v -> Optional.of(v.size()));
        size.ifPresent(System.out::println);

    }

}

Kết hợp map(), flatMap(), filter() 

Ngoài việc sử dụng riêng lẽ các method trên thì chúng ta cũng có thể kết hợp chúng lại với nhau để có được kết quả cuối cùng.

import java.util.Arrays;
import java.util.List;
import java.util.Optional;


class test {
    public static void main(String[] agrs) {
        List<Optional<String>> companyNames = Arrays.asList(
                Optional.of("paypal"), Optional.of("oracle"), Optional.of("microsoft"), Optional.of("apple"));

        Optional<List<Optional<String>>> optional = Optional.of(companyNames);

        Optional<Integer> size = optional
                .flatMap(v -> Optional.of(v.size()))
                .filter(e -> e >= 3);
        size.ifPresent(System.out::println);

    }

}

Optional trong Serialization

Như đã bàn luận ở trên, thì Optional được sử dụng như là một wrapper class để đảm bảo sử dụng các wrapped object đúng cách vì vậy Optional không được khuyến khích sử dụng như là một thuộc tính trong class. Ngoài ra sử dụng Optional trong serializable class sẽ dẫn đến NotSerializableException.

Tóm lược

Như vậy chúng ta đã tìm hiểu về các tính năng và mục đích sử dụng của Optional rồi. Đây là một trong những tính năng khá hữu ích mà Java đã cung cấp để hỗ trợ các lập trình viên của họ tránh khởi các NullPointerException không đáng có. 

Nguồn tham khảo

https://www.baeldung.com/java-optional

https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html

5 1 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x