最全 Java 8 講解【建議收藏,反復(fù)研讀】

轉(zhuǎn)載自:https://blog.csdn.net/qq_30757161/article/details/108079000

一、基礎(chǔ)知識

1)為什么要學(xué)習(xí) Java8

Java 8 所做的改變,在許多方面比Java 歷史上任何一次改變都更加深遠(yuǎn),這些改變會(huì)讓你的編程更加容易

例子:

傳統(tǒng)寫法

List?personList = Arrays.asList(newPerson(21,50),newPerson(22,55),newPerson(23,60));Collections.sort(personList,newComparator() {@Overridepublicintcompare(Person o1, Person o2){returno1.getWeight().compareTo(o2.getWeight());? ? }});復(fù)制代碼

Java 8寫法

personList.sort(Comparator.comparing(Person::getWeight));復(fù)制代碼

熟悉 Linux 操作的同學(xué)對這個(gè)指令應(yīng)該不默認(rèn)

cat testFile | tr "[A-Z]" "[a-z]" | sort | tail -3

這種操作便是基于流式操作,cat會(huì)把文件轉(zhuǎn)換創(chuàng)建成一個(gè)流,然后tr會(huì)轉(zhuǎn)換流中字符,sort會(huì)對流中的行進(jìn)行排序,tail -3則會(huì)輸出流的最后三行。這種就像是流水線操作,經(jīng)過每個(gè)中轉(zhuǎn)站,將處理完的結(jié)果轉(zhuǎn)入下一個(gè)處理中心,最后得到最終結(jié)果。

Java 8 的第一個(gè)編程思想就是流處理,流式一系列數(shù)據(jù)項(xiàng),一次只生成一項(xiàng),程序可以從輸入流中一個(gè)一個(gè)讀取數(shù)據(jù)項(xiàng),然后以同樣的方式將數(shù)據(jù)項(xiàng)寫入輸出流。一個(gè)程序的輸出流很可能就是另一個(gè)程序的輸入流。

函數(shù)傳遞

已知一個(gè)集合中有以下幾種花:

List?flowerList = Arrays.asList(newFlower("red",6),newFlower("yellow",7),newFlower("pink",8));復(fù)制代碼

這個(gè)時(shí)候如果我想要紅花,那么傳統(tǒng)寫法是這樣子的:

List?resList =newArrayList<>();for(Flower flower : flowerList) {if(StringUtils.equals("red", flower.getColor())) {? ? ? ? resList.add(flower);? ? }}復(fù)制代碼

那么如果我想要8塊錢以下的花,那么寫法就是這樣的:

List?resList =newArrayList<>();for(Flower flower : flowerList) {if(flower.getPrice() <8) {? ? ? ? resList.add(flower);? ? }}復(fù)制代碼

其實(shí)代碼寫法大部分都是一樣的,只是判斷的條件不一樣,那么我們進(jìn)行第一版優(yōu)化

我們將判斷方法抽取出來:

publicstaticbooleanisRed(Flower flower){returnStringUtils.equals("red", flower.getColor());}publicstaticbooleanisLowPrice(Flower flower){returnflower.getPrice() <8;}復(fù)制代碼

借助函數(shù)式接口Predicate,將我們自定義的方法傳遞進(jìn)去:

publicstaticListfilterFlower(List flowers, Predicate p){? ? List?resList =newArrayList<>();for(Flower flower : flowers) {if(p.test(flower)) {? ? ? ? ? ? resList.add(flower);? ? ? ? }? ? }returnresList;}復(fù)制代碼

使用:

filterFlower(flowerList,Flower::isRed);filterFlower(flowerList,Flower::isLowPrice);復(fù)制代碼

我們也可以借助Lambda流來傳遞函數(shù),就可以不用事先寫好判斷函數(shù)了:

filterFlower(flowerList, (Flower f) -> StringUtils.equals("red", f.getColor()));filterFlower(flowerList, (Flower f) -> f.getPrice() <8);復(fù)制代碼

默認(rèn)方法

在 Java 8 之前我們可以實(shí)現(xiàn)一個(gè)接口然后被強(qiáng)制重寫這個(gè)接口的方法,那么隱含著很多問題:如果動(dòng)物類新增一個(gè)fly()方法,那么其實(shí)dog這個(gè)類是不需要fly這個(gè)方法的,但是如果不重寫就會(huì)編譯報(bào)錯(cuò)。因此在 Java 8 之后也設(shè)計(jì)了默認(rèn)方法這一種方式巧妙的解決了這種問題。

interfaceAnimal{voideat();voidfly();}classbirdimplementsAnimal{@Overridepublicvoideat(){}@Overridepublicvoidfly(){? ? ? ? System.out.println("bird fly");? ? }}classdogimplementsAnimal{@Overridepublicvoideat(){}}復(fù)制代碼

Java 8 之后可以這樣寫:

interfaceAnimal{voideat();defaultvoidfly(){? ? ? ? System.out.println("animal fly");? ? }}classbirdimplementsAnimal{@Overridepublicvoideat(){}@Overridepublicvoidfly(){? ? ? ? System.out.println("bird fly");? ? }}classdogimplementsAnimal{@Overridepublicvoideat(){}}復(fù)制代碼

以上便是 Java 8 的部分特性,那么接下來就讓我們來了解 Java 8的使用

2)行為參數(shù)化

開發(fā)中,我們需要應(yīng)對不斷的需求,怎么樣才能做到自適應(yīng)可擴(kuò)展就是我們要關(guān)注的地方。

需求1:篩選出紅色的花

publicstaticListfilterFlower(List flowers){? ? List?resList =newArrayList<>();for(Flower flower : flowers) {if(StringUtils.equals("red", flower.getColor())) {? ? ? ? ? ? resList.add(flower);? ? ? ? }? ? }}復(fù)制代碼

需求2:篩選出綠色的話

聰明的你肯定想到了我們可以通過傳遞一個(gè)顏色參數(shù)來過濾花朵,而不用每一次都修改主要代碼。

publicstaticListfilterFlowerByColor(List flowers, String color){? ? List?resList =newArrayList<>();for(Flower flower : flowers) {if(StringUtils.equals(color, flower.getColor())) {? ? ? ? ? ? resList.add(flower);? ? ? ? }? ? }}復(fù)制代碼

