Java 8 新特性(二)流類庫

前面介紹了lambda表達式洞慎,但是我們可以看到告抄,lambda表達式其實也就是簡化了一部分代碼的編寫痒玩,說起來也不算是非常有用的語言特性。但是如果lambda表達式配合這篇文章介紹的流類庫搂妻,就會發(fā)揮出巨大的作用蒙保。

初識流類庫

老樣子,先來看一個例子欲主。有一個整數(shù)列表邓厕,我現(xiàn)在希望找到其中所有大于5的數(shù),所以我可能會這么寫扁瓢。雖然這是中規(guī)中矩的代碼详恼,但是就算是實現(xiàn)這么一個簡單的功能,也需要這么一大坨代碼引几,實在是讓人不爽单雾。

List<Integer> integers = new ArrayList<>();
integers.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8));
//獲取大于5的 所有元素
List<Integer> integersGreaterThan5 = new ArrayList<>();
for (int i : integers) {
    if (i > 5) {
        integersGreaterThan5.add(i);
    }
}
System.out.println(integersGreaterThan5);

那么如果配合流類庫呢?代碼會極大地縮減她紫,而且可讀性也大大提高。

integersGreaterThan5.clear();
//使用流類庫
integersGreaterThan5 = integers.stream()
        .filter(i -> i > 5)
        .collect(Collectors.toList());
System.out.println(integersGreaterThan5);

流類庫是Java 8新增的一組類庫屿储,讓我們可以對集合類庫進行復雜的操作贿讹,這些類庫代碼位于java.util.stream包下,注意不要和Java IO流搞混了够掠。從上面的代碼可以看到民褂,使用流類庫基本上可以分為以下幾步:把集合轉(zhuǎn)換為流、對流進行操作疯潭、將流轉(zhuǎn)換為相應的數(shù)據(jù)結(jié)構赊堪。

獲取流

在支持查看源代碼的IDE中追蹤上面代碼的stream()方法,可以發(fā)現(xiàn)這個方法在java.util.Collection接口中竖哩,大部分集合類都實現(xiàn)了這個接口哭廉,這也意味著大多數(shù)集合類都有這個方法,利用這個方法相叁,我們就可以將集合轉(zhuǎn)換為一個流遵绰,流類庫幾乎所有方法都需要在流上才能操作。

當然如果細究一下增淹,這個方法長的是這個樣子椿访。這也是Java 8的新特性,由于流類庫是在接口中添加的新方法虑润,Java 8以前的代碼是沒有實現(xiàn)這些新方法的成玫。為了老版本的代碼也可以正常運行,Java 8引入了接口默認方法,讓接口也可以實現(xiàn)方法哭当,如果在實現(xiàn)類中沒有實現(xiàn)猪腕,就會使用接口中的默認實現(xiàn)。這樣一來荣病,即使老版本的代碼沒有實現(xiàn)這些新接口码撰,程序也仍然可以正常工作。

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

流操作

過濾

最常見的流操作就是過濾了个盆,過濾相當于SQL中的where語句脖岛,就是按某種條件把流中的元素篩選出來,組成一個新流颊亮。大部分流操作的結(jié)果仍然是一個流柴梆,所以我們可以鏈式調(diào)用。下面是一個找出大于3的偶數(shù)的例子终惑。

List<Integer> integers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .filter(i -> i > 3)
        .filter(i -> i % 2 == 0)
        .collect(Collectors.toList());
System.out.println(integers);

映射

另一種常見的流操作是映射绍在,類似于SQL中的select,可以將一組元素轉(zhuǎn)換成另一種元素雹有。下面的例子將一組整數(shù)轉(zhuǎn)換為平方偿渡。這是一個簡單的例子,實際場合中常常需要將一組對象流轉(zhuǎn)換為另一組對象霸奕。

List<Integer> integers = Stream.of(1, 2, 3, 4, 5)
        .map(i -> i * i)
        .collect(Collectors.toList());
System.out.println(integers);

平整映射

有時候需要將多個流的結(jié)果合并為一個流溜宽,這時候需要使用平整映射。

List<Integer> integers = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4))
        .flatMap(n -> n.stream())
        .collect(Collectors.toList());
System.out.println(integers);

最大值和最小值

這兩個功能不必說了质帅。需要注意的是适揉,minmax方法接受的是比較器形式的lambda表達式,當然也可以用上篇文章介紹的比較器方法來簡化比較代碼的編寫煤惩。

int min = Stream.of(1, 2, 3, 4)
        .min(Comparator.comparingInt(i -> i)).get();
int max = Stream.of(1, 2, 3, 4)
        .max(Comparator.comparingInt(i -> i)).get();
System.out.println(String.format("max:%d,min:%d", max, min));

通用迭代

