Java8新特性(三) Optional与NullPointerException
导航引例Optional容器类有值状态与无值状态三种方法创建Optional对象Optional操作详解取值isPresent和ifPresentfilter、map、flatMap注意点引例说起NullPointerException你肯定不会陌生,因为它大概是我们日常开发中碰到最多的问题。为了避免空指针异常的出现,我们常常需要做很多的逻辑判断。下...
导航
引例
说起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对象时有两个注意点:
- 调用empty()方法创建Optional对象时,默认类型为Object。不过你也可以指定类型,例如Optional.<Apple>empty()。
- 调用of()方法时,若传入的参数为null会抛出NullPointerException。
Optional操作详解
四种取值方式
当我们得到一个Optional对象,应该怎么获取它的值呢?Optional类中提供四个方法:
- get():最简单但又最不安全的方法,若Optional对象有值返回值,无值抛出NoSuchElementException异常。
- orElse():参数为一个实例对象,若Optional对象有值返回值,无值返回该对象。
- orElseGet():将Supplier接口作为参数,若Optional对象有值返回值,无值返回调用Supplier生成的实例。
- 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
更多推荐
所有评论(0)