起因
羣裏黑神拋出了一個問題,意圖引起大家的思考
黑神簡單解釋之後,羣裏仍有同學不太理解
正好之前筆者在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
實例。但是我們仔細思考一下:
- 我們生成的實例對象並不包含複雜的屬性,很輕量,一次分配不需要佔用太多空間
- 代碼所在方法的生命週期一般比較短,符合朝生夕死的特點
實例對象因此會在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
作爲一種判空的優雅解決方案,會在我們的日常開發中經常使用到,上面兩種寫法,使用更多的應該是code1
:sku
或者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提供了一種包裹代碼的能力,被包裹的代碼並非實時執行,而是在真正需要使用的時候,被包裹代碼段纔會被執行,實現延遲計算(懶加載)的效果