有時候需要進行一種比較復雜的操作:從一個流中取前兩個元素執(zhí)行某個操作嫉嘀,然后用結(jié)果和第三個元素繼續(xù)操作,直到處理完所有元素魄揉。這種操作叫做reduce剪侮。下面的例子很簡單,求和以及求積洛退。

reduce有兩種形式票彪,第一種是取前兩個元素操作,然后將結(jié)果和第三個元素操作不狮,然后以此類推降铸。第二種是用給定的初始值和第一個元素操作,然后結(jié)果和第二個元素操作摇零。需要注意第一種形式的返回值是一個Optional對象推掸,為了得到最終的值我們需要調(diào)用get()方法。

int sum = Stream.of(1, 2, 3, 4, 5)
        .reduce((acc, e) -> acc + e)
        .get();
System.out.println(String.format("sum:%d", sum));
int product = Stream.of(1, 2, 3, 4, 5)
        .reduce(1, (acc, e) -> acc * e);
System.out.println(String.format("product:%d", product));

謂詞操作

還有一些流操作和SQL中的謂詞操作類似,可以實現(xiàn)一些判斷功能谅畅。allMatch當所有元素滿足條件時返回true登渣;anyMatch只要有一個元素滿足就會返回真;noneMatch當沒有元素滿足條件時返回真毡泻;distinct會去除流中的重復元素胜茧。

boolean allGreaterThan5 = Stream.of(1, 2, 3, 4, 5)
        .allMatch(i -> i > 5);
System.out.println("allGreaterThan5:" + allGreaterThan5);

boolean anyEqualsTo2 = Stream.of(1, 2, 3, 4, 5)
        .anyMatch(i -> i == 2);
System.out.println("anyEqualsTo2:" + anyEqualsTo2);

boolean noneLessThan0 = Stream.of(1, 2, 3, 4)
        .noneMatch(i -> i < 0);
System.out.println("noneLessThan0:" + noneLessThan0);

List<Integer> distinct = Stream.of(1, 1, 2, 3, 2, 4, 5)
        .distinct()
        .collect(Collectors.toList());

還有一些操作可以截取流的一部分,limit會保留流的前幾個元素仇味,而skip會跳過前幾個元素而獲取之后的所有元素呻顽。

String list1 = Stream.of(1, 2, 3, 4, 5)
        .limit(3)
        .map(String::valueOf)
        .collect(Collectors.joining(", ", "[", "]"));
System.out.println(list1);
String list2 = Stream.of(1, 2, 3, 4, 5)
        .skip(3)
        .map(String::valueOf)
        .collect(Collectors.joining(", ", "[", "]"));
System.out.println(list2);

基本類型流

流類庫是一個通用的框架,所以顯而易見地用到了Java泛型的技術丹墨。但是我們知道由于Java存在一個基本類型裝箱拆箱的過程廊遍,所以會有性能開銷。為了避免這些開銷贩挣,流類庫針對常見的基本類型int喉前、longdouble做了特殊處理王财,為它們單獨準備了一些類和方法卵迂。

IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
LongStream longStream = LongStream.of(1, 2, 3, 4);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0);

對于一些方法也有基本類型的版本,可以將一個對象流轉(zhuǎn)換為對應的基本類型绒净,這些方法的命名規(guī)則是方法名+To+基本類型见咒。如果需要處理大量數(shù)據(jù)的基本類型流,可以考慮使用這些方法疯溺。

int sum = Stream.of(1, 2, 3, 4)
        .mapToInt(i -> i)
        .reduce(0, (acc, e) -> acc + e);
System.out.println(String.format("sum:%d", sum));

收集器

使用流類庫的最后一步就是將流轉(zhuǎn)換為我們需要的集合了,這就需要用到收集器哎垦。收集數(shù)據(jù)的最后一步需要調(diào)用collect方法囱嫩,它的參數(shù)是java.util.stream.Collector類的靜態(tài)方法。聽起來是不是有點奇怪漏设,實際上墨闲,接受的是這些方法的返回值。例如toList()方法實際上是這樣的郑口,返回的是Collector對象鸳碧,然后由collect方法處理,獲取最后的集合犬性。

public static <T>
Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_ID);
}

獲得集合

如果需要把流轉(zhuǎn)換為集合瞻离,可以使用toList()toSet()等方法。它們會返回ListSet集合乒裆。如果需要使用特定的集合套利,可以使用toCollection方法,它的參數(shù)是一個比較特殊的函數(shù)接口,用于創(chuàng)建集合肉迫。所以這里可以直接使用構造方法引用來創(chuàng)建集合验辞。

List<Integer> integers = Stream.of(1, 2, 3, 4)
        .collect(Collectors.toList());
Set<Integer> set = Stream.of(1, 2, 3)
        .collect(Collectors.toSet());
