前面介紹了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);
最大值和最小值
這兩個功能不必說了质帅。需要注意的是适揉,min
和max
方法接受的是比較器形式的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
喉前、long
、double
做了特殊處理王财,為它們單獨準備了一些類和方法卵迂。
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()
等方法。它們會返回List
和Set
集合乒裆。如果需要使用特定的集合套利,可以使用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
跌造、minBy
、averageXXX
和summingXXX
方法來實現(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ù)式編程做了很多介紹帚湘,我覺得很不錯玫荣。