起因
群里黑神拋出了一個(gè)問題憨琳,意圖引起大家的思考
黑神簡單解釋之后非驮,群里仍有同學(xué)不太理解
正好之前筆者在Supplier上有一些實(shí)踐逞敷,因此打算跟大家分享一下使用經(jīng)驗(yàn)
基礎(chǔ)知識
JDK1.8為我們提供了一個(gè)函數(shù)接口Supplier
必怜,先來看一下它的接口定義
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
從接口的定義可以看出养涮,它代表了這樣的一類函數(shù):無入?yún)ⅲ幸粋€(gè)返回值罐监。
接口越簡單吴藻,看的越糊涂,這代表了什么含義弓柱?如此簡單的接口沟堡,存在的必要性是什么侧但?
接著再看下該接口的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的描述,更是讓人云里霧里
實(shí)踐
為了代入場景航罗,直接用大家開發(fā)過程中經(jīng)常能碰到禀横,但稍不注意卻會掉坑里的問題做為案例進(jìn)行講解。
案例一
首先思考一個(gè)問題:如何輸出日志粥血?(So easy)
log.info("print info log");
接著柏锄,如何輸出調(diào)試日志(debug)?(So easy)
log.debug("print debug log");
測試(開發(fā))環(huán)境與線上環(huán)境的日志級別一般不同复亏。測試環(huán)境為了調(diào)試趾娃,一般會開啟debug
級別,輸出一些調(diào)試信息便于問題排查蜓耻;而線上環(huán)境一般是處于穩(wěn)定狀態(tài),不太需要輸出調(diào)試信息械巡,再出于性能考慮刹淌,一般會開啟info
級別,過濾掉debug
日志讥耗。
再接著有勾,如果輸出的日志里,不再僅僅是簡單的句子古程,而有時(shí)候需要包含一個(gè)對象(例如遠(yuǎn)程調(diào)用的入?yún)ā⒊鰠?,怎么辦挣磨?
log.debug("invoke remote method, return value: {}", JSON.toJSONString(returnVal));
稍一疏忽雇逞,很容易寫出上述代碼(大家可以搜一下自己負(fù)責(zé)的項(xiàng)目,看看是否到處充斥這樣的代碼)茁裙,究其原因塘砸,是被log.debug()
的外表所欺騙與迷惑:log.debug()
只會在開啟debug
級別的日志下輸出日志,而線上日志級別是info
晤锥,不會輸出掉蔬,因此沒有性能問題。
誠然矾瘾,在開啟info
級別時(shí)女轿,這條日志并不會輸出,但這里容易被忽視的點(diǎn)是壕翩,無論開啟何種日志級別蛉迹,JSON.toJSONString(returnVal)
這段代碼都會首先被執(zhí)行,返回值做為log.debug
入?yún)⒑蠓怕瑁艜鶕?jù)日志級別判斷是否輸出日志婿禽。也即是說赏僧,即便最終判斷不輸出日志,也會執(zhí)行一遍序列化方法扭倾。這在被序列化對象很大的時(shí)候淀零,容易造成性能問題。(曾經(jīng)見過輸出一屏都裝不下的日志膛壹,序列化耗時(shí)50-70ms)
如何解決驾中?
if (log.isDebugEnabled()) {
log.debug("invoke remote method, return value: {}", JSON.toJSONString(returnVal));
}
即先判斷,再輸出
但是程序員天性懶惰(懶惰是科技進(jìn)步的動力)模聋,原來一行代碼能解決的事肩民,現(xiàn)在三行代碼才能完成,不能忍傲捶健持痰!而且如果需要輸出的調(diào)試日志有很多,就會出現(xiàn)滿屏if(log.isDebugEnabled())
祟蚀,代碼會很丑陋工窍,閱讀代碼時(shí)候很容易被干擾正常邏輯
解決方案:Supplier
首先定義一個(gè)Lazy
類,用于延遲計(jì)算(懶加載)
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();
}
}
這時(shí)候前酿,日志的輸出就變成了
log.debug("invoke remote method, return value: {}", Lazy.of(() -> JSON.toJSONString(returnVal)));
一行代碼患雏,實(shí)現(xiàn)了原來三行代碼才能實(shí)現(xiàn)的功能:判斷是否滿足輸出條件,滿足罢维,則執(zhí)行計(jì)算
淹仑,即延遲計(jì)算--->序列化;不滿足肺孵,則不計(jì)算匀借,不執(zhí)行序列化。
以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;
}
// 滿足輸出條件怀吻,才會執(zhí)行Lazy.toString(),即supplier.get().toString()
buildLoggingEventAndAppend(localFQCN, marker, level, msg, new Object[] { param }, t);
}
每次執(zhí)行這一行代碼初婆,會生成一個(gè)Supplier實(shí)例(Lazy)蓬坡,并做為log.debug
入?yún)ⅲ?code>log.debug中進(jìn)行判斷決定是否要使用該Lazy磅叛,即調(diào)用Lazy.toString()屑咳,如此便達(dá)到了延遲計(jì)算的效果。
只談優(yōu)點(diǎn)不談缺點(diǎn)有耍流氓的嫌疑:很顯然弊琴,每次執(zhí)行會生成一個(gè)Supplier
實(shí)例兆龙。但是我們仔細(xì)思考一下:
- 我們生成的實(shí)例對象并不包含復(fù)雜的屬性,很輕量,一次分配不需要占用太多空間
- 代碼所在方法的生命周期一般比較短紫皇,符合朝生夕死的特點(diǎn)
實(shí)例對象因此會在TLAB或者Young Gen上被分配慰安,并且?guī)缀鯖]有機(jī)會晉升到Old Gen就會被回收。
因此聪铺,這個(gè)缺點(diǎn)也就不復(fù)存在化焕。
案例二
// code1
Long price = Optional.ofNullable(sku)
.map(Sku::getPrice)
.orElse(0L);
// code2
Long price = Optional.ofNullable(sku)
.map(Sku::getPrice)
.orElseGet(() -> 0L);
Optional
作為一種判空的優(yōu)雅解決方案,會在我們的日常開發(fā)中經(jīng)常使用到铃剔,上面兩種寫法撒桨,使用更多的應(yīng)該是code1
:sku
或者sku.price
中只要任意一個(gè)為空,最終價(jià)格都為0
;code2
寫法键兜,在這種情況下凤类,會顯得很雞肋,而且也不好理解普气,為什么有了orElse
方法谜疤,還額外提供一個(gè)orElseGet
方法。
再看下面兩種方式现诀,稍稍有些區(qū)別
// 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中加載對象企锌。
這時(shí)候榆浓,orElse
或者orElseGet
里提供的對象于未,不再是一個(gè)簡單的數(shù)值,而是一個(gè)需要經(jīng)過計(jì)算的對象(言外之意:有額外的加載成本)陡鹃。orElseGet
在此處的作用顯而易見:code3
中烘浦,無論什么情況,都會執(zhí)行一遍selectFromDB
方法萍鲸,而code4
只有緩存為空或過濾之后為空闷叉,才會執(zhí)行selectFromDB
方法,即延遲計(jì)算(懶加載)脊阴。
總結(jié)
Supplier提供了一種包裹代碼的能力握侧,被包裹的代碼并非實(shí)時(shí)執(zhí)行,而是在真正需要使用的時(shí)候嘿期,被包裹代碼段才會被執(zhí)行品擎,實(shí)現(xiàn)延遲計(jì)算(懶加載)的效果