一、Java 8 Stream 介紹
Java8 API 添加了一個新的抽象稱為 流Stream,可以讓你以一種聲明的方式處理數據。Stream 使用一種類似用 SQL 語句從數據庫查詢數據的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。
Stream 這種風格將要處理的元素集合看作一種流,在流的過程中,流在管道中傳輸, 並且可以在管道的節點上進行處理,借助 Stream API 對流中的元素進行操作,比如:篩選、排序、聚合等。
元素流在管道中經過中間操作(intermediate operation)的處理,最后由最終操作(terminal operation)得到前面處理的結果。
1、什么是 Stream:Stream(流)是一個來自數據源的元素隊列並支持聚合操作
元素是特定類型的對象,形成一個隊列。 Java中的Stream並不會存儲元素,而是按需計算。
數據源: 流的來源,可以是集合,數組,I/O channel, 產生器generator 等。
聚合操作:類似SQL語句一樣的操作, 比如 filter, map, reduce, find, match, sorted 等。
和以前的Collection操作不同, Stream操作還有兩個基礎的特征:
(1)Pipelining: 中間操作都會返回流對象本身。 這樣多個操作可以串聯成一個管道, 如同流式風格(fluent style)。 這樣做可以對操作進行優化, 比如延遲執行(laziness)和短路( short-circuiting)。
(2)內部迭代: 以前對集合遍歷都是通過Iterator或者For-Each的方式, 顯式的在集合外部進行迭代, 這叫做外部迭代。 Stream提供了內部迭代的方式, 通過訪問者模式(Visitor)實現。
2、生成流:在 Java 8 中,集合接口有兩個方法來生成流:
(1)stream() − 為集合創建串行流。
(2)parallelStream() − 為集合創建並行流。
3、常用方法
forEach:Stream 提供了新的方法 'forEach' 來迭代流中的每個數據。
map 方法用於映射每個元素到對應的結果。
filter 方法用於通過設置的條件過濾出元素。
limit 方法用於獲取指定數量的流。 以下代碼片段使用 limit 方法打印出 10 條數據
Random random = new Random(); random.ints().limit(10).forEach(System.out::println);
sorted 方法用於對流進行排序
二、操作符
1、什么是操作符呢?
操作符就是對數據進行的一種處理工作,一道加工程序;就好像工廠的工人對流水線上的產品進行一道加工程序一樣。Stream的操作符大體上分為兩種:中間操作符和終止操作符
2、中間操作符 —— 對於數據流來說,中間操作符在執行制定處理程序后,數據流依然可以傳遞給下一級的操作符。中間操作符包含8種(排除了parallel,sequential,這兩個操作並不涉及到對數據流的加工操作):
(1)map(mapToInt,mapToLong,mapToDouble) 轉換操作符,把比如A->B,這里默認提供了轉int,long,double的操作符。
(2)flatmap(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作比如把 int[]{2,3,4} 拍平 變成 2,3,4 也就是從原來的一個數據變成了3個數據,這里默認提供了拍平成int,long,double的操作符。
(3)limit 限流操作,比如數據流中有10個 我只要出前3個就可以使用。
(4)distint 去重操作,對重復元素去重,底層使用了equals方法。
(5)filter 過濾操作,把不想要的數據過濾。
(6)peek 挑出操作,如果想對數據進行某些操作,如:讀取、編輯修改等。
(7)skip 跳過操作,跳過某些元素。
(8)sorted(unordered) 排序操作,對元素排序,前提是實現Comparable接口,當然也可以自定義比較器。
3、終止操作符 —— 數據經過中間加工操作,就輪到終止操作符上場了;
終止操作符就是用來對數據進行收集或者消費的,數據到了終止操作這里就不會向下流動了,終止操作符只能使用一次。
(1)collect 收集操作,將所有數據收集起來,這個操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以說Stream 的核心在於Collectors。
(2)count 統計操作,統計最終的數據個數。
(3)findFirst、findAny 查找操作,查找第一個、查找任何一個 返回的類型為Optional。
(4)noneMatch、allMatch、anyMatch 匹配操作,數據流中是否存在符合條件的元素 返回值為bool 值。
(5)min、max 最值操作,需要自定義比較器,返回數據流中最大最小的值。
(6)reduce 規約操作,將整個數據流的值規約為一個值,count、min、max底層就是使用reduce。
(7)forEach、forEachOrdered 遍歷操作,這里就是對最終的數據進行消費了。
(8)toArray 數組操作,將數據流的元素轉換成數組。
總體來說使用並不陌生,跟數組類的使用挺像的。具體代碼示例可以看這篇文章:https://www.jianshu.com/p/11c925cdba50
三、stream 的創建
1、創建流 stream 的方式
// 1、通過 java.util.Collection.stream() 方法用集合創建流
List<String> list = Arrays.asList("a", "b", "c"); // 創建一個順序流
Stream<String> stream = list.stream(); // 創建一個並行流
Stream<String> parallelStream = list.parallelStream(); // 2、使用java.util.Arrays.stream(T[] array)方法用數組創建流
int[] array={1,3,5,6,8}; IntStream stream = Arrays.stream(array); // 3、使用Stream的靜態方法:of()、iterate()、generate()
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6); Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4); stream2.forEach(System.out::println); // 0 3 6 9
Stream<Double> stream3 = Stream.generate(Math::random).limit(3); stream3.forEach(System.out::println);
2、stream 和 parallelStream 的簡單區分:
stream 是順序流,由主線程按順序對流執行操作; parallelStream 是並行流,內部以多線程並行執行的方式對流進行操作,但前提是流中的數據處理沒有順序要求。例如篩選集合中的奇數,兩者的處理不同之處
四、Stream 使用代碼示例
Stream 也是支持類似集合的遍歷和匹配元素的,只是Stream中的元素是以Optional類型存在的。Stream的遍歷、匹配非常簡單。
List<Integer> list = Arrays.asList(7, 6, 9, 3, 8, 2, 1); // 遍歷輸出符合條件的元素:先用 filter 過濾,再用 forEach 遍歷(跟數組用法挺像)
list.stream().filter(x -> x > 6).forEach(System.out::println); // 匹配第一個
Optional<Integer> findFirst = list.stream().filter(x -> x > 6).findFirst(); // 匹配任意(適用於並行流)
Optional<Integer> findAny = list.parallelStream().filter(x -> x > 6).findAny(); // 是否包含符合特定條件的元素
boolean anyMatch = list.stream().anyMatch(x -> x < 6);
2、篩選(filter):篩選,是按照一定的規則校驗流中的元素,將符合條件的元素提取到新的流中的操作。
3、聚合(max/min/count)
max、min、count這些字眼你一定不陌生,沒錯,在mysql中我們常用它們進行數據統計。Java stream中也引入了這些概念和用法,極大地方便了我們對集合、數組的數據統計工作
(1)獲取String集合中最長的元素
public class StreamTest { public static void main(String[] args) { List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd"); Optional<String> max = list.stream().max(Comparator.comparing(String::length)); System.out.println("最長的字符串:" + max.get()); } }
(2)計算Integer集合中大於6的元素的個數
public class StreamTest { public static void main(String[] args) { List<Integer> list = Arrays.asList(7, 6, 4, 8, 2, 11, 9); long count = list.stream().filter(x -> x > 6).count(); System.out.println("list中大於6的元素個數:" + count); } }
4、映射(map/flatMap):映射可以將一個流的元素按照一定的映射規則映射到另一個流中。分為map和flatMap:
- map:接收一個函數作為參數,該函數會被應用到每個元素上,並將其映射成一個新的元素。
- flatMap:接收一個函數作為參數,將流中的每個值都換成另一個流,然后把所有流連接成一個流。
public class StreamTest { public static void main(String[] args) { String[] strArr = { "abcd", "bcdd", "defde", "fTr" }; List<String> strList = Arrays.stream(strArr).map(String::toUpperCase).collect(Collectors.toList()); System.out.println("每個元素大寫:" + strList); List<Integer> intList = Arrays.asList(1, 3, 5, 7, 9, 11); List<Integer> intListNew = intList.stream().map(x -> x + 3).collect(Collectors.toList()); System.out.println("每個元素+3:" + intListNew); } }
(2)將兩個字符數組合並成一個新的字符數組。
public class StreamTest { public static void main(String[] args) { List<String> list = Arrays.asList("m,k,l,a", "1,3,5,7"); List<String> listNew = list.stream().flatMap(s -> { // 將每個元素轉換成一個stream
String[] split = s.split(","); Stream<String> s2 = Arrays.stream(split); return s2; }).collect(Collectors.toList()); System.out.println("處理前的集合:" + list); System.out.println("處理后的集合:" + listNew); } }
5、歸約(reduce):歸約,也稱縮減,顧名思義,是把一個流縮減成一個值,能實現對集合求和、求乘積和求最值操作。
List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4); // 求和方式
Optional<Integer> sum = list.stream().reduce(Integer::sum);
Integer sum3 = list.stream().reduce(0, Integer::sum);
// 求乘積
Optional<Integer> product = list.stream().reduce((x, y) -> x * y);
// 求最大值方式
Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y);
Integer max2 = list.stream().reduce(1, Integer::max);
6、歸集(toList/toSet/toMap):因為流不存儲數據,那么在流中的數據完成處理后,需要將流中的數據重新歸集到新的集合里。toList、toSet和toMap比較常用,另外還有toCollection、toConcurrentMap等復雜一些的用法。
List<Integer> list = Arrays.asList(1, 6, 3, 4, 6, 7, 9, 6, 20);
List<Integer> listNew = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList()); Set<Integer> set = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toSet());
List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); Map<?, Person> map = personList.stream().filter(p -> p.getSalary() > 8000) .collect(Collectors.toMap(Person::getName, p -> p));
7、統計(count/averaging):Collectors提供了一系列用於數據統計的靜態方法:
- 計數:count
- 平均值:averagingInt、averagingLong、averagingDouble
- 最值:maxBy、minBy
- 求和:summingInt、summingLong、summingDouble
- 統計以上所有:summarizingInt、summarizingLong、summarizingDouble
8、分組(partitioningBy/groupingBy)
- 分區:將stream按條件分為兩個Map,比如員工按薪資是否高於8000分為兩部分。
- 分組:將集合分為多個Map,比如員工按性別分組。有單級分組和多級分組。
List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "Washington")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "New York")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); // 將員工按薪資是否高於8000分組 Map<Boolean, List<Person>> part = personList.stream()
.collect(Collectors.partitioningBy(x -> x.getSalary() > 8000)); // 將員工按性別分組 Map<String, List<Person>> group = personList.stream()
.collect(Collectors.groupingBy(Person::getSex)); // 將員工先按性別分組,再按地區分組 Map<String, Map<String, List<Person>>> group2 = personList.stream()
.collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea)));
// 結果 // 員工按薪資是否大於8000分組情況: { false=[ Person{name='Jack', salary=7000, age=25, sex='male', area='Washington'},
Person{name='Lily', salary=7800, age=21, sex='female', area='New York'}
],
true=[
Person{name='Tom', salary=8900, age=23, sex='male', area='Washington'},
Person{name='Anni', salary=8200, age=24, sex='female', area='New York'}
]
} // 員工按性別分組情況: {
female=[
Person{name='Lily', salary=7800, age=21, sex='female', area='New York'},
Person{name='Anni', salary=8200, age=24, sex='female', area='New York'}
],
male=[
Person{name='Tom', salary=8900, age=23, sex='male', area='Washington'},
Person{name='Jack', salary=7000, age=25, sex='male', area='Washington'}
]
} // 員工按性別、地區: {
female={
New York=[
Person{name='Lily', salary=7800, age=21, sex='female', area='New York'},
Person{name='Anni', salary=8200, age=24, sex='female', area='New York'}
]
},
male={
Washington=[
Person{name='Tom', salary=8900, age=23, sex='male', area='Washington'},
Person{name='Jack', salary=7000, age=25, sex='male', area='Washington'}
]
}
}
9、接合(joining):joining可以將stream中的元素用特定的連接符(沒有的話,則直接連接)連接成一個字符串。
- sorted():自然排序,流中元素需實現Comparable接口
- sorted(Comparator com):Comparator排序器自定義排序
List<Person> personList = new ArrayList<Person>();// 按工資升序排序(自然排序)
List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)).map(Person::getName) .collect(Collectors.toList()); // 按工資倒序排序
List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed()) .map(Person::getName).collect(Collectors.toList()); // 先按工資再按年齡升序排序
List<String> newList3 = personList.stream() .sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)).map(Person::getName) .collect(Collectors.toList()); // 先按工資再按年齡自定義排序(降序)- 自定義排序
List<String> newList4 = personList.stream().sorted((p1, p2) -> { if (p1.getSalary() == p2.getSalary()) { return p2.getAge() - p1.getAge(); } else { return p2.getSalary() - p1.getSalary(); } }).map(Person::getName).collect(Collectors.toList());
11、提取/組合:流也可以進行合並、去重、限制、跳過等操作。
String[] arr1 = { "a", "b", "c", "d" }; String[] arr2 = { "d", "e", "f", "g" }; Stream<String> stream1 = Stream.of(arr1); Stream<String> stream2 = Stream.of(arr2); // concat:合並兩個流 distinct:去重
List<String> newList = Stream.concat(stream1, stream2).distinct().collect(Collectors.toList()); // limit:限制從流中獲得前n個數據
List<Integer> collect = Stream.iterate(1, x -> x + 2).limit(10).collect(Collectors.toList()); // skip:跳過前n個數據
List<Integer> collect2 = Stream.iterate(1, x -> x + 2).skip(1).limit(5).collect(Collectors.toList());
12、分頁操作:stream api 的強大之處還不僅僅是對集合進行各種組合操作,還支持分頁操作。
例如,將如下的數組從小到大進行排序,排序完成之后,從第1行開始,查詢10條數據出來,操作如下:
//需要查詢的數據
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5, 10, 6, 20, 30, 40, 50, 60, 100); List<Integer> dataList = numbers.stream().sorted(Integer::compareTo).skip(0).limit(10).collect(Collectors.toList()); System.out.println(dataList.toString());
13、並行操作
所謂並行,指的是多個任務在同一時間點發生,並由不同的cpu進行處理,不互相搶占資源;而並發,指的是多個任務在同一時間點內同時發生了,但由同一個cpu進行處理,互相搶占資源。
stream api 的並行操作和串行操作,只有一個方法區別,其他都一樣,例如下面我們使用parallelStream來輸出空字符串的數量:
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl"); // 采用並行計算方法,獲取空字符串的數量
long count = strings.parallelStream().filter(String::isEmpty).count(); System.out.println(count);
注意:在實際使用的時候,並行操作不一定比串行操作快!對於簡單操作,數量非常大,同時服務器是多核的話,建議使用Stream並行!反之,采用串行操作更可靠!
在實際的開發過程中,還有一個使用最頻繁的操作就是,將集合元素中某個主鍵字段作為key,元素作為value,來實現集合轉map的需求,這種需求在數據組裝方面使用的非常多。
public static void main(String[] args) { List<Person> personList = new ArrayList<>(); personList.add(new Person("Tom",7000,25,"male","安徽")); personList.add(new Person("Jack",8000,30,"female","北京")); personList.add(new Person("Lucy",9000,40,"male","上海")); personList.add(new Person("Airs",10000,40,"female","深圳")); Map<Integer, Person> collect = personList.stream().collect(Collectors.toMap(Person::getAge, v -> v, (k1, k2) -> k1)); System.out.println(collect); }
{ 40=Person{name='Lucy', salary=9000, age=40, sex='male', area='上海'},
25=Person{name='Tom', salary=7000, age=25, sex='male', area='安徽'},
30=Person{name='Jack', salary=8000, age=30, sex='female', area='北京'} }
打開Collectors.toMap方法源碼,一起來看看。
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) { return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new); }
從參數表可以看出:
- 第一個參數:表示 key
- 第二個參數:表示 value
- 第三個參數:表示某種規則
上文中的Collectors.toMap(Person::getAge, v -> v, (k1,k2) -> k1),表達的意思就是將age的內容作為key,v -> v是表示將元素person作為value,其中(k1,k2) -> k1表示如果存在相同的key,將第一個匹配的元素作為內容,第二個舍棄!
15、特別注意點:
使用並行流 parallelStream,千萬不能使用 toMap 方法,toMap 使用的是 HashMap,得用 toConcurrentMap:
//錯誤示例 Map<String, User> userMap = userList.parallelStream()
.collect(Collectors.toMap(User::getClassName, Function.identity())); //正確示例 Map<String, User> userMap = userList.parallelStream()
.collect(Collectors.toConcurrentMap(User::getClassName, Function.identity()));
也不能使用ArrayList,得用 Vector:
List<String> usernames = new ArrayList<>(); //返回結果順序不變
userList.forEach(user -> { usernames.add(user.getUsername()); }); //或者,返回結果順序不變,雖然用的並行流
List<String> collect1 = userList.parallelStream().map(User::getUsername).collect(Collectors.toList()); //返回結果順序改變
userList.parallelStream().forEach(user -> { usernames.add(user.getUsername()); });