需求3:篩選出價(jià)格小于8塊錢的花

這樣子我們只能再寫一個(gè)方法來實(shí)現(xiàn)這個(gè)需求,為了防止后續(xù)價(jià)格的變化,聰明的我們提前將價(jià)格設(shè)置成可變參數(shù)。

publicstaticListfilterFlowerByPrice(List flowers, Integer price){? ? List?resList =newArrayList<>();for(Flower flower : flowers) {if(flower.getPrice() < price) {? ? ? ? ? ? resList.add(flower);? ? ? ? }? ? }}復(fù)制代碼

為了保持代碼的整潔,我們被迫重寫了一個(gè)方法來實(shí)現(xiàn)上述的需求:

publicstaticListfilterFlower(List flowers, String color, Integer price, Boolean flag){? ? List?resList =newArrayList<>();for(Flower flower : flowers) {if((flag && flower.getPrice() < price) ||? ? ? ? ? ? (!flag && StringUtils.equals(color, flower.getColor()))) {? ? ? ? ? ? resList.add(flower);? ? ? ? }? ? }returnresList;}復(fù)制代碼

通過flag來控制要篩選價(jià)格類型的花還是顏色類型的花,但是這種寫法實(shí)在是不美觀。

那么,我們既然都能把花的屬性作為參數(shù)進(jìn)行傳遞,那么我們能不能我們能不能把過濾花的這種行為也作為一個(gè)參數(shù)進(jìn)行傳遞,想著想著,你就動(dòng)起了手:

首先定義一個(gè)過濾行為的接口:

interfaceFilterPrecidate{booleantest(Flower flower);}復(fù)制代碼

然后自定義兩個(gè)行為過濾類繼承這個(gè)接口:

classRedColorFilterPredicateimplementsFilterPrecidate{@Overridepublicbooleantest(Flower flower){returnStringUtils.equals("red", flower.getColor());? ? }}classLowPriceFilterPredicateimplementsFilterPrecidate{@Overridepublicbooleantest(Flower flower){returnflower.getPrice() <8;? ? }}復(fù)制代碼

然后重寫我們的過濾方法,通過將行為作為參數(shù)傳遞:

publicstaticListfilterFlower(List flowers, FilterPrecidate filter){? ? List?resList =newArrayList<>();for(Flower flower : flowers) {if(filter.test(flower)) {? ? ? ? ? ? resList.add(flower);? ? ? ? }? ? }returnresList;}/*****? ? 使用? ? *****/filterFlower(flowerList,newRedColorFilterPredicate());filterFlower(flowerList,newLowPriceFilterPredicate());復(fù)制代碼

這樣子我們的代碼已經(jīng)很明了,但是我們再觀察一下上面的方法,filterFlower()這個(gè)方法只能傳遞對象作為參數(shù),而FilterPrecidate對象的核心方法也只有test(),如果我們有新的行為就需要新建一個(gè)類繼承FilterPrecidate接口實(shí)現(xiàn)test()方法。那么我們有沒有辦法直接將test()這一個(gè)行為作為參數(shù)傳遞,答案是有的:Lombda.

filterFlower(flowerList, (Flower flower) -> flower.getPrice() >8);復(fù)制代碼

我們甚至可以將多種行為作為作為一個(gè)參數(shù)傳遞:

filterFlower(flowerList, (Flower flower) -> flower.getPrice() >8&& StringUtils.equals("red", flower.getColor()));復(fù)制代碼

可以看到,行為參數(shù)化是一個(gè)很有用的模式,它能夠輕松地使用不斷變化的需求,這種模式可以把一個(gè)行為封裝起來,并通過傳遞和使用創(chuàng)建的行為將方法的行為參數(shù)化。

它可以替代匿名類

如果我們將一個(gè)鮮花的集合按照價(jià)格進(jìn)行排序,我們會(huì)這樣做:

Collections.sort(flowerList,newComparator() {@Overridepublicintcompare(Flower o1, Flower o2){returno1.getPrice().compareTo(o2.getPrice());? ? }});復(fù)制代碼

那么通過行為參數(shù)化我們可以這樣寫:

Collections.sort(flowerList,(o1, o2) -> o1.getPrice().compareTo(o2.getPrice()));復(fù)制代碼

也可以這樣寫:

Collections.sort(flowerList, Comparator.comparing(Flower::getPrice));復(fù)制代碼

甚至可以這樣寫:

flowerList.sort(Comparator.comparing(Flower::getPrice));復(fù)制代碼

對比一下傳統(tǒng)寫法,你是不是已經(jīng)開始愛上這種方式的寫法了

3)初識 Lambda

Lambda可以理解為是一種簡潔的匿名函數(shù)的表示方式:它沒有名稱,但它有參數(shù)列表,函數(shù)主體,返回類型,還可以有一個(gè)可以拋出的異常。

Lambda表達(dá)式鼓勵(lì)采用行為參數(shù)化的風(fēng)格。利用Lambda表達(dá)式我們可以自定義一個(gè)Comparator對象

Lambda 例子

(String s)-> s.length():從一個(gè)對象中抽取值,具有一個(gè) String 類型的參數(shù),返回一個(gè) int 類型的值,Lambda 表達(dá)式?jīng)]有 return 語句,已經(jīng)隱含了 return

(Flower f) -> f.getPrice() > 8:布爾表達(dá)式,具有一個(gè) Flower 類型的參數(shù),返回一個(gè) boolean 類型的值

(String s) -> {System.out.print(s);} :消費(fèi)一個(gè)對象,具有一個(gè) String 類型的參數(shù),沒有返回值(void)

() -> new Flower("red",8):創(chuàng)建一個(gè)對象,沒有傳入?yún)?shù),返回一個(gè) int 類型的值(1)

函數(shù)式接口

函數(shù)式接口就是只定義一個(gè)抽象方法的接口,并使用@FunctionalInterface標(biāo)記。

例如

publicinterfaceComparator{intcompare(T o1, T o2);}復(fù)制代碼

publicinterfaceRunnable{voidrun();}復(fù)制代碼

publicinterfaceActionListenerextendsEventListener{voidactionPerformed(ActionEvent e);}復(fù)制代碼

publicinterfaceCallable{Vcall();}復(fù)制代碼