//使用自己希望的集合
ArrayList<Integer> integers2 = Stream.of(1, 2, 3)
        .collect(Collectors.toCollection(ArrayList::new));

獲得值

收集器不僅可以獲得集合,還可以由流獲取一個值喊衫,這可以通過調(diào)用maxBy跌造、minByaverageXXXsummingXXX方法來實現(xiàn)族购。下面是一組例子壳贪。

int max = Stream.of(1, 2, 3, 4)
        .collect(Collectors.maxBy(Comparator.comparing(i -> i)))
        .get();
int min = Stream.of(1, 2, 3, 4)
        .collect(Collectors.minBy(Comparator.comparing(i -> i)))
        .get();
double average = Stream.of(1, 2, 3, 4)
        .collect(Collectors.averagingDouble(Integer::doubleValue));
int sum = Stream.of(1, 2, 3, 4)
        .collect(Collectors.summingInt(i -> i));
System.out.println(
        String.format("max:%d,min:%d,average:%f,sum:%d", max, min, average, sum));

有時候需要將流的數(shù)據(jù)組合為一個字符串,這需要joining收集器联四,它的三個參數(shù)分別是分隔符撑碴、前綴和后綴。當然由于它需要字符序列朝墩,所以這里還需要用map方法將整數(shù)流轉(zhuǎn)換為字符串流醉拓。

String string = Stream.of(1, 2, 3, 4, 5)
        .map(String::valueOf)
        .collect(Collectors.joining(", ", "[", "]"));
System.out.println(string);
// 結(jié)果: [1, 2, 3, 4, 5]

還有一個簡單的收集器,作用就是計數(shù)收苏,需要注意的是計數(shù)返回的結(jié)果是long類型就行了亿卤。這個收集器的主要作用是和其他收集器一起完成復雜的功能。

long count = Stream.of(1, 2, 3)
        .collect(Collectors.counting());
System.out.println("count:" + count);
// 結(jié)果:3

數(shù)據(jù)分塊

數(shù)據(jù)分塊允許你給定一個條件鹿霸,然后收集器會按照這個條件將流分為滿足條件和不滿足條件的兩個部分排吴,這個收集器的返回結(jié)果是一個Map<Boolean, List<T>>。下面的例子將流分為了奇數(shù)和偶數(shù)兩個部分懦鼠。

Map<Boolean, List<Integer>> map = Stream.of(1, 2, 3, 4, 5)
        .collect(Collectors.partitioningBy(i -> i % 2 == 0));
System.out.println(map);
// 結(jié)果
// {false=[1, 3, 5], true=[2, 4]}

數(shù)據(jù)分組

數(shù)據(jù)分塊只能分為真假兩種情況钻哩,如果需要更細分的話,需要使用數(shù)據(jù)分組肛冶。這個大概類似于SQL中的group by語句街氢。下面的例子將流按照數(shù)組個位數(shù)分為好幾組。

Map<Integer, List<Integer>> map = Stream.of(21, 32, 43, 54, 11, 33, 22)
        .collect(Collectors.groupingBy(i -> i % 10));
System.out.println(map);
// 結(jié)果
// {1=[21, 11], 2=[32, 22], 3=[43, 33], 4=[54]}

組合收集器

如果問題比較復雜睦袖,還可以將多個收集器組合起來使用珊肃,一些收集器有重載的版本,支持第二個收集器馅笙,可以用來實現(xiàn)這個功能伦乔。就拿前面那個數(shù)據(jù)分組的例子來說,這次我不僅要分組董习,而且只需要每組的十位數(shù)字烈和,那么就可以這樣寫。groupingBy的第二個參數(shù)可以使用mapping收集器皿淋,mapping這里的作用和流操作的map類似斥杜,將流再次進行映射虱颗,然后收集結(jié)果作為最后Map的鍵值對。

//按個位數(shù)字分組蔗喂,然后只獲取十位數(shù)字
Map<Integer, List<Integer>> map = Stream.of(21, 32, 43, 54, 11, 33, 22)
        .collect(Collectors.groupingBy(i -> i % 10,
                Collectors.mapping(i -> i / 10, Collectors.toList())));
System.out.println(map);
// {1=[2, 1], 2=[3, 2], 3=[4, 3], 4=[5]}

再舉一個例子忘渔,現(xiàn)在我希望對每組的結(jié)果進行求值。就可以使用另外一個收集器summingXXX缰儿。還有一些收集器可以完成求平均數(shù)等的操作畦粮,這里就不一一列舉了。

//按個位數(shù)字分組乖阵,然后求各組的和
Map<Integer, Integer> map2 = Stream.of(21, 32, 43, 54, 11, 33, 22)
        .collect(Collectors.groupingBy(i -> i % 10,
                Collectors.summingInt(i -> i)));
System.out.println(map2);
// {1=32, 2=54, 3=76, 4=54}

