JDK1.8 Supplier實(shí)踐及總結(jié)

起因

群里黑神拋出了一個(gè)問題憨琳,意圖引起大家的思考


image.png

黑神簡單解釋之后非驮,群里仍有同學(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ì)思考一下:

  1. 我們生成的實(shí)例對象并不包含復(fù)雜的屬性,很輕量,一次分配不需要占用太多空間
  2. 代碼所在方法的生命周期一般比較短紫皇,符合朝生夕死的特點(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)該是code1sku或者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ì)算(懶加載)的效果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市备徐,隨后出現(xiàn)的幾起案子萄传,更是在濱河造成了極大的恐慌,老刑警劉巖蜜猾,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秀菱,死亡現(xiàn)場離奇詭異振诬,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)衍菱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門赶么,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人梦碗,你說我怎么就攤上這事禽绪。” “怎么了洪规?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵印屁,是天一觀的道長。 經(jīng)常有香客問我斩例,道長雄人,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任念赶,我火速辦了婚禮础钠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘叉谜。我一直安慰自己旗吁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布停局。 她就那樣靜靜地躺著很钓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪董栽。 梳的紋絲不亂的頭發(fā)上码倦,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天,我揣著相機(jī)與錄音锭碳,去河邊找鬼袁稽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛擒抛,可吹牛的內(nèi)容都是我干的推汽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼歧沪,長吁一口氣:“原來是場噩夢啊……” “哼歹撒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起槽畔,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤栈妆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鳞尔,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嬉橙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寥假。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片市框。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖糕韧,靈堂內(nèi)的尸體忽然破棺而出枫振,到底是詐尸還是另有隱情,我是刑警寧澤萤彩,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布粪滤,位于F島的核電站,受9級特大地震影響雀扶,放射性物質(zhì)發(fā)生泄漏杖小。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一愚墓、第九天 我趴在偏房一處隱蔽的房頂上張望予权。 院中可真熱鬧,春花似錦浪册、人聲如沸扫腺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽笆环。三九已至,卻和暖如春煞肾,著一層夾襖步出監(jiān)牢的瞬間咧织,已是汗流浹背嗓袱。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工籍救, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人渠抹。 一個(gè)月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓蝙昙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親梧却。 傳聞我的和親對象是個(gè)殘疾皇子奇颠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359