Lambda 表達(dá)式可以允許直接以內(nèi)聯(lián)的形式為函數(shù)式接口的抽象方法提供實(shí)現(xiàn),并把整個(gè)表達(dá)式作為函數(shù)式接口的示例(Lambda表達(dá)式就是函數(shù)式接口一個(gè)具體實(shí)現(xiàn)的示例)。

Runnable runnable =newRunnable() {@Overridepublicvoidrun(){? ? ? ? ? ? System.out.println("這是傳統(tǒng)的寫法");? ? ? ? }? ? };Runnable r = () -> System.out.println("這是使用 Lambda 的寫法");復(fù)制代碼

使用函數(shù)式接口

Predicate

這個(gè)接口中定義了一個(gè)test()的抽象方法,它接受泛型 T 對象,并返回一個(gè) boolean。你如果需要表示一個(gè)涉及類型 T 的布爾表達(dá)式時(shí),就可以使用這個(gè)接口。

publicstaticListfilterFlower(List flowers, Predicate p){? ? List?resList =newArrayList<>();for(Flower flower : flowers) {if(p.test(flower)) {? ? ? ? ? ? resList.add(flower);? ? ? ? }? ? }returnresList;}/*****? ? ? 使用方式? ? ? ? *****/filterFlower(flowerList, (Flower flower) -> flower.getPrice() >8);復(fù)制代碼

Consumer

這個(gè)接口定義了一個(gè)accept()的抽象方法,它接受泛型 T 對象,沒有返回(void)。你如果需要訪問類型 T 的對象,并對其執(zhí)行某些操作,就可以使用這個(gè)接口。

List?nums = Arrays.asList(1,2,3,4);nums.forEach(integer -> System.out.println(integer));復(fù)制代碼

Function

這個(gè)接口定義了一個(gè)apply()的抽象方法,它接受泛型 T 對象,并返回一個(gè)泛型 R 的對象。你如果需要定義一個(gè)Lambda,將輸入對象的信息映射輸出,就可以使用這個(gè)接口。

(String s) -> s.length()復(fù)制代碼

Supplier

這個(gè)接口定義了一個(gè)get()的抽象方法,它沒有傳入?yún)?shù),會(huì)返回一個(gè)泛型 T 的對象,如果你需要定義一個(gè) Lambda,輸出自定義的對象,就可以使用這個(gè)接口。

Callable?call = () ->1;復(fù)制代碼

類型檢查

以這個(gè)為例子:

filter(flowerList, (Flower flower) -> flower.getPrice() > 8);

首先找出 filter 方法的聲明

要求第二個(gè)參數(shù)是 Predicate 類型的對象

Predicate 是一個(gè)函數(shù)式接口,定義了一個(gè)test()的抽象方法,并返回一個(gè)boolean 類型的值

類型推斷

filterFlower(flowerList, (Flower flower) -> flower.getPrice() > 8);

我們可以繼續(xù)將這個(gè)代碼簡化為:

filterFlower(flowerList, f -> f.getPrice() > 8);

使用局部變量

Lambda 表達(dá)式不僅能夠使用主體里面的參數(shù),也能夠使用自由變量(在外層作用域中定義的變量)。

inttmpNum =1;Runnable r = () -> System.out.println(tmpNum);復(fù)制代碼

注意點(diǎn):Lambda 表達(dá)式對于全局變量和靜態(tài)變量可以沒有限制的使用,但是對于局部變量必須顯示聲明為 final

因?yàn)閷?shí)例變量是存儲(chǔ)在堆中,而局部變量是存儲(chǔ)在棧中,屬于線程私有的。而 Lambda 是在一個(gè)線程中使用的,訪問局部變量只是在訪問這個(gè)變量的副本,而不是訪問原始值。

方法引用

方法引用就是讓你根據(jù)已有的方法實(shí)現(xiàn)來創(chuàng)建 Lambda表達(dá)式??梢钥醋鍪菃我环椒ǖ?Lambda 的語法糖。

例子

List?flowerList = Arrays.asList(newFlower("red",6),newFlower("yellow",7),newFlower("pink",8));復(fù)制代碼

(Flower f)->f.getPrice();==>Flower::getPrice

flowerList.stream().map(t -> t.getPrice()).collect(Collectors.toList());===>flowerList.stream().map(Flower::getPrice).collect(Collectors.toList());

List?nums = Arrays.asList(1,2,3,4);復(fù)制代碼

nums.forEach(integer -> System.out.println(integer));===>nums.forEach(System.out::println);

如何構(gòu)建方法引用

指向靜態(tài)方法的方法引用(Integer的sum方法 ==Integer::sum)

指向任意類型示例方法的方法引用(String的length方法 ==String::length)

指向現(xiàn)有對象的示例方法的方法引用(flower實(shí)例的getPrice方法 ==flower::getPrice)

復(fù)合 Lambda 表達(dá)式

比較器復(fù)合

我們有一組鮮花集合如下:

List?flowerList = Arrays.asList(newFlower("red",6),newFlower("yellow",7),newFlower("pink",8),newFlower("white",8));復(fù)制代碼

按鮮花的價(jià)格進(jìn)行排序:

flowerList.sort(Comparator.comparing(Flower::getPrice));復(fù)制代碼

這樣子默認(rèn)是使用升序進(jìn)行排列的,那么我們?nèi)绻脒M(jìn)項(xiàng)降序:使用 reversed()

flowerList.sort(Comparator.comparing(Flower::getPrice).reversed());復(fù)制代碼

這里的粉花和白花的價(jià)格一樣,那我們在價(jià)格排序完后再按照顏色排序那應(yīng)該怎么做:使用 thenComparing()

flowerList.sort(Comparator.comparing(Flower::getPrice).thenComparing(Flower::getColor));復(fù)制代碼

謂詞復(fù)合

用于Predicate接口

negate:

Predicate?redFlower = (t) -> StringUtils.equals("red",t.getColor());Predicate?notRedFlower = redFlower.negate();復(fù)制代碼

and:

Predicate?redFlower = (t) -> StringUtils.equals("red", t.getColor());Predicate?lowPriceFlower = (t) -> t.getPrice() <8;Predicate?redAndLowPriceFlower = redFlower.and(lowPriceFlower);復(fù)制代碼

or:

Predicate?redFlower = (t) -> StringUtils.equals("red", t.getColor());Predicate?lowPriceFlower = (t) -> t.getPrice() <8;Predicate?redOrLowPriceFlower = redFlower.or(lowPriceFlower);復(fù)制代碼

函數(shù)復(fù)合

用于Function接口

andThen

Function?addRes = a1 -> a1 +1;Function?mulRes = a1 -> a1 *2;Function?andThenResult = addRes.andThen(mulRes);Integer apply = andThenResult.apply(1);// 結(jié)果為 4 ==> (1 + 1) * 2復(fù)制代碼

compose

Function?addRes = a1 -> a1 +1;Function?mulRes = a1 -> a1 *2;Function?composeResult = addRes.compose(mulRes);Integer apply = composeResult.apply(1);// 結(jié)果為 3 ==> (1 * 2) + 1復(fù)制代碼

兩者的區(qū)別就是操作的順序不一樣

二、函數(shù)式數(shù)據(jù)處理

1)流的使用

集合是 Java 中使用最多的API。流是 Java API 的新成員,它允許以聲明式方式處理數(shù)據(jù)集合,可以看作是遍歷數(shù)據(jù)集的高級迭代器。而且,劉??梢?i>透明地并行處理,這樣就可以無需多寫任何多線程代碼了。

現(xiàn)在有一組花的集合如下:

List?flowerList = Arrays.asList(newFlower("red",10),newFlower("yellow",7),newFlower("pink",8),newFlower("white",8),newFlower("black",12));復(fù)制代碼

需求:獲取10塊錢以下并且按照價(jià)格排序的花的顏色

傳統(tǒng)寫法

List?lowPriceFlowers =newArrayList<>();for(Flower flower : flowerList) {if(flower.getPrice() <10) {? ? ? ? lowPriceFlowers.add(flower);? ? }}Collections.sort(lowPriceFlowers,newComparator() {@Overridepublicintcompare(Flower o1, Flower o2){returno1.getPrice().compareTo(o2.getPrice());? ? }});List?lowPriceFlowerColor =newArrayList<>();for(Flower priceFlower : lowPriceFlowers) {? ? lowPriceFlowerNames.add(priceFlower.getColor());}復(fù)制代碼

為了完成這個(gè)需求不僅代碼量大,還多定義了lowPriceFlowers這個(gè)臨時(shí)變量,真的是糟糕透了! Java 8 之后,代碼才應(yīng)該有它該有的樣子:

List?colorList =? flowerList.stream().filter(t->t.getPrice()<10).sorted(Comparator.comparing(Flower::getPrice)).map(Flower::getColor).collect(Collectors.toList());復(fù)制代碼

通過filter篩選出10元以下的花,然后通過sorted按照花的價(jià)格進(jìn)行排序,再通過map映射出花的顏色,最后通過collect將流歸約成一個(gè)集合。filter 處理的結(jié)果傳給了 sorted 方法,再傳給 map 方法,最后傳給 collect 方法。

甚至我們還可以利用多核架構(gòu)并行執(zhí)行這段代碼,只需要把stream()換成parallelStream()

flowerList.parallelStream().filter(t->t.getPrice()<10).sorted(Comparator.comparing(Flower::getPrice)).map(Flower::getColor).collect(Collectors.toList());復(fù)制代碼

因?yàn)閒ilter、sorted、map和collect等操作是與具體線程模型無關(guān)的高層次構(gòu)件,所以它們的內(nèi)部實(shí)現(xiàn)可以是單線程的,也可能透明地充分利用你的多核架構(gòu)!在實(shí)踐中,這意味著你用不著為了讓某些數(shù)據(jù)處理任務(wù)并行而去操心線程和鎖。

2)流和集合

集合與流之間的差異就在于什么時(shí)候進(jìn)行計(jì)算。集合是一個(gè)內(nèi)存中的數(shù)據(jù)結(jié)構(gòu),它包含數(shù)據(jù)結(jié)構(gòu)中目前所有的值——集合中的每個(gè)元素都得先算出來才能添加到集合中。流則是在概念上固定的數(shù)據(jù)結(jié)構(gòu)(你不能添加或刪除元素),其元素則是按需計(jì)算的。從另一個(gè)角度來說,流就像是一個(gè)延遲創(chuàng)建的集合:只有在消費(fèi)者要求的時(shí)候才會(huì)計(jì)算值

只能遍歷一次:和迭代器類似,流只能遍歷一次。遍歷完之后,這個(gè)流已經(jīng)被消費(fèi)掉了。你可以從原始數(shù)據(jù)源那里再獲得一個(gè)新的流來重新遍歷一遍。

List?color = Arrays.asList("red","yellow","pink");Stream?s = title.stream();s.forEach(System.out::println);//在這里 流已經(jīng)被消費(fèi)了s.forEach(System.out::println);//如果這里再消費(fèi)流則會(huì)報(bào)錯(cuò)!復(fù)制代碼

3)流的操作

流可以拆成三大操作

獲取流->中間操作->終端操作

List?colorList =? flowerList.stream()//獲取流.filter(t->t.getPrice()<10)//中間操作.limit(3)//中間操作.map(Flower::getColor)//中間操作.collect(Collectors.toList());//終端操作復(fù)制代碼

中間操作:

操作返回類型操作參數(shù)

filterStreamPredicate

mapStreamFuncation<T, R>

limitStream

sortedStreamComparator

distinctStream

skipStreamlong

limitStreamlong

flatMapStreamFuncation<T, Steam>

終端操作

操作返回類型操作參數(shù)

forEachvoidConsumer

countlong

collectRCollector<T, A, R>

anyMatchbooleanPredicate

noneMatchbooleanPredicate

allMatchbooleanPredicate

findAnyOptional

findFirstOptional

reduceOptionalBinaryOperator

(1)使用流

篩選:filter

List?colorList =? flowerList.stream().filter(t->t.getPrice()<10).collect(Collectors.toList());復(fù)制代碼

篩選去重:distinct

List?numbers = Arrays.asList(1,2,1,3,3,2,4);numbers.stream().filter(i -> i %2==0).distinct().forEach(System.out::println);復(fù)制代碼

篩選截?cái)?/b>:limit

List?colorList =? flowerList.stream().filter(t->t.getPrice()<10).limit(3).collect(Collectors.toList());復(fù)制代碼

