Java8新特性(三) Optional與NullPointerException

導航

引例

Optional

容器類

有值狀態與無值狀態

三種方法創建Optional對象

Optional操作詳解

取值

isPresent和ifPresent

filter、map、flatMap

注意點


引例

說起NullPointerException你肯定不會陌生,因爲它大概是我們日常開發中碰到最多的問題。爲了避免空指針異常的出現,我們常常需要做很多的邏輯判斷。下面通過一個例子演示我們平常是如何應對空指針問題的。

蘋果有不同的種類,而不同的品牌會同生產同一種蘋果,蘋果、種類和品牌三者可以構成一種嵌套關係。

public class Apple {
	
	private Kind kind;

	public Kind getKind() {
		return kind;
	}
	public void setKind(Kind kind) {
		this.kind = kind;
	}
	
	@Override
	public String toString() {
		return "Apple [kind=" + kind + "]";
	}
	
}
public class Kind {
	
	private Brand brand;

	public Brand getBrand() {
		return brand;
	}
	public void setBrand(Brand brand) {
		this.brand = brand;
	}
	
	@Override
	public String toString() {
		return "Kind [brand=" + brand + "]";
	}
	
}
public class Brand {
	
	private String brandName;

	public String getBrandName() {
		return brandName;
	}
	public void setBrandName(String brandName) {
		this.brandName = brandName;
	}

	@Override
	public String toString() {
		return "Brand [brandName=" + brandName + "]";
	}

}

可能有的人得到一個蘋果,就直接查看這個蘋果的品牌。例如這樣: 

String brandName = apple.getKind().getBrand().getBrandName();

這樣的代碼極有可能出現空指針問題,當然大多數人會採取一定的手段避免NullPointerException。下面演示兩種做法:

public class TestNullPointer {
	
	public static void main(String[] args) {
		System.out.println(getAppleBrand1(null));
		System.out.println(getAppleBrand2(null));
	}
	// 方式一
	private static String getAppleBrand1(Apple apple) {
		String result = "empty";
		if(apple != null) {
			Kind kind = apple.getKind();
			if (kind != null) {
				Brand brand = kind.getBrand();
				if (brand != null) {
					result = brand.getBrandName();
				}
			}
		}
		return result;
	}
	// 方式二
	private static String getAppleBrand2(Apple apple) {
		String defaultValue = "empty";
		if(apple == null) 
			return defaultValue;
		Kind kind = apple.getKind();
		if(kind == null) 
			return defaultValue;
		Brand brand = kind.getBrand();
		if(brand == null)
			return defaultValue;
		return brand.getBrandName();
	}
	
}

打印結果如下:

empty
empty

上面給出的兩種做法都使用了大段的判斷邏輯避免空指針問題,這讓我們的代碼的可讀性變得很差。那麼還有更好的方法解決這個問題嗎?答案是肯定的,我們可以使用Java8提供Optional免去代碼中大段的判斷邏輯,讓代碼變的更加簡潔。

首先我們需要重新設計Apple和Brand兩個類:

public class Apple2 {

	private Kind2 kind2;

	public Optional<Kind2> getKind2() {   // 使用Optional封裝返回值
		return Optional.ofNullable(kind2);
	}
	public void setKind2(Kind2 kind2) {
		this.kind2 = kind2;
	}
	
	@Override
	public String toString() {
		return "Apple2 [kind2=" + kind2 + "]";
	}
	
}
public class Kind2 {
	
	private Brand brand;

	public Optional<Brand> getBrand() {   // 使用Optional封裝返回值
		return Optional.ofNullable(brand);
	}
	public void setBrand(Brand brand) {
		this.brand = brand;
	}
	
	@Override
	public String toString() {
		return "Kind2 [brand=" + brand + "]";
	}
	
}

在新設計的Apple2和Brand2類中,我們用Optional封裝兩個類中get()方法的返回值。重新實現TestNullPointer中的方法:

public class TestOptional1 {
	
	public static void main(String[] args) {
		System.out.println(getAppleBrand(null));
	}
	
	private static String getAppleBrand(Apple2 apple) {
		return Optional.ofNullable(apple)
				.flatMap(Apple2::getKind2)
				.flatMap(Kind2::getBrand)
				.map(Brand::getBrandName)
				.orElse("empty");
	}
	
}

打印結果如下(代碼中出現的::操作爲方法引用):

empty

上面的代碼中我們並沒有進行任何顯式判斷空值的操作,卻仍然達到了避免空指針的目的。

所以Optional讓我們避免了這樣的問題:爲了判斷空值而不得不顯式使用大段的判斷邏輯,降低代碼的可讀性。 換而言之,Optional可以讓你的代碼變得更加優雅。

 

Optional

容器類

Optional是JDK8提供的一個類。它可以簡單的封裝一個對象,使用成員變量(value)記錄該對象並提供一系列方法來操作該對象。簡單來說,Optional就是一個容器類。

public final class Optional<T> {
    private final T value;
    ... 
}

有值狀態與無值狀態

Optional有兩種狀態,一種是無值狀態(value爲null,一個空的Optional對象),另一種是有值狀態(value不爲null)。

