Java 8 Optional類使用的實踐經驗

前言

Java中空指針異常(NPE)一直是令開發者頭疼的問題。Java 8引入了一個新的Optional類,使用該類能夠儘量地防止出現空指針異常。html

Optional 類是一個能夠爲null的容器對象。若是值存在則isPresent()方法會返回true,調用get()方法會返回該對象。Optional提供不少有用的方法,這樣開發者就沒必要顯式進行空值檢測。java

本文將介紹Optional類包含的方法,並經過示例詳細展現其用法。git


1、基礎知識

1.1 Optional類方法

本節基於做者的實踐,給出Optional類經常使用的方法(其餘方法不推薦使用):正則表達式

方法 描述
static Optional ofNullable(T value) 爲指定的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 filter(Predicate<? super T> predicate) 如有值而且知足斷言條件返回包含該值的Optional,不然返回空Optional
T orElseThrow(Supplier<? extends X> exceptionSupplier) 若存在該值則將其返回,不然拋出由 Supplier 繼承的異常

其中,map()方法的? super T表示泛型 T 或其父類,? extend U表示泛型U或其子類。泛型的上限和下限遵循PECS(Producer Extends Consumer Super)原則,即swift

帶有子類限定的可從泛型讀取,帶有超類限定的可從泛型寫入數組

FunctionSupplierConsumer均爲函數式接口,支持Lambda表達式。oracle

1.2 Lambda表達式與方法引用

標準的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 ReferencesJava 8 Method Reference: How to Use it

2、用法示例

爲充分體現Optional類的「威力」,首先以組合方式定義LocationPerson兩個類。

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()做爲實例方法,卻未訪問任何實例字段。

下文將基於LocationPerson類,展現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;
}

3、規則總結

使用Optional時,需注意如下規則:

  1. Optional的包裝和訪問都有成本,所以不適用於一些特別注重性能和內存的場景。

  2. 不要將null賦給Optional,應賦以Optional.empty()

  3. 避免調用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);
    }
  4. Optional應該只用處理返回值,而不該做爲類的字段(Optional類型不可被序列化)或方法(包括constructor)的參數。

  5. 不要爲了鏈式方法而使用Optional,尤爲是在僅僅獲取一個值時。例如:

    // good
    return variable == null ? "blablabla" : variable;
    // bad
    return Optional.ofNullable(variable).orElse("blablabla");
    // bad
    Optional.ofNullable(someVariable).ifPresent(this::blablabla)

    濫用Optional不只影響性能,可讀性也不高。應儘量避免使用null引用。

  6. 避免使用Optional返回空的集合或數組,而應返回Collections.emptyList()emptyMap()emptySet()new Type[0]。注意不要返回null,以便調用者能夠省去繁瑣的null檢查。

  7. 避免在集合中使用Optional,應使用getOrDefault()computeIfAbsent()等集合方法。

  8. 針對基本類型,使用對應的OptionalIntOptionalLongOptionalDouble類。

  9. 切忌過分使用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;
    }
相關文章
相關標籤/搜索