篩選跳躍:skip

List?colorList =? flowerList.stream().filter(t->t.getPrice()<10).skip(2).collect(Collectors.toList());復(fù)制代碼

(2)映射

流支持map()方法,它會(huì)接受一個(gè)函數(shù)作為參數(shù),這個(gè)行數(shù)會(huì)被應(yīng)用到每個(gè)元素上,并將其映射成一個(gè)新的元素。

List?colors = flowerList.stream().map(Flower::getColor).collect(Collectors.toList());復(fù)制代碼

它是創(chuàng)建一個(gè)新的集合,而不是修改原有的集合

(3)流的扁平化

將一個(gè)單詞的集合,拆分成各個(gè)字母的集合:

[Hello,World]===>[H, e, l, o, W, r, d]

首先我們嘗試使用map看能不能解決問題:

List?words = Arrays.asList("Hello","World");words.stream().map(t->t.split("")).distinct().collect(Collectors.toList());/*****? ? ? 結(jié)果? ? ? *****

[[Ljava.lang.String;@2cdf8d8a, [Ljava.lang.String;@30946e09]

**/復(fù)制代碼

可以看到,這樣處理后的結(jié)果是一個(gè)數(shù)組的集合,并不是我們想要的結(jié)果,這是因?yàn)閙ap返回的流實(shí)際上是Stream<String[]>類型的。但是我們想要的是Stream<String>來表示一個(gè)字符流。

既然需要Stream<String>的字符流,那我們使用Arrays.stream()來處理試一下:

words.stream().map(t -> t.split("")).map(Arrays::stream).distinct.collect(Collectors.toList());/*****? ? ? 結(jié)果? ? ? *****

[java.util.stream.ReferencePipeline$Head@1698c449, java.util.stream.ReferencePipeline$Head@5ef04b5]

**/復(fù)制代碼

這是返回了一個(gè)Stream<String>的集合,貌似只要將這個(gè)集合處理合并一下就可以解決問題了。所以flatMap()出現(xiàn)了。

words.stream().map(t->t.split("")).flatMap(t -> Arrays.stream(t)).distinct().collect(Collectors.toList());/*****? ? ? 結(jié)果? ? ? *****

[H, e, l, o, W, r, d]

**/復(fù)制代碼

果然,已經(jīng)成功解決了問題,flatMap方法就是讓你把一個(gè)流中的每個(gè)值都轉(zhuǎn)成另一個(gè)流,然后把所有的流連接起來成為一個(gè)流。

(4)匹配

anyMatch()

流中是否有一個(gè)元素能夠匹配所給定謂詞,只有有一個(gè)匹配上就返回 true

booleanres = flowerList.stream().anyMatch(t -> t.getPrice() <8);復(fù)制代碼

allMatch()

流中的元素是否都能匹配給定的謂詞,所有匹配上才能返回 true

booleanres = flowerList.stream().allMatch(t -> t.getPrice() <8);復(fù)制代碼

noneMatch()

流中沒有任何元素與給定的謂詞相匹配,有一個(gè)匹配就會(huì)返回 false

booleanres = flowerList.stream().noneMatch(t -> t.getPrice() <8);復(fù)制代碼

(5)查找

findAny

返回當(dāng)前流中的任意元素

flowerList.stream().filter(t->t.getPrice()<8).findAny();復(fù)制代碼

findFirst

返回當(dāng)前流中的第一個(gè)元素

flowerList.stream().filter(t->t.getPrice()<8).findFirst();復(fù)制代碼

(6)歸約

reduce接收兩個(gè)參數(shù),一個(gè)是初始值,一個(gè)是將集合中所有元素結(jié)合的操作

reduce也支持一個(gè)參數(shù),將集合中所有元素結(jié)合的操作,不過返回的是一個(gè) Option ,Option 下面會(huì)講到

List<Integer> nums = Arrays.asList(1,2,3,4,5,6,7,8,9);

元素求和

傳統(tǒng)寫法:

intres =0;for(Integer num : nums) {? ? res += num;}復(fù)制代碼

改進(jìn)后:

// 兩個(gè)參數(shù)版intres = nums.stream().reduce(0,(a, b) -> a + b);intres = nums.stream().reduce(0,Integer::sum);// 一個(gè)參數(shù)版Optional?o = nums.stream().reduce(Integer::sum);復(fù)制代碼

最大值和最小值

傳統(tǒng)寫法:

intmax =0;intmin = Integer.MAX_VALUE;for(Integer num : nums) {if(num > max) {? ? ? ? max = num;? ? }if(num < min) {? ? ? ? min = num;? ? }}復(fù)制代碼

改進(jìn)后:

// 兩個(gè)參數(shù)版intmax = nums.stream().reduce(0,Integer::max);intmin = nums.stream().reduce(Integer.MAX_VALUE,Integer::min);// 一個(gè)參數(shù)版Optional?maxOption = nums.stream().reduce(Integer::max);Optional?minOption = nums.stream().reduce(Integer::min);復(fù)制代碼

(7)小練習(xí)(出于網(wǎng)上)

(1) 找出2011年發(fā)生的所有交易,并按交易額排序(從低到高)。

(2) 交易員都在哪些不同的城市工作過?

(3) 查找所有來自于劍橋的交易員,并按姓名排序。

(4) 返回所有交易員的姓名字符串,按字母順序排序。

(5) 有沒有交易員是在米蘭工作的?

(6) 打印生活在劍橋的交易員的所有交易額。

(7) 所有交易中,最高的交易額是多少?

(8) 找到交易額最小的交易

答案:

4)流的構(gòu)建

由值創(chuàng)建流:Stream.of()/Stream.empty()

Stream?stream = Stream.of("hello","world");Stream?emptyStream = Stream.empty();復(fù)制代碼

由數(shù)組創(chuàng)建流:Arrays.stream()

int[] numbers = {2,3,5,7,11,13};intsum = Arrays.stream(numbers).sum();復(fù)制代碼

由文件生成流:File.lines()

longuniqueWords =0;try(Stream?lines =Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))).distinct().count();}catch(IOException e){}// 使用 Files.lines 得到一個(gè)流,其中的每個(gè)元素都是給定文件中的一行。然后,你可以對 line 調(diào)用 split 方法將行拆分成單詞復(fù)制代碼

5)收集器的使用

