JDK1.8 Supplier實踐及總結

起因

羣裏黑神拋出了一個問題,意圖引起大家的思考
image.png
黑神簡單解釋之後,羣裏仍有同學不太理解

正好之前筆者在Supplier上有一些實踐,因此打算跟大家分享一下使用經驗

基礎知識

JDK1.8爲我們提供了一個函數接口Supplier,先來看一下它的接口定義

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

從接口的定義可以看出,它代表了這樣的一類函數:無入參,有一個返回值
接口越簡單,看的越糊塗,這代表了什麼含義?如此簡單的接口,存在的必要性是什麼?

接着再看下該接口的java doc描述

Represents a supplier of results.
There is no requirement that a new or distinct result be returned each time the supplier is invoked.
This is a functional interface whose functional method is get().

java doc的描述,更是讓人云裏霧裏

實踐

爲了代入場景,直接用大家開發過程中經常能碰到,但稍不注意卻會掉坑裏的問題做爲案例進行講解。

案例一

首先思考一個問題:如何輸出日誌?(So easy)

log.info("print info log");

接着,如何輸出調試日誌(debug)?(So easy)

log.debug("print debug log");

測試(開發)環境與線上環境的日誌級別一般不同。測試環境爲了調試,一般會開啓debug級別,輸出一些調試信息便於問題排查;而線上環境一般是處於穩定狀態,不太需要輸出調試信息,再出於性能考慮,一般會開啓info級別,過濾掉debug日誌。

再接着,如果輸出的日誌裏,不再僅僅是簡單的句子,而有時候需要包含一個對象(例如遠程調用的入參、出參),怎麼辦?

log.debug("invoke remote method, return value: {}", JSON.toJSONString(returnVal));

稍一疏忽,很容易寫出上述代碼(大家可以搜一下自己負責的項目,看看是否到處充斥這樣的代碼),究其原因,是被log.debug()的外表所欺騙與迷惑:log.debug()只會在開啓debug級別的日誌下輸出日誌,而線上日誌級別是info,不會輸出,因此沒有性能問題。

誠然,在開啓info級別時,這條日誌並不會輸出,但這裏容易被忽視的點是,無論開啓何種日誌級別,JSON.toJSONString(returnVal)這段代碼都會首先被執行,返回值做爲log.debug入參後,纔會根據日誌級別判斷是否輸出日誌。也即是說,即便最終判斷不輸出日誌,也會執行一遍序列化方法。這在被對象很大的時候,容易造成性能問題。(曾經見過輸出一屏都裝不下的日誌,序列化耗時50-70ms)

如何解決?

if (log.isDebugEnabled()) {
    log.debug("invoke remote method, return value: {}", JSON.toJSONString(returnVal));
}

即先判斷,再輸出

但是程序員天性懶惰(懶惰是科技進步的動力),原來一行代碼能解決的事,現在三行代碼才能完成,不能忍啊!而且如果需要輸出的調試日誌有很多,就會出現滿屏if (log.isDebugEnabled()),代碼會很醜陋,閱讀代碼時候很容易被幹擾正常邏輯

解決方案:Supplier

首先定義一個Lazy類,用於延遲計算(懶加載)

public class Lazy<T> implements Supplier<T> {
    private Supplier<T> supplier;

    public static <T> Lazy<T> of(Supplier<T> supplier) {
        Objects.requireNonNull(supplier, "supplier is null");
        if (supplier instanceof Lazy) {
            return (Lazy) supplier;
        } else {
            return new Lazy(supplier);
        }
    }

    private Lazy(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    @Override
    public T get() {
        return supplier.get();
    }

    @Override
    public String toString() {
        return supplier.get().toString();
    }
}

這時候,日誌的輸出就變成了

log.debug("invoke remote method, return value: {}", Lazy.of(() -> JSON.toJSONString(returnVal)));

一行代碼,實現了原來三行代碼才能實現的功能:判斷是否滿足輸出條件,滿足,則執行計算,即延遲計算—>序列化;不滿足,則不計算,不執行序列化。

以Logback中的源碼爲例

public void debug(String format, Object arg) {
    filterAndLog_1(FQCN, null, Level.DEBUG, format, arg, null);
}

private void filterAndLog_1(final String localFQCN, final Marker marker, final Level level, final String msg, final Object param, final Throwable t) {

    final FilterReply decision = loggerContext.getTurboFilterChainDecision_1(marker, this, level, msg, param, t);

    if (decision == FilterReply.NEUTRAL) {
    	// 不滿足輸出條件,直接返回
        if (effectiveLevelInt > level.levelInt) {
            return;
        }
    } else if (decision == FilterReply.DENY) {
        return;
    }

    // 滿足輸出條件,纔會執行Lazy.toString(),即supplier.get().toString()
    buildLoggingEventAndAppend(localFQCN, marker, level, msg, new Object[] { param }, t);
}

每次執行這一行代碼,會生成一個Supplier實例(Lazy),並做爲log.debug入參,在log.debug中進行判斷決定是否要使用該Lazy,即調用Lazy.toString(),如此便達到了延遲計算的效果。

只談優點不談缺點有耍流氓的嫌疑:很顯然,每次執行會生成一個Supplier實例。但是我們仔細思考一下:

  1. 我們生成的實例對象並不包含複雜的屬性,很輕量,一次分配不需要佔用太多空間
  2. 代碼所在方法的生命週期一般比較短,符合朝生夕死的特點

實例對象因此會在TLAB或者Young Gen上被分配,並且幾乎沒有機會晉升到Old Gen就會被回收。
因此,這個缺點也就不復存在。

案例二
// code1
Long price = Optional.ofNullable(sku)
                .map(Sku::getPrice)
                .orElse(0L);

// code2
Long price = Optional.ofNullable(sku)
        .map(Sku::getPrice)
        .orElseGet(() -> 0L);

Optional作爲一種判空的優雅解決方案,會在我們的日常開發中經常使用到,上面兩種寫法,使用更多的應該是code1sku或者sku.price中只要任意一個爲空,最終價格都爲0;code2寫法,在這種情況下,會顯得很雞肋,而且也不好理解,爲什麼有了orElse方法,還額外提供一個orElseGet方法。

再看下面兩種方式,稍稍有些區別

// code3
Object object = Optional.ofNullable(getFromCache())
                .filter(obj -> validate(obj))
                .orElse(selectFromDB()); // here

// code4
Object object = Optional.ofNullable(getFromCache())
        .filter(obj -> validate(obj))
        .orElseGet(() -> selectFromDB()); // here
// Optional
public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

含義是:先從緩存中獲取對象,然後做一下過濾,如果緩存爲空或者過濾之後爲空,就重新從DB中加載對象。

這時候,orElse或者orElseGet裏提供的對象,不再是一個簡單的數值,而是一個需要經過計算的對象(言外之意:有額外的加載成本)。orElseGet 在此處的作用顯而易見:code3中,無論什麼情況,都會執行一遍selectFromDB方法,而code4只有緩存爲空或過濾之後爲空,纔會執行selectFromDB方法,即延遲計算(懶加載)。

總結

Supplier提供了一種包裹代碼的能力,被包裹的代碼並非實時執行,而是在真正需要使用的時候,被包裹代碼段纔會被執行,實現延遲計算(懶加載)的效果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章