如何很“礼貌”的避免抛出空指针异常
如何很“礼貌”的避免抛出空指针异常
摘要
说到空指针,是猿宝宝们最头疼的问题(赞同的评论区“+1”哦),这篇文章教大家如何很“礼貌”的避免抛出空指针异常,大家此时此刻有个疑问,怎么个礼貌发,那么大招来了,大家装备都升级了吧,以免我放大招伤到猿宝宝,哈哈哈,那就是JDK1.8自动新特性Optional,到这里不得不说JDK爸爸牛扒... ...
1.概述
概述不多说了,主要是jdk中的这个类
import java.util.Optional;
2.如何创建Optional
有几种创建可选对象的方法。要创建一个空的Optional对象,我们只需要使用其空的static方法
package xyz.mrzhangxd.optionaltest;
@Test
public void whenCreatesEmptyOptional_thenCorrect() {
Optional<String> empty = Optional.empty();
assertFalse(empty.isPresent());
}
猿宝宝们注意了,我们使用了isPresent()方法来检查Optional对象中是否有一个值。仅当我们使用非空值创建Optional时,该值才存在。我们将在下一节中讨论isPresent方法。
我们还可以使用以下静态方法创建Optional对象:
package xyz.mrzhangxd.optionaltest;
@Test
public void givenNonNull_whenCreatesNonNullable_thenCorrect() {
String userName = "MrZhangxd";
Optional<String> opt = Optional.of(userName);
assertTrue(opt.isPresent());
}
在这里,我得插一嘴了,传递给of()方法的参数不能为null。否则,我们将获得NullPointerException:
package xyz.mrzhangxd.optionaltest;
@Test(expected = NullPointerException.class)
public void givenNull_whenThrowsErrorOnCreate_thenCorrect() {
String userName = null;
Optional.of(userName);
}
但是,如果我们期望某些空值,则可以使用ofNullable()方法:
package xyz.mrzhangxd.optionaltest;
@Test
public void givenNonNull_whenCreatesNullable_thenCorrect() {
String userName = "MrZhangxd";
Optional<String> opt = Optional.ofNullable(userName);
assertTrue(opt.isPresent());
}
通过ofNullable()方法这样做,如果我们在一个空引用传递,它不抛出一个异常,而是返回一个空的可选对象
package xyz.mrzhangxd.optionaltest;
@Test
public void givenNull_whenCreatesNullable_thenCorrect() {
String userName = null;
Optional<String> opt = Optional.ofNullable(userName);
assertFalse(opt.isPresent());
}
3.检查值是否存在:isPresent()和isEmpty()
当我们有一个从方法返回或由我们创建的Optional对象时,我们可以使用isPresent()方法检查其中是否有值
package xyz.mrzhangxd.optionaltest;
@Test
public void givenOptional_whenIsPresentWorks_thenCorrect() {
Optional<String> opt = Optional.of("MrZhangxd");
assertTrue(opt.isPresent());
opt = Optional.ofNullable(null);
assertFalse(opt.isPresent());
}
如果包装的值不为null,则此方法返回true。
另外,从Java 11开始,我们可以使用isEmpty方法执行相反的操作
package xyz.mrzhangxd.optionaltest;
@Test
public void givenAnEmptyOptional_thenIsEmptyBehavesAsExpected() {
Optional<String> opt = Optional.of("My name is MrZhangxd");
assertFalse(opt.isEmpty());
opt = Optional.ofNullable(null);
assertTrue(opt.isEmpty());
}
4.使用ifPresent()的条件操作
如果发现包装值非空,则ifPresent()方法使我们能够对包装值运行一些代码。。
if(userName != null) {
System.out.println(userName.length());
}
此代码在继续执行一些代码之前检查userName变量是否为null。这种方法很冗长,这不是唯一的问题,也容易出错。
确实,可以保证的是,在打印完该变量后,我们将不再使用它,然后忘记执行空检查。
如果在该代码中找到空值,则可能在运行时导致NullPointerException。当程序由于输入问题而失败时,通常是由于不良的编程习惯造成的。
可选使我们可以显式处理可空值,作为执行良好编程习惯的一种方式。现在让我们看一下如何在Java 8中重构以上代码。
在典型的函数式编程风格中,我们可以对实际存在的对象执行操作。
package xyz.mrzhangxd.optionaltest;
@Test
public void givenOptional_whenIfPresentWorks_thenCorrect() {
Optional<String> opt = Optional.of("MrZhangxd");
opt.ifPresent(userName -> System.out.println(userName.length()));
}
在上面的示例中,我们仅使用两行代码来替换第一个示例中的七行代码。一行将对象包装到Optional对象中,另一行执行隐式验证以及执行代码。
5.使用orElse()的默认值
orElse()方法用于检索包装在Optional实例内的值。它采用一个参数作为默认值。 orElse()方法返回包装的值(如果存在),否则返回其参数。
package xyz.mrzhangxd.optionaltest;
@Test
public void whenOrElseWorks_thenCorrect() {
String nullName = null;
String userName = Optional.ofNullable(nullName).orElse("MrZhangxd");
assertEquals("MrZhangxd", userName);
}
6.使用orElseGet()的默认值
orElseGet()方法类似于orElse()。但是,如果没有提供Optional值,则不采用返回值,而是采用供应商功能接口,该接口将被调用并返回调用的值.
package xyz.mrzhangxd.optionaltest;
@Test
public void whenOrElseGetWorks_thenCorrect() {
String nullName = null;
String userName = Optional.ofNullable(nullName).orElseGet(() -> "MrZhangxd");
assertEquals("MrZhangxd", userName);
}
7. orElse和orElseGet()之间的区别
对于Optional或Java 8的新手来说,orElse()和orElseGet()之间的区别尚不清楚。实际上,这两种方法给人的印象是它们在功能上相互重叠。
但是,如果不了解的话,两者之间会有细微但非常重要的区别,这可能会严重影响我们的代码性能。
让我们在测试类中创建一个名为getMyDefault()的方法,该方法不带任何参数并返回默认值.
package xyz.mrzhangxd.optionalproject;
public String getMyDefault() {
System.out.println("Getting Default Value");
return "Default Value";
}
让我们看两个测试并观察它们的作用,以确定orElse()和orElseGet()重叠的地方以及它们的不同之处.
package xyz.mrzhangxd.optionaltest;
import xyz.mrzhangxd.optionalproject;
@Test
public void whenOrElseGetAndOrElseOverlap_thenCorrect() {
String text = null;
String defaultText = Optional.ofNullable(text).orElseGet(this::getMyDefault);
assertEquals("Default Value", defaultText);
defaultText = Optional.ofNullable(text).orElse(getMyDefault());
assertEquals("Default Value", defaultText);
}
在上面的示例中,我们在Optional对象中包装了一个空文本,然后尝试使用两种方法中的每一种来获取包装后的值。作用如下.
Getting default value...
Getting default value...
在每种情况下都会调用getMyDefault()方法。碰巧的是,当不存在包装的值时,orElse()和orElseGet()都以完全相同的方式工作。
现在让我们运行另一个测试,其中存在该值,理想情况下,甚至不应创建默认值.
package xyz.mrzhangxd.optionaltest;
import xyz.mrzhangxd.optionalproject;
@Test
public void whenOrElseGetAndOrElseDiffer_thenCorrect() {
String text = "Text present";
System.out.println("Using orElseGet:");
String defaultText
= Optional.ofNullable(text).orElseGet(this::getMyDefault);
assertEquals("Text present", defaultText);
System.out.println("Using orElse:");
defaultText = Optional.ofNullable(text).orElse(getMyDefault());
assertEquals("Text present", defaultText);
}
在上面的示例中,我们不再包装空值,其余代码保持不变。现在,让我们看一下运行此代码的作用:
Using orElseGet:
Using orElse:
Getting default value...
猿宝宝们注意啦,使用orElseGet()检索包装的值时,由于存在包含的值,因此甚至不会调用getMyDefault()方法。
但是,使用orElse()时,无论是否存在包装的值,都会创建默认对象。因此,在这种情况下,我们仅创建了一个从未使用过的冗余对象。
在这个简单的示例中,创建默认对象不会花费很多成本,因为JVM知道如何处理此类对象。但是,当诸如getMyDefault()之类的方法必须进行Web服务调用或什至查询数据库时,则成本变得非常明显。
8. orElseThrow()的异常
orElseThrow()方法紧随orElse()和orElseGet()之后,并添加了一种新方法来处理缺少的值。当包装值不存在时,它不会返回默认值,而是会引发异常。
package xyz.mrzhangxd.optionaltest;
@Test(expected = IllegalArgumentException.class)
public void whenOrElseThrowWorks_thenCorrect() {
String nullName = null;
String userName = Optional.ofNullable(nullName).orElseThrow(
IllegalArgumentException::new);
}
Java 8中的方法引用在这里很方便,可以传入异常构造函数。
9.使用get()返回值
检索包装值的最终方法是get()方法.
package xyz.mrzhangxd.optionaltest;
@Test
public void givenOptional_whenGetsValue_thenCorrect() {
Optional<String> opt = Optional.of("baeldung");
String name = opt.get();
assertEquals("baeldung", name);
}
但是,与上述三种方法不同,get()仅在包装的对象不为null时才能返回值,否则,将引发no这样的元素异常。
package xyz.mrzhangxd.optionaltest;
@Test(expected = NoSuchElementException.class)
public void givenOptionalWithNull_whenGetThrowsException_thenCorrect() {
Optional<String> opt = Optional.ofNullable(null);
String userName = opt.get();
}
这是get()方法的主要缺陷。理想情况下,Optional应该可以帮助我们避免此类不可预见的异常。因此,此方法违背Optional的目标,并且可能在以后的版本中弃用。
因此,建议使用其他变量,这些变量使我们能够准备并显式处理null情况。这是get()方法的主要缺陷。理想情况下,Optional应该可以帮助我们避免此类不可预见的异常。因此,此方法违背Optional的目标,并且可能在以后的版本中弃用。
因此,建议使用其他变体,这些变体使我们能够准备并显式处理空情况。
10.带filter()的条件返回
我们可以使用filter方法对包装的值进行内联测试。它以谓词作为参数,并返回Optional对象。如果包装的值通过谓词的测试,则按原样返回Optional。
但是,如果谓词返回false,则它将返回空的Optional.
package xyz.mrzhangxd.optionaltest;
@Test
public void whenOptionalFilterWorks_thenCorrect() {
Integer year = 2019;
Optional<Integer> yearOptional = Optional.of(year);
boolean is2019 = yearOptional.filter(y -> y == 2019).isPresent();
assertTrue(is2019);
boolean is2020 = yearOptional.filter(y -> y == 2020).isPresent();
assertFalse(is2020);
}
通常以这种方式使用filter方法来基于预定义规则拒绝包装的值。我们可以使用它来拒绝错误的电子邮件格式或强度不够的密码。
让我们看另一个有意义的例子。假设我们要购买调制解调器,而我们只关心它的价格。我们从某个站点接收有关调制解调器价格的推送通知,并将其存储在对象中.
package xyz.mrzhangxd.optionalproject;
public class Modem {
private Double price;
public Modem(Double price) {
this.price = price;
}
// standard getters and setters
}
然后,我们将这些对象提供给某些代码,其唯一目的是检查调制解调器的价格是否在我们预算的范围内。
现在让我们看一下没有Optional的代码.
package xyz.mrzhangxd.optionalproject;
public boolean priceIsInRange1(Modem modem) {
boolean isInRange = false;
if (modem != null && modem.getPrice() != null
&& (modem.getPrice() >= 10
&& modem.getPrice() <= 15)) {
isInRange = true;
}
return isInRange;
}
猿宝宝们注意要实现此目的必须编写多少代码,尤其是在if条件下。如果条件对应用程序至关重要,则唯一的部分是最后的价格范围检查;其余的检查是防御性的。
package xyz.mrzhangxd.optionaltest;
import xyz.mrzhangxd.optionalproject.priceIsInRange1;
import xyz.mrzhangxd.optionalproject.Modem;
@Test
public void whenFiltersWithoutOptional_thenCorrect() {
assertTrue(priceIsInRange1(new Modem(10.0)));
assertFalse(priceIsInRange1(new Modem(9.9)));
assertFalse(priceIsInRange1(new Modem(null)));
assertFalse(priceIsInRange1(new Modem(15.5)));
assertFalse(priceIsInRange1(null));
}
除此之外,很可能会忘记一整天的空检查而不会出现任何编译时错误。
现在让我们看一下带有Optional#filter的变体.
package xyz.mrzhangxd.optionalproject;
public boolean priceIsInRange2(Modem modem2) {
return Optional.ofNullable(modem2)
.map(Modem::getPrice)
.filter(p -> p >= 10)
.filter(p -> p <= 15)
.isPresent();
}
映射调用仅用于将一个值转换为其他值。请记住,此操作不会修改原始值。
在我们的例子中,我们从Model类中获取一个价格对象。在下一节中,我们将详细介绍map()方法。
首先,如果将null对象传递给此方法,则不会有任何问题。
其次,我们在其主体内编写的唯一逻辑就是方法名称所描述的,即价格范围检查。可选的照顾其余的.
package xyz.mrzhangxd.optionaltest;
import xyz.mrzhangxd.optionalproject.priceIsInRange2;
import xyz.mrzhangxd.optionalproject.Modem;
@Test
public void whenFiltersWithOptional_thenCorrect() {
assertTrue(priceIsInRange2(new Modem(10.0)));
assertFalse(priceIsInRange2(new Modem(9.9)));
assertFalse(priceIsInRange2(new Modem(null)));
assertFalse(priceIsInRange2(new Modem(15.5)));
assertFalse(priceIsInRange2(null));
}
先前的方法有望检查价格范围,但除了防御其固有的脆弱性外,还必须做更多的事情。因此,我们可以使用filter方法替换不必要的if语句并拒绝不需要的值。
11.使用map()转换价值
在上面,我们研究了如何基于过滤器拒绝或接受值。我们可以使用类似的语法通过map()方法转换Optional值。
package xyz.mrzhangxd.optionaltest;
@Test
public void givenOptional_whenMapWorks_thenCorrect() {
List<String> companyNames = Arrays.asList(
"京东", "阿里巴巴", "", "华为", "", "苹果");
Optional<List<String>> listOptional = Optional.of(companyNames);
int size = listOptional
.map(List::size)
.orElse(0);
assertEquals(6, size);
}
在此示例中,我们将字符串列表包装在Optional对象中,并使用其map方法对包含的列表执行操作。我们执行的操作是检索列表的大小。
map方法返回包装在Optional中的计算结果。然后,我们必须在返回的Optional上调用适当的方法以获取其值。
请注意,filter方法仅对值进行检查并返回布尔值。另一方面,map方法采用现有值,使用该值执行计算,然后返回包装在Optional对象中的计算结果。
package xyz.mrzhangxd.optionaltest;
@Test
public void givenOptional_whenMapWorks_thenCorrect2() {
String userName = "MrZhangxd";
Optional<String> nameOptional = Optional.of(userName);
int len = nameOptional
.map(String::length)
.orElse(0);
assertEquals(8, len);
}
我们可以将地图和过滤器链接在一起,以执行更强大的操作。
假设我们要检查用户输入的密码是否正确;我们可以使用地图转换清除密码,并使用过滤器检查密码的正确性.
package xyz.mrzhangxd.optionaltest;
@Test
public void givenOptional_whenMapWorksWithFilter_thenCorrect() {
String password = " password ";
Optional<String> passOpt = Optional.of(password);
boolean correctPassword = passOpt.filter(
pass -> pass.equals("password")).isPresent();
assertFalse(correctPassword);
correctPassword = passOpt
.map(String::trim)
.filter(pass -> pass.equals("password"))
.isPresent();
assertTrue(correctPassword);
}
如果不先清除输入内容,就会将其过滤掉-但是用户可能会认为前导空格和尾随空格都构成了输入。因此,在过滤掉不正确的密码之前,我们先将带有映射的脏密码转换为干净的密码。
12.使用flatMap()转换价值
就像map()方法一样,我们也有flatMap()方法作为转换值的替代方法。区别在于,地图仅在展开值时才转换值,而flatMap会在转换值之前采用已包装的值并对其进行解包。
以前,我们创建了简单的String和Integer对象,用于包装在Optional实例中。但是,通常,我们将从复杂对象的访问者那里接收这些对象。
为了更清楚地了解两者之间的区别,让我们看一下一个Person对象,该对象带有一个人的详细信息,例如姓名,年龄和密码等。
package xyz.mrzhangxd.optionalproject;
public class Person {
private String name;
private int age;
private String password;
public Optional<String> getName() {
return Optional.ofNullable(name);
}
public Optional<Integer> getAge() {
return Optional.ofNullable(age);
}
public Optional<String> getPassword() {
return Optional.ofNullable(password);
}
// normal constructors and setters
}
我们通常会创建一个这样的对象并将其包装在Optional对象中,就像处理String一样。或者,可以通过另一个方法调用将其返回给我们.
Person person = new Person("MrZhangxd", 18);
Optional<Person> personOptional = Optional.of(person);
当包装一个Person对象时,它将包含嵌套的Optional实例.
package xyz.mrzhangxd.optionaltest;
import xyz.mrzhangxd.optionalproject.Person;
@Test
public void givenOptional_whenFlatMapWorks_thenCorrect2() {
Person person = new Person("MrZhangxd", 18);
Optional<Person> personOptional = Optional.of(person);
Optional<Optional<String>> nameOptionalWrapper
= personOptional.map(Person::getName);
Optional<String> nameOptional
= nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
String name1 = nameOptional.orElse("");
assertEquals("MrZhangxd", name1);
String name = personOptional
.flatMap(Person::getName)
.orElse("");
assertEquals("MrZhangxd", name);
}
接下来,我们试图检索Person对象的name属性以执行断言。
注意在第三条语句中如何使用map()方法实现此目的,然后注意之后如何使用flatMap()方法来实现此目的。
Person :: getName方法的引用类似于上一节中用于清理密码的String :: trim调用。
唯一的区别是getName()返回的是Optional而不是String,与trim()操作一样。这加上映射转换将结果包装在Optional对象中的事实导致嵌套的Optional。
因此,在使用map()方法时,我们需要在使用转换后的值之前添加一个额外的调用来检索值。这样,可选包装将被删除。使用flatMap时,将隐式执行此操作。
13. Java 8中的可选链接
有时,我们可能需要从多个Optional中获取第一个非空的Optional对象。在这种情况下,使用类似orElseOptional()的方法将非常方便。不幸的是,Java 8不直接支持这种操作。
让我们首先介绍在本节中将要使用的一些方法.
private Optional<String> getEmpty() {
return Optional.empty();
}
private Optional<String> getHello() {
return Optional.of("hello");
}
private Optional<String> getBye() {
return Optional.of("bye");
}
private Optional<String> createOptional(String input) {
if (input == null || "".equals(input) || "empty".equals(input)) {
return Optional.empty();
}
return Optional.of(input);
}
为了链接多个Optional对象并获得Java 8中的第一个非空对象,我们可以使用Stream API
package xyz.mrzhangxd.optionaltest;
@Test
public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturned() {
Optional<String> found = Stream.of(getEmpty(), getHello(), getBye())
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
assertEquals(getHello(), found);
}
这种方法的缺点是,无论Stream中非空Optional出现在何处,我们所有的get方法都始终执行。
如果我们想懒惰地评估传递给Stream.of()的方法,则需要使用方法参考和Supplier接口.
package xyz.mrzhangxd.optionaltest;
@Test
public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturnedAndRestNotEvaluated() {
Optional<String> found =
Stream.<Supplier<Optional<String>>>of(this::getEmpty, this::getHello, this::getBye)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
assertEquals(getHello(), found);
}
如果需要使用带有参数的方法,则必须求助于lambda表达式.
package xyz.mrzhangxd.optionaltest;
@Test
public void givenTwoOptionalsReturnedByOneArgMethod_whenChaining_thenFirstNonEmptyIsReturned() {
Optional<String> found = Stream.<Supplier<Optional<String>>>of(
() -> createOptional("empty"),
() -> createOptional("hello")
)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
assertEquals(createOptional("hello"), found);
}
通常,在所有链接的Optionals为空的情况下,我们通常希望返回一个默认值。我们可以通过添加对orElse()或orElseGet()的调用来做到这一点,如以下示例所示.
package xyz.mrzhangxd.optionaltest;
@Test
public void givenTwoEmptyOptionals_whenChaining_thenDefaultIsReturned() {
String found = Stream.<Supplier<Optional<String>>>of(
() -> createOptional("empty"),
() -> createOptional("empty")
)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElseGet(() -> "default");
assertEquals("default", found);
}
14. JDK 9可选API
Java 9的发行版为Optional API添加了更多新方法
- or()方法,用于提供创建替代项的供应商;
- ifPresentOrElse()方法,如果存在Optional,则允许执行一个动作;否则,则允许另一个动作;
- stream()方法,用于将Optional转换为Stream.
15.滥用Optional
最后,让我们看看使用Optionals的一种诱人但危险的方法:将Optional参数传递给方法。
想象一下,我们有一个“人”列表,并且我们希望有一种方法可以在该列表中搜索具有给定名称的人。另外,如果指定了年龄,我们希望该方法匹配至少具有一定年龄的条目。由于此参数是可选的,因此我们提供了以下方法。
public List<Person> search(List<Person> people, String name, Optional<Integer> age) {
// Null checks for people and name
people.stream()
.filter(p -> p.getName().equals(name))
.filter(p -> p.getAge() >= age.orElse(0))
.collect(Collectors.toList());
}
然后,我们释放我们的方法,让另一个开发人员尝试使用它.
someObject.search(people, "MrZhangxd", null);
现在,开发人员执行其代码并获取NullPointerException。在这里,我们必须对我们的可选参数进行空检查,这在想要避免这种情况时违背了我们的初衷。
我们可能会做一些更好的处理方法。
public List<Person> search(List<Person> people, String name, Integer age) {
// Null checks for people and name
age = age != null ? age : 0;
people.stream()
.filter(p -> p.getName().equals(name))
.filter(p -> p.getAge() >= age)
.collect(Collectors.toList());
}
该参数仍然是可选的,但是我们仅需检查一下即可处理它。另一种可能性是创建两个重载方法.
public List<Person> search(List<Person> people, String name) {
return doSearch(people, name, 0);
}
public List<Person> search(List<Person> people, String name, int age) {
return doSearch(people, name, age);
}
private List<Person> doSearch(List<Person> people, String name, int age) {
// Null checks for people and name
return people.stream()
.filter(p -> p.getName().equals(name))
.filter(p -> p.getAge() >= age)
.collect(Collectors.toList());
}
这样,我们提供了一个清晰的API,其中包含两种可以完成不同任务的方法(尽管它们共享实现)。
因此,有一些解决方案可以避免使用Optionals作为方法参数。发行Optional时,Java的意图是将其用作返回类型,从而表明一种方法可以返回空值。实际上,某些代码检查人员甚至不建议使用Optional作为方法参数。
16.总结
在本篇博客中,主要介绍了Java 8 Optional类的大多数重要功能。
主要还简要探讨了为什么选择使用Optional而不是显式的空检查和输入验证的一些原因。
我们主要学习了如何使用get()或orElse()和orElseGet()方法获取Optional或默认值(如果为空)的值(并了解了两者之间的重要区别)。
然后,我们了解了如何使用map(),flatMap()和filter()转换或过滤Optional。
我们看到了流畅的API Optional提供了什么,因为它使我们能够轻松地链接不同的方法。
最后,我们看到了如何使用Optionals作为方法参数是一个坏主意,以及如何避免使用它。