如今有一組花的集合如下:

List?flowerList = Arrays.asList(newFlower("red",10),newFlower("yellow",7),newFlower("pink",8),newFlower("yellow",8),newFlower("red",12));復(fù)制代碼

這個(gè)時(shí)候我想按照花的顏色進(jìn)行分類,獲取一個(gè)Map<String, List<Flower>>

傳統(tǒng)寫法:

Map> listMap =newHashMap<>();for(Flower flower : flowerList) {if(null== listMap.get(flower.getColor())) {? ? ? ? List?flowers =newArrayList<>();? ? ? ? listMap.put(flower.getColor(), flowerList);? ? }? ? listMap.get(flower.getColor()).add(flower);}復(fù)制代碼

相信以上代碼是比較常見的,那么當(dāng)我們學(xué)習(xí)了 Java 8之后有沒有什么比較好的寫法呢:

Map> map = flowerList.stream().collect(Collectors.groupingBy(Flower::getColor));復(fù)制代碼

一行代碼解決,Java 8 真的是秀??!

函數(shù)式變成的一個(gè)主要優(yōu)勢就是,我們只要告訴它“做什么”,而不用關(guān)心“怎么做”。就像是上一個(gè)例子中,我們需要的是按顏色分組,所以我們只要跟收集器說 按照顏色分組就行collect(Collectors.groupingBy(Flower::getColor))。我們上面也比較經(jīng)常用到的是collect(Collectors.toList(),它的作用就是將我們需要的結(jié)果收集成一個(gè)集合。

用來計(jì)算總數(shù)

Long c1 = flowerList.stream().collect(Collectors.counting());//也可以直接用 count() 方法來計(jì)數(shù)Long c2 = flowerList.stream().count();復(fù)制代碼

用來查找最大值和最小值

Optional?max = flowerList.stream().collect(Collectors.maxBy(Comparator.comparing(Flower::getPrice)));Optional?min = flowerList.stream().collect(Collectors.minBy(Comparator.comparing(Flower::getPrice)));復(fù)制代碼

用來求和

Integer sum = flowerList.stream().collect(Collectors.summingInt(Flower::getPrice));復(fù)制代碼

用來求平均數(shù)

Double avg = flowerList.stream().collect(Collectors.averagingInt(Flower::getPrice));復(fù)制代碼

用來連接字符串

String color = flowerList.stream().map(Flower::getColor).collect(Collectors.joining(", "));復(fù)制代碼

6)分組的使用

如今有一組花的集合如下:

List?flowerList = Arrays.asList(newFlower("red",10),newFlower("yellow",7),newFlower("pink",8),newFlower("yellow",8),newFlower("red",12));/*****? ? ? 結(jié)果? ? ? *****

{red=[Flower(color=red, price=10), Flower(color=red, price=12)], pink=[Flower(color=pink, price=8)], yellow=[Flower(color=yellow, price=7), Flower(color=yellow, price=8)]}

**/復(fù)制代碼

按照顏色分組:Map>

Map> color = flowerList.stream().collect(Collectors.groupingBy(Flower::getColor));/*****? ? ? 結(jié)果? ? ? *****

{red=[Flower(color=red, price=10), Flower(color=red, price=12)], pink=[Flower(color=pink, price=8)], yellow=[Flower(color=yellow, price=7), Flower(color=yellow, price=8)]}

**/復(fù)制代碼

統(tǒng)計(jì)每種顏色的數(shù)量:Map

Map?longMap = flowerList.stream().collect(Collectors.groupingBy(Flower::getColor, Collectors.counting()));/*****? ? ? 結(jié)果? ? ? *****

{red=2, pink=1, yellow=2}

**/復(fù)制代碼

也可以支持多級分組

先按顏色分組,再按價(jià)格分組:Map>>

Map>> collect = flowerList.stream().collect(Collectors.groupingBy(Flower::getColor, Collectors.groupingBy(t -> {if(t.getPrice() <8) {return"LOW_PRICE";? ? }else{return"HIGHT_PRICE";? ? }})));/*****? ? ? 結(jié)果? ? ? *****

{red={HIGHT_PRICE=[Flower(color=red, price=10), Flower(color=red, price=12)]}, pink={HIGHT_PRICE=[Flower(color=pink, price=8)]}, yellow={HIGHT_PRICE=[Flower(color=yellow, price=8)], LOW_PRICE=[Flower(color=yellow, price=7)]}}

**/復(fù)制代碼

先按顏色分組,再找每個(gè)顏色中最貴的花:Map

Map?f = flowerList.stream().collect(Collectors.groupingBy(Flower::getColor, Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Flower::getPrice)), Optional::get)));/*****? ? ? 結(jié)果? ? ? *****

{red=Flower(color=red, price=12), pink=Flower(color=pink, price=8), yellow=Flower(color=yellow, price=8)}

**/復(fù)制代碼

這個(gè)工廠方法接受兩個(gè)參數(shù)——要轉(zhuǎn)換的收集器以及轉(zhuǎn)換函數(shù),并返回另一個(gè)收集器。這個(gè)收集器相當(dāng)于舊收集器的一個(gè)包裝, collect 操作的最后一步就是將返回值用轉(zhuǎn)換函數(shù)做一個(gè)映射。在這里,被包起來的收集器就是用 maxBy 建立的那個(gè),而轉(zhuǎn)換函數(shù) Optional::get 則把返回的 Optional 中的值提取出來。

Collectors 的常用方法

方法返回類型用途

toListList把流中所有項(xiàng)目都收集到一個(gè)List

toSetSet把流中所有項(xiàng)目都收集到一個(gè)Set,刪除重復(fù)項(xiàng)

toCollectionCollection把流中所有項(xiàng)目收集到給定的供應(yīng)源創(chuàng)建的集合

countingLong計(jì)算流中元素的個(gè)數(shù)

summingIntInteger對流中項(xiàng)目的一個(gè)整數(shù)屬性求和

averagingIntDouble計(jì)算流中項(xiàng)目Integer屬性的平均值

joiningString連接對流中每個(gè)項(xiàng)目調(diào)用toString方法所生成的字符串

maxByOptional一個(gè)包裹了流中按照給定比較器選出最大元素的Optional,如果為空則為Optional.empty()

