Java8————Stream API
引言
Java8 加入了java.util.stream包,這個包中的相關(guān)API將極大的增強容器對象對元素的操作能力。
它專注于對集合對象進行各種便利、高效的聚合操作,或大批量數(shù)據(jù)處理。
Stream API借助于同樣新出現(xiàn)的Lambda表達式,極大的提高了編程效率和程序信噪比。
它提供了串行和并行兩種模式進行匯聚操作。并行模式底層采用 Fork / Join 框架來拆分任務和加速處理過程。
什么是流?
一、流的概念
1、流不是數(shù)據(jù)結(jié)構(gòu)
它沒有內(nèi)部存儲,它只是用操作管道從source(數(shù)據(jù)結(jié)構(gòu)、數(shù)組、IO channel)抓取數(shù)據(jù)。
2、不修改源數(shù)據(jù)
例如Stream 的 filter操作會產(chǎn)生一個不包含被過濾元素的新的Stream,而不是從source中刪除那些元素。
3、流的操作參數(shù)
所有的Stream操作必須以Lambda表達式作為參數(shù)。
4、不支持索引訪問
Stream操作實際上是?增強For循環(huán) 的函數(shù)編程變式,它沒有元素下標的訪問方式。
5、流可以轉(zhuǎn)換成數(shù)組或者List
6、惰性化
Intermediate操作永遠是惰性化的
7、并行能力
當一個集合不要求元素的順序時,我們可以通過Stream的并行化特性來充分利用多核資源,不需要再寫多線程代碼,所有對它的操作會自動并行進行。
8、可以是無限的
集合有固定大小,Stream則不必,limit(n)、findFirst()這類short-circuiting操作可以對無限的Stream進行運算并很快完成。
二、流的操作分類
流的操作類型被分為三種:Intermediate、Terminal、short-circuiting 。
Intermediate :代表流的中間操作,這種操作的目的主要是打開流,做出某種程度的映射或過濾,然后返回一個新的流,交給下一個操作使用。這類操作是惰性的,也就是說,僅僅調(diào)用到這類方法,并沒有真正開始流的遍歷。
Terminal :一個流只能有一個Terminal操作。所以這必定是流的最后一個操作。而Terminal操作的執(zhí)行,才會真正開始流的遍歷,并且會生成一個結(jié)果,或者一個副作用。
short-circuiting?:對于Intermediate 操作,如果接收的是一個無限大的Stream,則返回一個有限的新Stream;對于Terminal 操作,如果它接收的是一個無限大的Stream,但能在有限的時間計算出結(jié)果。
三、惰性化(lazy)
我們說Intermediate操作都是惰性化的,這如何理解?在對于一個Stream進行多次轉(zhuǎn)換操作(Intermediate操作),每次操作都對Stream中的每個元素進行轉(zhuǎn)換,而且是執(zhí)行多次,這樣時間復雜度就是 N(轉(zhuǎn)換次數(shù))個for循環(huán)里所有操作都執(zhí)行完的總和嗎?其實不是這樣的!
我們說轉(zhuǎn)換操作是Lazy的,多個轉(zhuǎn)換操作只會在Terminal 操作的時候融合進來,一次循環(huán)完成。
我們可以這樣簡單的類比,在Java 8 未引進Stream API的時候,使用命令式進行for循環(huán),并對每個元素進行諸如 if-else 、賦值、計算、獲取、添加等操作,而這些操作你可以理解為Stream中的Intermediate操作,只有在for循環(huán)真正執(zhí)行的時候才會執(zhí)行它們,這就是惰性化的語義,即提前安排好篩選、計算等Intermediate操作,當循環(huán)時再執(zhí)行它們。
常見用法歸納
一、創(chuàng)建流
我們可以通過一個“集合”對象來創(chuàng)建流,這個集合對象并不限于Collection接口,還包含那些能容納多個對象的容器。
創(chuàng)建流的方式大致可以分為三種:Arrays.stream()、Stream.of()、集合.stream(),下面舉例來說明:
1、數(shù)組生成流
// 基本類型數(shù)組 int[] numx = new int[] { 1, 2, 2, 3, 5 }; // 方法一: IntStream stream1 = Arrays.stream(numx);// 方法二: IntStream stream2 = IntStream.of(numx);// 引用類型數(shù)組 Integer[] nums = new Integer[] { 1, 2, 2, 3, 5 };// 方法一: Stream<Integer> stream3 = Arrays.stream(nums);// 方法二: Stream<Integer> stream4 = Stream.of(nums);需要注意的是,對于基本數(shù)值型,目前有三種對應的包裝類型 Stream:
IntStream、LongStream、DoubleStream。當然我們也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 會很耗時,所以特別為這三種基本數(shù)值型提供了對應的 Stream。?Java 8 中還沒有提供其它數(shù)值型 Stream,因為這將導致擴增的內(nèi)容較多。而常規(guī)的數(shù)值型聚合運算可以通過上面三種 Stream 進行。
?2、集合生成流
List<Integer> numsList = Arrays.asList( 1, 2, 2, 3, 5 ); // 使用parallelStream會將List進行分段并行處理,因此處理的順序是不固定的。 Stream<Integer> parallelStream = numsList.parallelStream();二、流轉(zhuǎn)化為容器(Terminal)
Stream<String> names = Arrays.asList("Tom", "Jerry", "Tim", "Morty").stream();1、Stream轉(zhuǎn)Array
String[] namesArr = names.toArray(String[]::new);2、?Stream轉(zhuǎn)Collection
List<String> list1 = names.collect(Collectors.toList()); // 或 List<String> list2 = names.collect(Collectors.toCollection(ArrayList::new)); Set<String> set = names.collect(Collectors.toSet()); Stack<String> stack = names.collect(Collectors.toCollection(Stack::new));3、Stream轉(zhuǎn)String
String str = names.collect(Collectors.joining());// joining()有重載?三、映射操作(Intermediate)
map將input stream中的每一個元素,映射成output? stream中的另外一個元素(一對一映射)
List<String> output = names.map(String::toUpperCase).collect(Collectors.toList());四、多集合映射操作(Intermediate)
flatMap(功能和map相同,只不過映射是一對多),flatMap 把 input Stream 中的層級結(jié)構(gòu)扁平化,就是將最底層元素抽出來放到一起。
List<String> names1 = Arrays.asList("Tom", "Jerry", "Tim", "Morty"); List<String> names2 = Arrays.asList("Tony", "Jack", "Tina", "Marry"); List<String> collect = Stream.of(names1, names2).flatMap(ns -> ns.stream().map(String::toLowerCase)).collect(Collectors.toList());五、篩選操作(Intermediate)
filter對原始 Stream 進行某項測試,符合條件表達式的元素被留下來生成一個新 Stream 或集合。
Integer[] sixNums = { 1, 2, 3, 4, 5, 6 }; Integer[] evens = Stream.of(sixNums).filter(n -> n % 2 == 0).toArray(Integer[]::new);六、循環(huán)操作(Terminal)
forEach() 方法接收一個 Lambda 表達式,然后在 Stream 的每一個元素上執(zhí)行該表達式。
但一般認為,forEach 和常規(guī) for 循環(huán)的差異不涉及到性能,它們僅僅是函數(shù)式風格與傳統(tǒng) Java 風格的差別.
注意:forEach 不能修改自己包含的本地變量值,也不能用 break/return 之類的關(guān)鍵字提前結(jié)束循環(huán)。
當需要為多核系統(tǒng)優(yōu)化時,可以 parallelStream().forEach()。另外一點需要注意,forEach 是 terminal 操作。具有相似功能的 intermediate 操作 peek 可以達到上述目的。
Stream.of("one", "two", "three", "four").filter(e -> e.length() > 3).peek(e -> System.out.println("Filtered value: " + e)).map(String::toUpperCase).peek(e -> System.out.println("Mapped value: " + e)).collect(Collectors.toList());七、第一個元素(Terminal)
findFirst是一個 termimal 兼 short-circuiting 操作,它總是返回 Stream 的第一個元素,或者空。
注意,它的返回值類型:Optional。使用Optional的目的是盡可能避免 NullPointerException。它提供的是編譯時檢查,能極大的降低 NPE 這種 Runtime Exception 對程序的影響。
Optional<String> firstName = names2.stream().findFirst();八、聚合操作(Terminal)
reduce方法的主要作用是把 Stream 元素組合起來。它提供一個起始值(種子),然后依照運算規(guī)則(BinaryOperator),和前面 Stream 的第一個、第二個、第 n 個元素組合。從這個意義上說,字符串拼接、數(shù)值的 sum、min、max、average 都是特殊的 reduce。
下面代碼例如第一個示例的 reduce(),第一個參數(shù)(空白字符)即為起始值,第二個參數(shù)(String::concat)為 BinaryOperator。這類有起始值的 reduce() 都返回具體的對象。而對于第四個示例沒有起始值的 reduce(),由于可能沒有足夠的元素,返回的是 Optional,請留意這個區(qū)別。
// 字符串連接,concat = "ABCD" String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat); // 求最小值,minValue = -3.0 double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min); // 求和,sumValue = 10, 有起始值 int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum); // 求和,sumValue = 10, 無起始值 sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get(); // 過濾,字符串連接,concat = "ace" String s = Stream.of("a", "B", "c", "D", "e", "F").filter(x -> x.compareTo("Z") > 0).reduce("", String::concat);九、limit/skip (Short-circuiting)
limit 返回 Stream 的前面 n 個元素;skip 則是扔掉前 n 個元素(它是由一個叫 subStream 的方法改名而來)。
List<String> persons = new ArrayList<>(); for (int i = 1; i <= 10000; i++) {persons.add(new String("name" + i)); } List<String> personNameList = persons.stream().map(String::toUpperCase).limit(10).skip(3).collect(Collectors.toList());?上述代碼是一個有 10000 個元素的 Stream,但在 short-circuiting 操作 limit 和 skip 的作用下,管道中 map 操作指定的toUpperCase()方法的執(zhí)行次數(shù)為 limit 所限定的 10 次,而最終返回結(jié)果再跳過前 3 個元素后只有后面 7 個返回。
執(zhí)行結(jié)果:
注意,有一種情況 limit/skip 無法達到 short-circuiting 目的,就是把它們放在 Stream 的排序操作后,原因跟 sorted這個 intermediate 操作有關(guān):此時系統(tǒng)并不知道 Stream 排序后的次序如何,所以 sorted 中的操作看上去就像完全沒有被 limit 或者 skip 一樣。
經(jīng)典案例:內(nèi)存全量數(shù)據(jù)實現(xiàn)分頁返回:
如下代碼所示,page 從 1 開始,分別翻頁查詢,可以得到完美的分頁輸出結(jié)果。
其中skip代表一個偏移量,limit代表輸出限制,由此方式達到了 SQL 語句中 limit x, y 的效果。
public class TestMemoryPage {public static final List<String> totalNameList = new ArrayList<>();static {for (int i = 1; i <= 100; i++) {totalNameList.add("name"+ i);}}public static void main(String[] args) {// 可自由調(diào)整分頁大小int pageSize = 9;int total = totalNameList.size();int totalPage = (total + pageSize - 1) / pageSize;System.out.println(totalNameList);System.out.println("pageSize = "+pageSize+", total = "+total+", totalPage = "+totalPage);// 分頁開始for (int page = 1; page <= totalPage; page++) {List<String> pageList = totalNameList.stream().skip((page - 1) * pageSize).limit(pageSize).collect(Collectors.toList());System.out.println("第"+page+"頁,數(shù)量:"+pageList.size()+" 數(shù)據(jù):" + pageList);}} }十、排序操作(Intermediate)
對 Stream 的排序通過 sorted 進行,它比數(shù)組的排序更強之處在于你可以首先對 Stream 進行各類 map、filter、limit、skip 甚至 distinct 來減少元素數(shù)量后,再排序,這能幫助程序明顯縮短執(zhí)行時間。
List<String> pList = persons.stream().limit(2).sorted((p1, p2) -> p1.toString().compareTo(p2.toString())).collect(Collectors.toList());?十一、最大/最小值、去重操作(Intermediate)
min 和 max 的功能也可以通過對 Stream 元素先排序,再 findFirst 來實現(xiàn),但前者的性能會更好,為 O(n),而 sorted 的成本是 O(n log n)。同時它們作為特殊的 reduce 方法被獨立出來也是因為求最大最小值是很常見的操作。
BufferedReader br = new BufferedReader(new FileReader("c:\\noThisFile.txt")); int longest = br.lines().mapToInt(String::length).max().getAsInt();br.close(); System.out.println(longest);十二、匹配操作(Terminal)
Stream 有三個 match 方法,從語義上說:
allMatch:Stream 中全部元素符合傳入的 predicate,返回 true?
anyMatch:Stream中只要有一個元素符合傳入的 predicate,返回 true
noneMatch:Stream 中沒有一個元素符合傳入的predicate,返回 true
它們都不是要遍歷全部元素才能返回結(jié)果。例如 allMatch 只要一個元素不滿足條件,就 skip 剩下的所有元素,返回 false。
List<Person> persons = new ArrayList(); persons.add(new Person(1, "name" + 1, 10)); persons.add(new Person(2, "name" + 2, 21)); persons.add(new Person(3, "name" + 3, 34)); persons.add(new Person(4, "name" + 4, 6)); persons.add(new Person(5, "name" + 5, 55)); boolean isAllAdult = persons.stream().allMatch(p -> p.getAge() > 18); System.out.println("All are adult? " + isAllAdult); boolean isThereAnyChild = persons.stream().anyMatch(p -> p.getAge() < 12); System.out.println("Any child? " + isThereAnyChild);十三、常用聚合函數(shù)案例
數(shù)據(jù)準備,聲明一個User對象,然后初始化一個user 列表:
@Data @AllArgsConstructor public static class User implements Jsonable {private Long id;private String name;private Integer age;private String address;private String group;@Overridepublic String toString() {return this.toJsonStr();} }public static final List<User> users = new ArrayList<>();static {User morty = new User(1L, "morty", 28, "昌平區(qū)天通中苑", "研發(fā)組");User tom = new User(2L, "tom", 24, "朝陽區(qū)將臺街道", "研發(fā)組");User lucy = new User(3L, "lucy", 22, "朝陽區(qū)美景東方小區(qū)", "測試組");users.add(morty);users.add(tom);users.add(lucy); }13.1 提取某一列
List<String> names = users.stream().map(User::getName).collect(Collectors.toList()); System.out.println("names = " + names);// names = [morty, tom, lucy]13.2?連接某一列
String joiningNames = users.stream().map(User::getName).collect(Collectors.joining("_")); System.out.println("joiningNames = " + joiningNames);// joiningNames = morty_tom_lucy13.3?映射表
Map<Long, User> userMapping = users.stream().collect(Collectors.toMap(User::getId, o -> o)); System.out.println("userMapping = " + userMapping);// userMapping = {1={"address":"昌平區(qū)天通中苑","name":"morty","id":1,"age":28,"group":"研發(fā)組"}, 2={"address":"朝陽區(qū)將臺街道","name":"tom","id":2,"age":24,"group":"研發(fā)組"}, 3={"address":"朝陽區(qū)美景東方小區(qū)","name":"lucy","id":3,"age":22,"group":"測試組"}}13.4 按某列分組
Map<String, List<User>> userGroups = users.stream().collect(Collectors.groupingBy(User::getGroup)); System.out.println("userGroups = " + userGroups);// userGroups = {測試組=[{"address":"朝陽區(qū)美景東方小區(qū)","name":"lucy","id":3,"age":22,"group":"測試組"}], 研發(fā)組=[{"address":"昌平區(qū)天通中苑","name":"morty","id":1,"age":28,"group":"研發(fā)組"}, {"address":"朝陽區(qū)將臺街道","name":"tom","id":2,"age":24,"group":"研發(fā)組"}]}鳴謝
《Java 8 中的 Streams API 詳解》
總結(jié)
以上是生活随笔為你收集整理的Java8————Stream API的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高并发负载均衡——网络协议原理
- 下一篇: Maven配置_01