給女朋友講解什麼是Optional【JDK 8特性】

Java3y發表於2018-11-19

前言

只有光頭才能變強

前兩天帶女朋友去圖書館了,隨手就給她來了一本《與孩子一起學程式設計》的書,於是今天就給女朋友講解一下什麼是Optional類。

  • 至於她能不能看懂,那肯定是看不懂的。(學到變數/for迴圈的女人怎麼能看懂呢)

不知道大家還記得上一篇《阿里巴巴 Java開發手冊》讀後感不,當時閱讀到空指標異常(NPE)時,書上提到JDK 8有個Optional類供我們使用,該類可以儘可能地防止出現空指標異常(NPE)。

文字力求簡單講清每個知識點,希望大家看完能有所收穫

一、基礎鋪墊

我們都知道JDK 8最重要的新特性是Lambda表示式,這個可以讓我們簡化非常多的程式碼編寫,不知道大家會使用了沒有。這裡我簡單跟大家來回顧一下~

1.1Lambda簡化程式碼例子

下面就以幾個例子來看看Lambda表示式是怎麼簡化我們程式碼的編寫的。

首先我們來看看建立執行緒


public static void main(String[] args) {
    // 用匿名內部類的方式來建立執行緒
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("公眾號:Java3y---回覆1進群交流");
        }
    });

    // 使用Lambda來建立執行緒
    new Thread(() -> System.out.println("公眾號:Java3y---回覆1進群交流"));
}
複製程式碼

再來看看遍歷Map集合:



public static void main(String[] args) {
    Map<String, String> hashMap = new HashMap<>();
    hashMap.put("公眾號", "Java3y");
    hashMap.put("交流群", "回覆1");

    // 使用增強for的方式來遍歷hashMap
    for (Map.Entry<String, String> entry : hashMap.entrySet()) {
        System.out.println(entry.getKey()+":"+entry.getValue());
    }

    // 使用Lambda表示式的方式來遍歷hashMap
    hashMap.forEach((s, s2) -> System.out.println(s + ":" + s2));
}
複製程式碼

在List中刪除某個元素


public static void main(String[] args) {

    List<String> list = new ArrayList<>();
    list.add("Java3y");
    list.add("3y");
    list.add("光頭");
    list.add("帥哥");
    
    // 傳統的方式刪除"光頭"的元素
    ListIterator<String> iterator = list.listIterator();
    while (iterator.hasNext()) {
        if ("光頭".equals(iterator.next())) {
            iterator.remove();
        }
    }

    // Lambda方式刪除"光頭"的元素
    list.removeIf(s -> "光頭".equals(s));
    
    // 使用Lambda遍歷List集合
    list.forEach(s -> System.out.println(s));
}
複製程式碼

從上面的例子我們可以看出,Lambda表示式的確是可以幫我們簡化程式碼的。

1.1函式式介面

使用Lambda表示式,其實都是建立在函式式介面上的。我們看看上面的程式碼的介面:

建立多執行緒的Runnable介面:


@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
複製程式碼

遍歷HashMap的BiConsumer介面:


@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);
    default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
        Objects.requireNonNull(after);
        return (l, r) -> {
            accept(l, r);
            after.accept(l, r);
        };
    }
}
複製程式碼

在List中刪除元素的Predicate介面:


@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}
複製程式碼

函式式介面的特點:由@FunctionalInterface註解標識,介面有且僅有一個抽象方法!

1.2Lambda簡單講解

或許我們一開始看到Lambda的時候,發現Lambda表示式的語法有點奇葩,甚至有點看不懂。沒事,這裡3y給大家用圖的形式畫一畫:

Lambda表示式組成

以Runnable介面來舉例:

Lambda表示式很簡單!

再不濟,我們在用IDE的時候,可以提示出Lambda表示式的語法的,這樣可以幫我們快速上手Lambda表示式:

IDEA提示Lambda表示式

說白了,我們使用Lambda表示式的架子是這樣的()->{},具體的時候看看函式式介面的抽象方法要求就可以了,再不濟就使用IDE智慧提示。

1.3泛型回顧

比如說public<U> Optional<U> map(Function<? super T, ? extends U> mapper)這個宣告,你看懂了嗎?


// 介面
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
複製程式碼

在泛型的上限和下限中有一個原則:PECS(Producer Extends Consumer Super)

  • 帶有子類限定的可以從泛型讀取【也就是--->(? extend T)】-------->Producer Extends
  • 帶有超類限定的可以從泛型寫入【也就是--->(? super T)】-------->Consumer Super

解析:傳入的引數是泛型 T 或者其父類,返回值是U或其子類。