minByOptional一個(gè)包裹了流中按照給定比較器選出最小元素的Optional,如果為空則為Optional.empty()

reducing歸約操作產(chǎn)生的類型從一個(gè)作為累加器的初始值開始,利用 BinaryOperator 與流中的元素組個(gè)結(jié)合,從而將流歸約成單個(gè)值

collectingAndThen轉(zhuǎn)換函數(shù)返回的類型包裹另一個(gè)收集器,對其結(jié)果應(yīng)用轉(zhuǎn)換函數(shù)

groupingByMap<K, List>根據(jù)項(xiàng)目的一個(gè)屬性的值對流中的項(xiàng)目作為組,并將屬性值作為結(jié)果Map的鍵

三、學(xué)會(huì)使用Optional

開發(fā)中最經(jīng)常遇到的異常某過于NullPointException了吧。因?yàn)檫@就是我們?yōu)榱朔奖闵踔敛豢杀苊獾南?null 引用這樣的構(gòu)造所付出的代價(jià)。Java 8之后仿佛出現(xiàn)了轉(zhuǎn)機(jī),那就是用Optional來代替null。

上面這段代碼乍看之下應(yīng)該沒啥問題,平時(shí)開發(fā)的時(shí)候也很有可能會(huì)情不自禁的寫出類似這種的代碼。但是問題也就來了,真的是每個(gè)人都有手機(jī)嗎,如果new Person().getPhone()獲取不到手機(jī),那么調(diào)用getType()是不是就會(huì)出現(xiàn)熟悉的NullPointException異常了。

1)防御式檢查

為了避免空指針異常,Java 8出現(xiàn)的Optional為我們很好的避免了。

經(jīng)典預(yù)防方式

privateStringgetPhoneType(Person person){if(person !=null) {? ? ? ? Phone phone = person.getPhone();if(phone !=null) {returnphone.getType();? ? ? ? }? ? }return"";}復(fù)制代碼

每次引用都做一次判空操作,效果想必也不賴,也可以避免空指針異常。當(dāng)時(shí)每一次判空都得添加一個(gè)if判斷,真實(shí)讓人頭大。

Optional 預(yù)防

從圖中可以看出Optional相當(dāng)于是一個(gè)容器,里面可以裝 T 類型的對象。當(dāng)變量不存在的時(shí)候,缺失的值就會(huì)被建模成一個(gè)“空”的Optional對象,由方法Optional.empty()返回。這就是Optional.empty()和null的區(qū)別,如果引用一個(gè) null,那結(jié)果肯定是會(huì)觸發(fā)NullPointException異常,但是引用Optional.empty()則沒事。

上述代碼可修改為:

privateStringgetPhoneType(Person person){returnOptional.ofNullable(person).map(Person::getPhone).map(Phone::getType).orElse("");}復(fù)制代碼

一行代碼搞定,干凈利落。

2)學(xué)會(huì)使用Option

創(chuàng)建Optional對象

創(chuàng)建一個(gè)空的Optional

Optional<Person> personOpt = Optional.empty()

創(chuàng)建一個(gè)非空的Optional

Optional<Person> personOpt = Optional.of(person)

Optional.of()不接受空值。如果 person 是一個(gè)空值則會(huì)拋出 NullPointException 異常,而不是等你試圖訪問 person 的屬性才拋出異常。

創(chuàng)建一個(gè)可接受空值的Optional

Optional<Person> personOpt = Optional.ofNullable(Person)

如果 person 是 null ,那么得到的 Optional 對象就是個(gè)空對象。

使用map

Optional 中的map()方法和流中的map()相似,都是從Optional對象中提取和轉(zhuǎn)換值。

Optional?name = Optional.ofNullable(person).map(Person::getName);復(fù)制代碼

獲取到的是一個(gè)Optional對象是為了防止獲取到一個(gè) null,我們可以通過Optional.get()來獲取值。

默認(rèn)行為

我們可以使用get()方法來獲取 Optional 的值,也可以使用orElse()來定義一個(gè)默認(rèn)值,遭遇到空的Optional值的時(shí)候,默認(rèn)值會(huì)作為該方法的調(diào)用返回值。以下是Optional的常用方法:

get()

最簡單但又是最不安全的方法,如果變量存在,則直接返回封裝的變量值,反之則拋出NullpointException異常。

orElse(T other)

允許自己定義一個(gè)默認(rèn)值在Optional為空的時(shí)候返回。

orElseGet(Supplier<? extend T> other)

是orElse()方法的延遲調(diào)用版,在Optional對象不含值的時(shí)候執(zhí)行調(diào)用。

orElseThrow(Supplier<? extend X> excetionSupplier)

和get()方法類似,在Optional對象為空的時(shí)候會(huì)拋出一個(gè)異常,但是這個(gè)異常我們可以自定義。

ifPresent(Consumer<? extend T>)

在Optional對象存在的執(zhí)行的方法,反之不操作。也接受一個(gè)空參數(shù)的,如果

方法描述

empty返回一個(gè)空的Optional實(shí)例

filter如果值存在并且滿足提供的謂詞,就會(huì)返回包含該值的Optional對象;否則返回一個(gè)空的Optional對象

get如果值存在,將該值用Optional封裝返回,否則拋出一個(gè)NullPointException異常

ifPresent如果值存在,就執(zhí)行使用該值的方法調(diào)用,否則什么也不做

ifPresent如果值存在就返回true,否則返回false

map如果值存在,就對該值執(zhí)行提供的 mapping 函數(shù)調(diào)用

of將指定值用Optional封裝后返回,如果該值為 null,則拋出一個(gè) NullPointException異常

ofNullable將指定值用 Optional 封裝之后返回,如果該值為null,則返回一個(gè)空的 Optional 對象

orElse如果有值則將其返回,否則返回一個(gè)默認(rèn)值

orElseGet如果有值則將其返回,否則返回一個(gè)由指定的 Supplier 接口生成的值

orElseThrow如果有值則將其放回,否則拋出一個(gè)由指定的 Supplier 接口生成的異常

四、新的日期和時(shí)間

在 Java 8之前,我們對日期和時(shí)間的支持智能依賴java.util.Date類,這個(gè)類無法表示日期,只能以毫秒的精度表示時(shí)間。而且它的表現(xiàn)方式也不是那么直觀,在Java1.0的Date這個(gè)類中,年份的起始是 1900 年,月份的起始是 0 開始,如果我們這個(gè)時(shí)候想要構(gòu)造一個(gè) 2020年7月18號的日期,我們就得這樣做:

Date date =newDate(120,6,18);System.out.println(date);// Sat Jul 18 00:00:00 CST 2020復(fù)制代碼

這種的構(gòu)造方式簡直是糟糕透了不是嗎,對于不了解Date 的來說太不友好了。在java1.1 后出現(xiàn)了Calender這個(gè)類,而Date中大部分方法都被廢棄了,但是Calender這個(gè)類中也有類似的問題和設(shè)計(jì)缺陷,而且兩個(gè)日期類的出現(xiàn),我們有時(shí)候也難以選擇使用哪一個(gè)。

LocalDate

創(chuàng)建一個(gè) LocalDate 對象

LocalDate nowDate = LocalDate.of(2020,7,18);//2020-07-18intyear = nowDate.getYear();//2020Month month = nowDate.getMonth();//07intday = nowDate.getDayOfMonth();//18DayOfWeek dayOfWeek = nowDate.getDayOfWeek();//SATURDAYintdays = nowDate.lengthOfMonth();//31LocalDate nowdate = LocalDate.now();//獲取當(dāng)前時(shí)間>2020-07-18復(fù)制代碼

也可以使用 TemporalField 讀取 LocalDate 的值

LocalDate nowDate = LocalDate.of(2020,7,18);//2020-07-18intyear = nowDate.get(ChronoField.YEAR);//2020intmonth = nowDate.get(ChronoField.MONTH_OF_YEAR);//07intday = nowDate.get(ChronoField.DAY_OF_MONTH);//18復(fù)制代碼

LocalTime

創(chuàng)建一個(gè) LocalTime 對象

LocalTime nowTime = LocalTime.of(19,34,32);//19:34:32inthour = nowTime.getHour();//19intminute = nowTime.getMinute();//34intsecond = nowTime.getSecond();//32復(fù)制代碼

同樣也可以使用 TemporalField 讀取 LocalTime 的值

LocalTime nowTime = LocalTime.of(19,34,32);//19:34:32inthour = nowTime.get(ChronoField.HOUR_OF_DAY);//19intminute = nowTime.get(ChronoField.MINUTE_OF_HOUR);//34intsecond = nowTime.get(ChronoField.SECOND_OF_MINUTE);//32復(fù)制代碼

LocalDateTime

LocalDateTime 相當(dāng)于合并了日期和時(shí)間,以下是創(chuàng)建的幾種方式:

LocalDate nowDate = LocalDate.of(2020,7,18);//2020-07-18LocalTime nowTime = LocalTime.of(19,45,20);//19:34:32LocalDateTime dt1 = LocalDateTime.of(2020, Month.JULY,18,19,45,20);LocalDateTime dt2 = LocalDateTime.of(nowDate, nowTime);LocalDateTime dt3 = nowDate.atTime(19,45,20);LocalDateTime dt4 = nowDate.atTime(nowTime);LocalDateTime dt5 = nowTime.atDate(nowDate);LocalDate date1 = dt1.toLocalDate();//2020-07-18LocalTime time1 = dt1.toLocalTime();//19:45:20復(fù)制代碼

時(shí)間點(diǎn)的日期? ?時(shí)間類的通用方法:

方法名是否靜態(tài)方法描述

from是依據(jù)傳入的 Temporal 對象創(chuàng)建對象實(shí)例

now是依據(jù)系統(tǒng)時(shí)鐘創(chuàng)建 Temporal 對象

of是由 Temporal 對象的某個(gè)部分創(chuàng)建該對象的實(shí)例

parse否由字符串創(chuàng)建 Temporal 對象的實(shí)例

atOffset否將 Temporal 對象和某個(gè)時(shí)區(qū)偏移相結(jié)合

atZone否將 Temporal 對象和某個(gè)時(shí)區(qū)相結(jié)合

format否使用某個(gè)指定的格式器將 Temporal 對象轉(zhuǎn)換為字符串( Instant 類不提供該方法)

get否讀取 Temporal 對象的某一部分的值

minus否創(chuàng)建 Temporal 對象的一個(gè)副本,通過將當(dāng)前 Temporal 對象的值減去一定的時(shí)長創(chuàng)建該副本

plus否創(chuàng)建 Temporal 對象的一個(gè)副本,通過將當(dāng)前 Temporal 對象的值加上一定的時(shí)長創(chuàng)建該副本

with否以該 Temporal 對象為模板,對某些狀態(tài)進(jìn)行修改創(chuàng)建該對象的副本

Duration和Period

這兩個(gè)類是用來表示兩個(gè)時(shí)間內(nèi)的間隔的

Duration d1 = Duration.between(time1, time2);Duration d1 = Duration.between(dateTime1, dateTime2);Duration threeMinutes = Duration.ofMinutes(3);Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);Period tenDays = Period.ofDays(10);Period threeWeeks = Period.ofWeeks(3);Period twoYearsSixMonthsOneDay = Period.of(2,6,1);復(fù)制代碼

日期 - 時(shí)間類中表示時(shí)間間隔的通用方法:

方法名是否靜態(tài)方法方法描述

between是創(chuàng)建兩個(gè)時(shí)間點(diǎn)之間的 interval

from是由一個(gè)臨時(shí)時(shí)間點(diǎn)創(chuàng)建 interval

of是由它的組成部分創(chuàng)建 interval 的實(shí)例

parse是由字符串創(chuàng)建 interval 的實(shí)例

addTo否創(chuàng)建該 interval 的副本,并將其疊加到某個(gè)指定的 temporal 對象

get否讀取該 interval 的狀態(tài)

isNegative否檢查該 interval 是否為負(fù)值,不包含零

isZero否檢查該 interval 的時(shí)長是否為零

minus否通過減去一定的時(shí)間穿件該 interval 的副本

multipliedBy否將 interval 的值乘以某個(gè)標(biāo)量創(chuàng)建該 interval 的副本

negated否以忽略某個(gè)時(shí)長的方式創(chuàng)建該 interval 的副本

plus否以增加某個(gè)指定的時(shí)長的方式創(chuàng)建該 interval 的副本

subtractFrom否從指定的temporal對象中減去該 interval

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容