可能你會覺得奇怪——一個空的Optional對象和null有區別嗎?

在語義上你可以把它們當作一回事兒,但是實際使用上它們之間的差別很大:如果你嘗試解引用一個null,一定會觸發NullPointerException;不過使用空的Optional對象卻不會出現這種問題,因爲它依然是一個有效的對象。 

三種方法創建Optional對象

Optional的構造方法都是私有方法,這意味着如果我們想要創建Optional對象就只能通過該類提供的靜態方法。Optional提供了三個靜態方法創建一個Optional對象:empty(),of()和ofNullable()。下面是Optional的部分源碼:

public final class Optional<T> {

    private static final Optional<?> EMPTY = new Optional<>();    //  調用空參構造方法

    private final T value;

    private Optional() {                                                         //  空參構造方法
        this.value = null;
    }

    private Optional(T value) {                                             //  有參構造方法
        this.value = Objects.requireNonNull(value);
    }

    public static<T> Optional<T> empty() {                          //  返回EMPTY
        Optional<T> t = (Optional<T>) EMPTY; 
        return t;
    }

    public static <T> Optional<T> of(T value) {                    // 調用有參構造
        return new Optional<>(value);   
    }

    public static <T> Optional<T> ofNullable(T value) {       // 根據傳入值是否爲null,選擇調用of()方法或empty()方法
        return value == null ? empty() : of(value);           
    }
    ...  
}

從源碼中可以看到,Optional類在初始化階段就會調用空參構造方法創建靜態常量EMPTY——一個空的Optional對象。三個靜態方法的作用如下:

  • empty():返回EMPTY。
  • of():調用有參構造方法。
  • ofNullable():根據參數選擇執行empty()方法或者of()方法。

下面通過一個例子演示三個方法:

public class TestOptional2 {
	
	public static void main(String[] args) {
		Optional<Object> op = Optional.empty();	
		System.out.println(op);          
		// 調用empty()方法創建Optional對象時,可以指定類型
		Optional<Apple> empty = Optional.<Apple>empty();
		
		Optional<Apple> op2 = Optional.of(new Apple());	
//		Optional<Object> op3 = Optional.of(null);	// 報錯: java.lang.NullPointerException
		System.out.println(op2); 
		
		Optional<Object> op4 = Optional.ofNullable(null);
		System.out.println(op4);           
	}
	
}

打印結果如下:

Optional.empty
Optional[Apple [kind=null]]
Optional.empty

創建Optional對象時有兩個注意點:

  1. 調用empty()方法創建Optional對象時,默認類型爲Object。不過你也可以指定類型,例如Optional.<Apple>empty()。
  2. 調用of()方法時,若傳入的參數爲null會拋出NullPointerException。

 

Optional操作詳解

四種取值方式

當我們得到一個Optional對象,應該怎麼獲取它的值呢?Optional類中提供四個方法:

  1. get():最簡單但又最不安全的方法,若Optional對象有值返回值,無值拋出NoSuchElementException異常。
  2. orElse():參數爲一個實例對象,若Optional對象有值返回值,無值返回該對象。
  3. orElseGet():將Supplier接口作爲參數,若Optional對象有值返回值,無值返回調用Supplier生成的實例。
  4. orElseThrow():將Supplier接口作爲參數,若Optional對象有值返回值,無值拋出調用Supplier的生成的異常。

下面通過一個例子演示:

public class TestOptional3 {
	
	public static void main(String[] args) {
		Optional<Apple> op = Optional.<Apple>ofNullable(null); 
//		Apple apple1 = op.get();     	            // NoSuchElementException
		Apple apple2 = op.orElse(new Apple());
		System.out.println(apple2);
		Apple apple3 = op.orElseGet(Apple::new);	
		System.out.println(apple3);
//		Apple apple4 = op.orElseThrow(RuntimeException::new);	// RuntimeException
	}
	
}

打印結果如下:

Apple [kind=null]
Apple [kind=null]

這些方法都比較容易理解,但是有兩個方法在使用上會有略微的區別——orElse()和orElseGet()。若Optional對象是無值狀態的,這兩個方法並沒有區別;但是如果Optional對象有值,區別就會出現。

爲了更好的演示效果,對Apple類出一點修改。在Apple類中加入構造方法:

public Apple() {
     System.out.println("create apple");
}

 用有值狀態的Optional對象分別調用orElse()和orElseGet()方法:

public class TestOptional4 {
	
	public static void main(String[] args) {
		Apple apple = new Apple();
		Optional<Apple> op = Optional.ofNullable(apple);
		System.out.println("======================");		
		op.orElseGet(Apple::new);
		System.out.println("======================");
		op.orElse(new Apple());  // 即使Optional對象有值,orElse()方法還是會創建對象
	}
	
}

打印結果如下:

create apple
======================
======================
create apple

通過打印結果我們可以發現:即使Optional對象有值,調用orElse()方法時還是會創建一個Apple對象;而orElseGet()方法在Optional對象是有值狀態下並不會調用傳入的Lambda表達式創建對象。

isPresent和ifPresent

