Java中空指针异常(NPE)一直是令开发者头疼的问题。Java 8引入了一个新的Optional类,使用该类能够尽量地防止出现空指针异常。html
Optional 类是一个能够为null的容器对象。若是值存在则isPresent()方法会返回true,调用get()方法会返回该对象。Optional提供不少有用的方法,这样开发者就没必要显式进行空值检测。java
本文将介绍Optional类包含的方法,并经过示例详细展现其用法。git
本节基于做者的实践,给出Optional类经常使用的方法(其余方法不推荐使用):正则表达式
方法 | 描述 |
---|---|
static
|
为指定的value建立一个Optional。若value为null,则返回空的Optional |
Optional map(Function<? super T, ? extends U> mapper) | 如有值,则对其执行调用mapper映射函数获得返回值。若返回值不为 null,则建立包含映射返回值的Optional做为map方法返回值,不然返回空Optional |
T orElse(T other) | 若存在该值则将其返回, 不然返回 other |
T orElseGet(Supplier<? extends T> other) | 若存在该值则将其返回,不然触发 other,并返回 other 调用的结果。注意,该方法为惰性计算 |
void ifPresent(Consumer<? super T> consumer) | 若Optional实例有值则为其调用consumer,不然不作处理 |
Optional
|
如有值而且知足断言条件返回包含该值的Optional,不然返回空Optional |
|
若存在该值则将其返回,不然抛出由 Supplier 继承的异常 |
其中,map()
方法的? super T
表示泛型 T 或其父类,? extend U
表示泛型U或其子类。泛型的上限和下限遵循PECS(Producer Extends Consumer Super)原则,即swift
带有子类限定的可从泛型读取,带有超类限定的可从泛型写入数组
Function
、Supplier
和Consumer
均为函数式接口,支持Lambda表达式。oracle
标准的Lambda表达式语法结构以下:app
(参数列表) -> {方法体}ide
只有一个参数时,可省略小括号;当方法体只有一条语句时,可省略大括号和return关键字。函数
详细的Lambda语法介绍可参考深刻理解Java8 Lambda表达式。
若是Lambda表达式里只调用了一个方法,还可使用Java 8新增的方法引用(method reference
)写法,以提高编码简洁度。方法引用有以下四种类型:
类别 | 方法引用格式 | 等效的lambda表达式 |
---|---|---|
静态方法的引用 | Class::staticMethod | (args) -> Class.staticMethod(args) |
对特定对象的实例方法的引用 | object::instanceMethod | (args) -> obj.instanceMethod(args) |
对特定类型任意对象的实例方法的引用 | Class::instanceMethod | (obj, args) -> obj.instanceMethod(args) |
构造方法的引用 | ClassName::new | (args) -> new ClassName(args) |
例如:System.out::println等同于x->System.out.println(x),String::toLowerCase等同于x->x.toLowerCase(),BigDecimal::new等同于x->new BigDecimal(x)。
详情也可参考Method References或Java 8 Method Reference: How to Use it。
为充分体现Optional类的“威力”,首先以组合方式定义Location
和Person
两个类。
Location类:
public class Location { private String country; private String city; public Location(String country, String city) { this.country = country; this.city = city; } public void setCountry(String country) { this.country = country; } public void setCity(String city) { this.city = city; } public String getCountry() { return country; } public String getCity() { return city; } public static void introduce(String country) { System.out.println("I'm from " + country + "."); } }
Person类:
public class Person { private String name; private String gender; private int age; private Location location; public Person() { } public Person(String name, String gender, int age) { this.name = name; this.gender = gender; this.age = age; } public void setName(String name) { this.name = name; } public void setGender(String gender) { this.gender = gender; } public void setAge(int age) { this.age = age; } public Person setLocation(String country, String city) { this.location = new Location(country, city); return this; } public String getName() { return name; } public String getGender() { return gender; } public int getAge() { return age; } public Location getLocation() { return location; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + '}'; } public void greeting(Person person) { System.out.println("Hello " + person.getName() + "!"); } public static void showIdentity(Person person) { System.out.println("Person: {" + "name='" + person.getName() + '\'' + ", gender='" + person.getGender() + '\'' + ", age=" + person.getAge() + '}'); } }
注意,以上两个类仅做演示示例用,并不表明规范写法。例如,Person类所提供的构造方法未包含location
参数,而是经过setLocation()
方法间接设置。这是为了简化Person对象初始化及构造location
为null的状况。此外,greeting()
做为实例方法,却未访问任何实例字段。
下文将基于Location
和Person
类,展现Optional的推荐用法。考虑到代码简洁度,示例中尽可能使用方法引用。
功能描述:判断Person在哪一个城市,并返回城市小写名;失败时返回nowhere。
传统写法1:
public static String inWhichCityLowercaseTU(Person person) { //Traditional&Ugly if (person != null) { Location location = person.getLocation(); if (location != null) { String city = location.getCity(); if (city != null) { return city.toLowerCase(); } else { return "nowhere"; } } else { return "nowhere"; } } else { return "nowhere"; } }
可见,层层嵌套,繁琐且易错。
传统写法2:
public static String inWhichCityLowercaseT(Person person) { //Traditional if (person != null && person.getLocation() != null && person.getLocation().getCity() != null) { return person.getLocation().getCity().toLowerCase(); } return "nowhere"; }
这种写法优于前者,但级联判空很容易"淹没"正常逻辑(return句)。
新式写法:
public static String inWhichCityLowercase(final Person person) { return Optional.ofNullable(person) .map(Person::getLocation) .map(Location::getCity) .map(String::toLowerCase) .orElse("nowhere"); }
采用Optional的写法,逻辑层次一目了然。似无判空,胜却判空,尽在不言中。
功能描述:判断Person在哪一个国家,并返回国家大写名;失败时抛出异常。
传统写法相似上节,新式写法以下:
public static String inWhichCountryUppercase(final Person person) { return Optional.ofNullable(person) .map(Person::getLocation) .map(Location::getCountry) .map(String::toUpperCase) .orElseThrow(NoSuchElementException::new); // 或orElseThrow(() -> new NoSuchElementException("No country information")) }
功能描述:判断Person在哪一个国家,并返回from + 国家名;失败时返回from Nowhere。
新式写法:
private String fromCountry(final String country) { return "from " + country; } private String fromNowhere() { return "from Nowhere"; } private String fromWhere(final Person person) { return Optional.ofNullable(person) .map(Person::getLocation) .map(Location::getCountry) .map(this::fromCountry) .orElseGet(this::fromNowhere); }
功能描述:当Person在中国时,调用Location.introduce()
;不然什么都不作。
传统写法:
public static void introduceChineseT(final Person person) { if (person != null && person.getLocation() != null && person.getLocation().getCountry() != null && "China".equals(person.getLocation().getCountry())) { Location.introduce("China"); } }
新式写法:
public static void introduceChinese(final Person person) { Optional.ofNullable(person) .map(Person::getLocation) .map(Location::getCountry) .filter("China"::equals) .ifPresent(Location::introduce); }
注意,ifPresent()
用于无需返回值的状况。
Optional也可与Java 8的Stream特性共用,例如:
private static void optionalWithStream() { Stream<String> names = Stream.of("Zhou Yi", "Wang Er", "Wu San"); Optional<String> preWithL = names .filter(name -> name.startsWith("Wang")) .findFirst(); preWithL.ifPresent(name -> { String u = name.toUpperCase(); System.out.println("Get " + u + " with family name Wang!"); }); }
测试代码及其输出以下:
public class OptionalDemo { //methods from 2.1 to 2.5 public static void main(String[] args) { optionalWithStream(); // 输出:Get WANG ER with family name Wang! Person person = new Person(); //fetchPersonFromSomewhereElse() System.out.println(new OptionalDemo().fromWhere(person)); // 输出:from Nowhere List<Person> personList = new ArrayList<>(); Person mike = new Person("mike", "male", 10).setLocation("China", "Nanjing"); personList.add(mike); System.out.println(inWhichCityLowercase(mike)); // 输出:nanjing Person lucy = new Person("lucy", "female", 4); personList.add(lucy); personList.forEach(lucy::greeting); // 输出:Hello mike!\nHello lucy! // 注意,此处仅为展现object::instanceMethod写法 personList.forEach(Person::showIdentity); // 输出:Person: {name='mike', gender='male', age=10} // Person: {name='lucy', gender='female', age=4} personList.forEach(OptionalDemo::introduceChinese); // 输出:I'm from China. System.out.println(inWhichCountryUppercase(lucy)); // 输出:Exception in thread "main" java.util.NoSuchElementException // at java.util.Optional.orElseThrow(Optional.java:290) // at com.huawei.vmf.adapter.inventory.OptionalDemo.inWhichCountryUppercase(OptionalDemo.java:47) // at com.huawei.vmf.adapter.inventory.OptionalDemo.main(OptionalDemo.java:108) } }
原始实现以下:
public String makeDevDetailVersion(final String strDevVersion, final String strDevDescr, final String strDevPlatformName) { String detailVer = "VRP"; if (null != strDevPlatformName && !strDevPlatformName.isEmpty()) { detailVer = strDevPlatformName; } String versionStr = null; Pattern verStrPattern = Pattern.compile("Version(\\s)*([\\d]+[\\.][\\d]+)"); if(strDevDescr != null) { Matcher verStrMatcher = verStrPattern.matcher(strDevDescr); if (verStrMatcher.find()) { versionStr = verStrMatcher.group(); } if (null != versionStr) { Pattern digitalPattern = Pattern.compile("([\\d]+[\\.][\\d]+)"); Matcher digitalMatcher = digitalPattern.matcher(versionStr); if (digitalMatcher.find()) { detailVer = detailVer + digitalMatcher.group() + " "; } } } return detailVer + strDevVersion; }
采用Optional类改写以下(正则匹配部分略有修改):
private static final Pattern VRP_VER = Pattern.compile("Version\\s+(\\d\\.\\d{3})\\s+"); private static String makeDetailedDevVersion(final String strDevVersion, final String strDevDescr, final String strDevPlatformName) { String detailVer = Optional.ofNullable(strDevPlatformName) .filter(s -> !s.isEmpty()).orElse("VRP"); return detailVer + Optional.ofNullable(strDevDescr) .map(VRP_VER::matcher) .filter(Matcher::find) .map(m -> m.group(1)) .map(v -> v + " ").orElse("") + strDevVersion; }
使用Optional时,需注意如下规则:
Optional的包装和访问都有成本,所以不适用于一些特别注重性能和内存的场景。
不要将null赋给Optional,应赋以Optional.empty()
。
避免调用isPresent()和get()方法,而应使用ifPresent()
、orElse()
、 orElseGet()
和orElseThrow()
。举一isPresent()
用法示例:
private static boolean isIntegerNumber(String number) { number = number.trim(); String intNumRegex = "\\-{0,1}\\d+"; if (number.matches(intNumRegex)) { return true; } else { return false; } } // Optional写法1(含NPE修复及正则表达式优化) private static boolean isIntegerNumber1(String number) { return Optional.ofNullable(number) .map(String::trim) .filter(n -> n.matches("-?\\d+")) .isPresent(); } // Optional写法2(含NPE修复及正则表达式优化,不用isPresent) private static boolean isIntegerNumber2(String number) { return Optional.ofNullable(number) .map(String::trim) .map(n -> n.matches("-?\\d+")) .orElse(false); }
Optional应该只用处理返回值,而不该做为类的字段(Optional类型不可被序列化)或方法(包括constructor)的参数。
不要为了链式方法而使用Optional,尤为是在仅仅获取一个值时。例如:
// good return variable == null ? "blablabla" : variable; // bad return Optional.ofNullable(variable).orElse("blablabla"); // bad Optional.ofNullable(someVariable).ifPresent(this::blablabla)
滥用Optional不只影响性能,可读性也不高。应尽量避免使用null引用。
避免使用Optional返回空的集合或数组,而应返回Collections.emptyList()
、emptyMap()
、emptySet()
或new Type[0]
。注意不要返回null,以便调用者能够省去繁琐的null检查。
避免在集合中使用Optional,应使用getOrDefault()
或computeIfAbsent()
等集合方法。
针对基本类型,使用对应的OptionalInt
、OptionalLong
和OptionalDouble
类。
切忌过分使用Optional,不然可能使代码难以阅读和维护。
常见的问题是Lambda表达式过长,例如:
private Set<String> queryValidUsers() { Set<String> userInfo = new HashSet<String>(10); Optional.ofNullable(toJSonObject(getJsonStrFromSomewhere())) .map(cur -> cur.optJSONArray("data")) .map(cur -> { // 大段代码割裂了"思路" for (int i = 0; i < cur.length(); i++) { JSONArray users = cur.optJSONObject(i).optJSONArray("users"); if (null == users || 0 == users.length()) { continue; } for (int j = 0; j < users.length(); j++) { JSONObject userObj = users.optJSONObject(j); if (!userObj.optBoolean("stopUse")) { // userObj可能为null! userInfo.add(userObj.optString("userId")); } } } return userInfo; }); return userInfo; }
经过简单的抽取方法,可读性获得很大提升:
private Set<String> queryValidUsers() { return Optional.ofNullable(toJSonObject(getJsonStrFromSomewhere())) .map(cur -> cur.optJSONArray("data")) .map(this::collectNonStopUsers) .orElse(Collections.emptySet()); } private Set<String> collectNonStopUsers(JSONArray dataArray) { Set<String> userInfo = new HashSet<String>(10); for (int i = 0; i < dataArray.length(); i++) { JSONArray users = dataArray.optJSONObject(i).optJSONArray("users"); Optional.ofNullable(users).ifPresent(cur -> { for (int j = 0; j < cur.length(); j++) { Optional.ofNullable(cur.optJSONObject(j)) .filter(user -> !user.optBoolean("stopUse")) .ifPresent(user -> userInfo.add(user.optString("userId"))); } }); } return userInfo; }