具體可參考:

二、Optional類

一句話介紹Optional類:使用JDK8的Optional類來防止NPE(空指標異常)問題。

接下來我們看看文件是怎麼說的:

A container object which may or may not contain a non-null value.Additional methods that depend on the presence or absence of a contained value are provided

它是一個容器,裝載著非NULL元素(或者沒有裝載元素),提供了一系列的方法供我們判斷該容器裡的物件是否存在(以及後續的操作)。

Optional類的方法結構圖:

Optional類的方法結構圖

2.1建立Optional容器

我們先來看看Optional的屬性以及建立Optional容器的方法:


	// 1、建立出一個Optional容器,容器裡邊並沒有裝載著物件
    private static final Optional<?> EMPTY = new Optional<>();

	// 2、代表著容器中的物件
    private final T value;

	// 3、私有構造方法
    private Optional() {
        this.value = null;
    }

	// 4、得到一個Optional容器,Optional沒有裝載著物件
    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }

	// 5、私有構造方法(帶引數),引數就是具體的要裝載的物件,如果傳進來的物件為null,丟擲異常
    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }

	// 5.1、如果傳進來的物件為null,丟擲異常
    public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }


	// 6、建立出Optional容器,並將物件(value)裝載到Optional容器中。
	// 傳入的value如果為null,丟擲異常(呼叫的是Optional(T value)方法)
    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }

	// 建立出Optional容器,並將物件(value)裝載到Optional容器中。
	// 傳入的value可以為null,如果為null,返回一個沒有裝載物件的Optional物件
    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }
複製程式碼

所以可以得出建立Optional容器有兩種方式:

  • 呼叫ofNullable()方法,傳入的物件可以為null
  • 呼叫of()方法,傳入的物件不可以為null,否則丟擲NullPointerException

下面我們簡單就可以看看用法了:

現在我有一個User物件,這裡用到了Lombok,有興趣的同學可去學學瞭解一下:兩個月的Java實習結束,繼續努力


import lombok.Data;
@Data
public class User {

    private Integer id;
    private String name;
    private Short age;
}

複製程式碼

測試:


public static void main(String[] args) {

    User user = new User();
    User user1 = null;

    // 傳遞進去的物件不可以為null,如果為null則丟擲異常
    Optional<User> op1 = Optional.of(user1);

    // 傳遞進去的物件可以為null,如果為null則返回一個沒有裝載物件的Optional容器
    Optional<User> op2 = Optional.ofNullable(user);
}
複製程式碼

結果

2.2Optional容器簡單的方法


// 得到容器中的物件,如果為null就丟擲異常
public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

// 判斷容器中的物件是否為null
public boolean isPresent() {
    return value != null;
}

// 如果容器中的物件存在,則返回。否則返回傳遞進來的引數
public T orElse(T other) {
    return value != null ? value : other;
}

複製程式碼

這三個方法是Optional類比較常用的方法,並且是最簡單的。(因為引數不是函式式介面)

下面我們繼續看看用法:


public static void main(String[] args) {

        User user = new User();
        User user1 = null;

        Optional<User> op1 = Optional.ofNullable(user);
        System.out.println(op1.isPresent());
        System.out.println(op1.get());
        System.out.println(op1.orElse(user1));

    }
複製程式碼

結果很明顯,因為我們的user是不為null的:

結果

我們調換一下順序看看:


public static void main(String[] args) {

    User user = new User();
    User user1 = null;

    Optional<User> op1 = Optional.ofNullable(user1);
    System.out.println(op1.isPresent());
    System.out.println(op1.orElse(user));
    System.out.println(op1.get());

}
複製程式碼

結果

2.3Optional容器進階用法

當然了,我們到目前為止看起來Optional類好像就這麼一回事了,這樣程式碼寫起來還不如我自己判斷null呢...

我們對比一下:

對比

我們可以發現,手動判斷是否為null好像還更方便簡潔一點呢。

所以,我們帶函式式介面的方法登場了!

2.3.1ifPresent方法

首先來看看ifPresent(Consumer<? super T> consumer)方法



public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

複製程式碼

如果容器中的物件存在,則呼叫accept方法,比如說:


public static void main(String[] args) {

    User user = new User();
    user.setName("Java3y");
    test(user);
}

public static void test(User user) {

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

    // 如果存在user,則列印user的name
    optional.ifPresent((value) -> System.out.println(value.getName()));

    // 舊寫法
    if (user != null) {
        System.out.println(user.getName());
    }
}
複製程式碼

2.3.2orElseGet和orElseThrow方法

直接看原始碼:


// 如果物件存在,則直接返回,否則返回由Supplier介面的實現用來生成預設值
public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}


@FunctionalInterface
public interface Supplier<T> {
    T get();
}


// 如果存在,則返回。否則丟擲supplier介面建立的異常
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}

複製程式碼

例子:


public static void main(String[] args) {

    User user = new User();
    user.setName("Java3y");
    test(user);
}

public static void test(User user) {

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

    // 如果存在user,則直接返回,否則建立出一個新的User物件
    User user1 = optional.orElseGet(() -> new User());
    
    // 舊寫法
    if (user != null) {
        user = new User();
    }
}
複製程式碼

總的來說跟我們上面所講的orElse()差不多,只不過它可以通過Supplier介面的實現來生成預設值。

2.3.3filter方法

直接看原始碼:


// 如果容器中的物件存在,並且符合過濾條件,返回裝載物件的Optional容器,否則返回一個空的Optional容器
public Optional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    if (!isPresent())
        return this;
    else
        return predicate.test(value) ? this : empty();
}


// 介面
@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
}
複製程式碼

返回Optional物件我們就可以實現鏈式呼叫了!

例子:


public static void test(User user) {

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

    // 如果容器中的物件存在,並且符合過濾條件,返回裝載物件的Optional容器,否則返回一個空的Optional容器
    optional.filter((value) -> "Java3y".equals(value.getName()));
}

複製程式碼

2.3.4map方法

直接看原始碼:


// 如果容器的物件存在,則對其執行呼叫mapping函式得到返回值。然後建立包含mapping返回值的Optional,否則返回空Optional。
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Optional.ofNullable(mapper.apply(value));
    }
}


// 介面
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
複製程式碼

例子:


public static void test(User user) {

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

    // 如果容器的物件存在,則對其執行呼叫mapping函式得到返回值。然後建立包含mapping返回值的Optional,否則返回空Optional。
    optional.map(user1 -> user1.getName()).orElse("Unknown");
}

// 上面一句程式碼對應著最開始的老寫法:

public String tradition(User user) {
    if (user != null) {
        return user.getName();
    }else{
        return "Unknown";
    }
}

複製程式碼

2.3.5flatMap方法

直接看原始碼:


// flatMap方法與map方法類似,區別在於apply函式的返回值不同。map方法的apply函式返回值是? extends U,而flatMap方法的apply函式返回值必須是Optional
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Objects.requireNonNull(mapper.apply(value));
    }
}
複製程式碼

2.3.6總結

再來感受一下Optional的魅力


public static void main(String[] args) {
    User user = new User();
    user.setName("Java3y");
    System.out.println(test(user));
}

// 以前的程式碼v1
public static String test2(User user) {
    if (user != null) {
        String name = user.getName();
        if (name != null) {
            return name.toUpperCase();
        } else {
            return null;
        }
    } else {
        return null;
    }
}

// 以前的程式碼v2
public static String test3(User user) {
    if (user != null && user.getName() != null) {
        return user.getName().toUpperCase();
    } else {
        return null;
    }
}

// 現在的程式碼
public static String test(User user) {
    return Optional.ofNullable(user)
            .map(user1 -> user1.getName())
            .map(s -> s.toUpperCase()).orElse(null);
}
複製程式碼

Optional總結:

filter,map或flatMap一個函式,函式的引數拿到的值一定不是null。所以我們通過filter,map 和 flatMap之類的函式可以將其安全的進行變換,最後通過orElse系列,get,isPresent 和 ifPresent將其中的值提取出來。

其實吧,用Optional類也沒有簡化很多的程式碼,只是把NPE異常通過各種方法隱藏起來(包裝了一層)。通過Lambda表示式可以讓我們處理起來更加"優雅"一些。

三、最後

之前在初學的時候沒在意JDK8的特性,其實JDK更新很多時候都能給我們帶來不少好處的(簡化程式碼編寫,提高效能等等),所以作為一名Java程式設計師,還是得多學學新特性。(話說JDK9該類又有新特性了...)

如果你要評論“醒醒吧,程式設計師哪來的女朋友”,“我尿黃,讓我來”之類的話,我建議你是不是好好反省一下自己,為什麼別的程式設計師都有女朋友,就你沒有,是不是自己技術不過關了?通過“工廠”找一個有那麼難嗎?再不濟也能自己new一個出來啊。

當然了,我的女朋友是現實存在的。

參考資料:

如果你覺得我寫得還不錯,瞭解一下:

  • 堅持原創的技術公眾號:Java3y。
  • 文章的目錄導航(精美腦圖+海量視訊資源):github.com/ZhongFuChen…

相關文章