另外一點內(nèi)容

還有一點知識點不知道該怎么說宣赔,干脆都放在這里好了。

惰性求值

流類庫設計的非常精巧瞪浸,也對性能做了很多優(yōu)化儒将。所有的流操作都是惰性的,也就是說直到最后調(diào)用收集器的時候对蒲,整個流操作才開始進行钩蚊。在這之前,你的流操作只不過類似于SQL的執(zhí)行計劃蹈矮,這時候還沒有真正執(zhí)行程序砰逻。所以下面的代碼什么都不會輸出。

Stream.of(1, 2, 3, 4, 5)
        .filter(i -> i > 1)
        .filter(i -> {
            System.out.print(i);
            return i <= 3;
        });

單次循環(huán)

如果進行了很多流操作泛鸟,流類庫會不會對流進行多次迭代蝠咆,導致程序速度很慢呢?這個擔心也是多余的北滥,流類庫經(jīng)過優(yōu)化刚操,會保證迭代以最少的次數(shù)進行。所以下面的代碼輸出結(jié)果是122334455再芋,這說明流只迭代了一次菊霜。

List<Integer> integers = Stream.of(1, 2, 3, 4, 5)
        .filter(i -> {
            System.out.print(i);
            return i > 1;
        })
        .filter(i -> {
            System.out.print(i);
            return i <= 3;
        })
        .collect(Collectors.toList());
System.out.println();

新的for循環(huán)

原來,我們?nèi)绻枰M行一定次數(shù)的循環(huán)祝闻,需要使用for來做到占卧。

//傳統(tǒng)for循環(huán)
for (int i = 0; i < 3; ++i) {
    System.out.print(i);
}
System.out.println();

現(xiàn)在利用流類庫的range方法遗菠,我們可以簡化這一點联喘。

IntStream.range(0, 3)
        .forEach(i -> System.out.print(i));
System.out.println();

數(shù)據(jù)并行化

由于流類庫的特性,所以讓流辙纬;并行化非常容易豁遭,只需要調(diào)用parrallel方法即可。數(shù)據(jù)的分割贺拣、結(jié)果的合并都會由類庫自動完成蓖谢。

List<Integer> integers = IntStream.range(1, 101)
        .parallel()
        .filter(i -> i % 2 == 0)
        .boxed()
        .collect(Collectors.toList());

需要注意并不是說并行化之后捂蕴,速度就一定會比串行化快,這需要根據(jù)當前系統(tǒng)闪幽、機器啥辨、執(zhí)行的數(shù)據(jù)流大小來進行綜合評估。ArrayList這類容器就比較容易并行化盯腌,而HashMap并行化就比較困難溉知。

總之,并行化這個主題比較復雜腕够,這里就不詳細討論了级乍。

最后推薦一本關于Java函數(shù)式編程的書籍,這本書對于Java 8的函數(shù)式編程做了很多介紹帚湘,我覺得很不錯玫荣。

Java 8 函數(shù)式編程
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市大诸,隨后出現(xiàn)的幾起案子捅厂,更是在濱河造成了極大的恐慌,老刑警劉巖底挫,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恒傻,死亡現(xiàn)場離奇詭異,居然都是意外死亡建邓,警方通過查閱死者的電腦和手機盈厘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來官边,“玉大人沸手,你說我怎么就攤上這事∽⒉荆” “怎么了契吉?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長诡渴。 經(jīng)常有香客問我捐晶,道長,這世上最難降的妖魔是什么妄辩? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任惑灵,我火速辦了婚禮,結(jié)果婚禮上眼耀,老公的妹妹穿的比我還像新娘英支。我一直安慰自己,他們只是感情好哮伟,可當我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布干花。 她就那樣靜靜地躺著妄帘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪池凄。 梳的紋絲不亂的頭發(fā)上抡驼,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天,我揣著相機與錄音肿仑,去河邊找鬼婶恼。 笑死,一個胖子當著我的面吹牛柏副,可吹牛的內(nèi)容都是我干的勾邦。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼割择,長吁一口氣:“原來是場噩夢啊……” “哼眷篇!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荔泳,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤蕉饼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后玛歌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昧港,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年支子,在試婚紗的時候發(fā)現(xiàn)自己被綠了创肥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡值朋,死狀恐怖叹侄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情昨登,我是刑警寧澤趾代,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站丰辣,受9級特大地震影響撒强,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜笙什,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一飘哨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧得湘,春花似錦杖玲、人聲如沸顿仇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鸿吆,卻和暖如春囤采,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惩淳。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工蕉毯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人思犁。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓代虾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親激蹲。 傳聞我的和親對象是個殘疾皇子棉磨,可洞房花燭夜當晚...
    茶點故事閱讀 45,747評論 2 361

推薦閱讀更多精彩內(nèi)容