导航

引例

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/

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