如果Optional是無值狀態的,調用get()方法時會出現NoSuchElementException異常。爲了規避這個問題Optional提供isPresent()和ifPresent()方法。

  • isPresent():返回布爾值表示Optional是否爲有值狀態。
  • ifPresent():將Consumer接口作爲參數,若Optional爲有值狀態則調用該接口。

見下面一個例子:

public class TestOptional5 {
	
	public static void main(String[] args) {
		Optional<Apple> op = Optional.ofNullable(new Apple());
		// 兩種寫法等價
		if(op.isPresent())  
			System.out.println(op.get());
		op.ifPresent(System.out::println);
	}
	
}

打印結果如下:

create apple
Apple [kind=null]
Apple [kind=null]

代碼中兩種寫法是等價的,第二種寫法更高明一些。因爲第一種寫法完全沒有體現出Optional的作用,它與直接判null並沒有本質上的區別。

filter、map與flatMap

filter()方法可以過濾Optional對象中不符合條件的值,該方法使用Predicate接口作爲參數:

public Optional<T> filter(Predicate<? super T> predicate) 

調用此方法時,若Optional對象無值直接返回該Optional對象;反之,對Optional中的值應用predicate操作。若該操作執行結果爲true,將此Optional對象返回,否則返回一個空的Optional對象。

public class TestOptional6 {
	
	public static void main(String[] args) {
		Predicate<String> predicate = item -> item.length() > 4; // Lambada表達式
		Optional<String> op1 = Optional.ofNullable("hello");
		Optional<String> result1 = op1.filter(predicate);	// 將此Optional對象返回
		System.out.println(result1.get());
		
		Optional<String> op2 = Optional.<String>empty();
		Optional<String> result2 = op2.filter(predicate);  // 返回一個空的Optional
		System.out.println(result2.orElse("no element"));
	}
	
}

打印結果如下:

hello
no element

map()方法可以將Optional對象中的值進行映射,該方法將Function接口作爲參數:

public<U> Optional<U> map(Function<? super T, ? extends U> mapper)

調用此方法時,若Optional對象無值返回一個空的Optional對象;反之,對Optional對象中的值應用mapper操作,將該操作的返回值封裝爲Optional對象並返回此Optional對象。

public class TestOptional7 {
	
	public static void main(String[] args) {
		Function<String, Integer> function = String::length; // 方法引用
		Optional<String> op1 = Optional.ofNullable("hello");
		Optional<Integer> result1 = op1.map(function);  
		System.out.println(op1.get() + " length is " + result1.orElse(0));

		Optional<String> op2 = Optional.<String>empty();
		Optional<Integer> result2 = op2.map(function);
		System.out.println(result2);
	}

}

打印結果如下:

hello length is 5
Optional.empty

上面的例子中,mapper操作的返回值是Integer類型,所以將返回值封裝爲Optional對象並沒有什麼問題。但是如果mapper操作的返回值是本身就是Optional類型的,此時再將返回值封裝爲Optional對象,結果就不盡人意了。

public class TestOptional8 {
	
	public static void main(String[] args) {
		Optional<Apple2> op = Optional.of(new Apple2());
		// public Optional<Kind2> getKind2()
		Optional<Optional<Kind2>> map = op.map(Apple2::getKind2);
	}
	
}

通過上面的例子可以看到,如果mapper操作的返回值類型是Optional,調用map()方法得到的結果類型就是Optional嵌套的類型。所以針對這種情況,我們需要使用flatMap()方法:

public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) 

flatMap()方法和map()方法的不同之處在於:調用map()方法會先對Optional對象中的值應用mapper操作,然後將該操作返回的值封裝成一個Optional對象;而調用flatMap()方法只對Optional對象中的值應用mapper操作,該操作的返回值就是Optional類型,不需要再對返回值進行封裝。

public class TestOptional9 {
	
	public static void main(String[] args) {
		Optional<Apple2> op = Optional.of(new Apple2());
		Optional<Kind2> op2 = op.flatMap(Apple2::getKind2);
		Optional<Brand> op3 = op2.flatMap(Kind2::getBrand);
		Optional<String> op4 = op3.map(Brand::getBrandName);
		System.out.println(op4.orElse("no element"));
	}
	
}

上面的例子就是引例中TestOptional1的詳細版本。

 

注意點

1、Optional不能序列化,最好避免將Optional用作類的字段(field)類型。

public class Person {  
   private Optional<String> name;      // 應該避免將類的字段定義成Optional類型
   ...
}

2、推薦將Optional作爲方法的返回值類型,例如Stream中的findFirst()方法就是如此。

Optional<T> findFirst();

3、如果僅僅用作判空,那麼不應該使用Optional,直接判null就好了。

Apple apple = getApple();       
Optional.ofNullable(apple).ifPresent(System.out::println);    //  這樣使用Optional沒有體現Optional的價值
if (apple != null) System.out.println(apple);                           //   不如直接判null

4、更多Optional的使用技巧請參考:使用Optional的正確姿勢

 

參考:

https://www.cnblogs.com/zhangboyu/p/7580262.html

https://blog.rmiao.top/java-optional-usage-note/

https://yanbin.blog/proper-ways-of-using-java8-optional/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章