前言
Java中空指針異常(NPE)一直是令開發者頭疼的問題。Java 8引入了一個新的Optional類,使用該類可以盡可能地防止出現空指針異常。
Optional 類是一個可以為null的容器對象。如果值存在則isPresent()方法會返回true,調用get()方法會返回該對象。Optional提供很多有用的方法,這樣開發者就不必顯式進行空值檢測。
本文將介紹Optional類包含的方法,並通過示例詳細展示其用法。
一、基礎知識
1.1 Optional類方法
本節基於作者的實踐,給出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)原則,即
帶有子類限定的可從泛型讀取,帶有超類限定的可從泛型寫入
Function
、Supplier
和Consumer
均為函數式接口,支持Lambda表達式。
1.2 Lambda表達式與方法引用
標准的Lambda表達式語法結構如下:
(參數列表) -> {方法體}
只有一個參數時,可省略小括號;當方法體只有一條語句時,可省略大括號和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的推薦用法。考慮到代碼簡潔度,示例中盡量使用方法引用。
2.1 map + orElse
功能描述:判斷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的寫法,邏輯層次一目了然。似無判空,勝卻判空,盡在不言中。
2.2 map + orElseThrow
功能描述:判斷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"))
}
2.3 map + orElseGet
功能描述:判斷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);
}
2.4 map + filter + ifPresent
功能描述:當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()
用於無需返回值的情況。
2.5 Optional + Stream
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!");
});
}
2.6 測試與輸出
測試代碼及其輸出如下:
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)
}
}
2.7 真實項目代碼
原始實現如下:
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; }