概念
Stream
將要處理的元素集合看作一種流,在流的過程中,藉助Stream API
對流中的元素進行操作,比如:篩選、排序、聚合等。
Stream
的操作符大體上分為兩種:中間操作符
和終止操作符
中間操作符
對於資料流來說,中間操作符在執行制定處理程式後,資料流依然可以傳遞給下一級的操作符。
中間操作符包含8種(排除了parallel,sequential,這兩個操作並不涉及到對資料流的加工操作):
- map(mapToInt,mapToLong,mapToDouble) 轉換操作符,把比如A->B,這裡預設提供了轉int,long,double的操作符。
- flatmap(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作比如把 int[]{2,3,4} 拍平 變成 2,3,4 也就是從原來的一個資料變成了3個資料,這裡預設提供了拍平成int,long,double的操作符。
- limit 限流操作,比如資料流中有10個 我只要出前3個就可以使用。
- distint 去重操作,對重複元素去重,底層使用了equals方法。
- filter 過濾操作,把不想要的資料過濾。
- peek 挑出操作,如果想對資料進行某些操作,如:讀取、編輯修改等。
- skip 跳過操作,跳過某些元素。
- sorted(unordered) 排序操作,對元素排序,前提是實現Comparable介面,當然也可以自定義比較器。
終止操作符
資料經過中間加工操作,就輪到終止操作符上場了;
終止操作符就是用來對資料進行收集或者消費的,資料到了終止操作這裡就不會向下流動了,終止操作符只能使用一次。
- collect 收集操作,將所有資料收集起來,這個操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以說Stream 的核心在於Collectors。
- count 統計操作,統計最終的資料個數。
- findFirst、findAny 查詢操作,查詢第一個、查詢任何一個 返回的型別為Optional。
- noneMatch、allMatch、anyMatch 匹配操作,資料流中是否存在符合條件的元素 返回值為bool 值。
- min、max 最值操作,需要自定義比較器,返回資料流中最大最小的值。
- reduce 規約操作,將整個資料流的值規約為一個值,count、min、max底層就是使用reduce。
- forEach、forEachOrdered 遍歷操作,這裡就是對最終的資料進行消費了。
- toArray 陣列操作,將資料流的元素轉換成陣列。
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);
輸出結果:
3
6
9
0.8106623442686114
0.11554643727388458
0.1404645961428974
Process finished with exit code 0
stream
和parallelStream
的簡單區分:
stream
是順序流,由主執行緒按順序對流執行操作;
parallelStream
是並行流,內部以多執行緒並行執行的方式對流進行操作,但前提是流中的資料處理沒有順序要求。
例如篩選集合中的奇數,兩者的處理不同之處:
Stream使用
遍歷/匹配(foreach/find/match)
Stream
也是支援類似集合的遍歷和匹配元素的,只是Stream
中的元素是以Optional
型別存在的。Stream
的遍歷、匹配非常簡單。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 9, 3, 8, 2, 1);
// 遍歷輸出符合條件的元素
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);
System.out.println("匹配第一個值:" + findFirst.get());
System.out.println("匹配任意一個值:" + findAny.get());
System.out.println("是否存在大於6的值:" + anyMatch);
}
}
輸出結果:
7
9
8
匹配第一個值:7
匹配任意一個值:8
是否存在大於6的值:true
Process finished with exit code 0
篩選(filter)
篩選,是按照一定的規則校驗流中的元素,將符合條件的元素提取到新的流中的操作。
篩選出Integer
集合中大於7的元素,並列印出來
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(6, 7, 3, 8, 1, 2, 9);
Stream<Integer> stream = list.stream();
stream.filter(x -> x > 7).forEach(System.out::println);
}
}
輸出結果:
8
9
Process finished with exit code 0
聚合(max/min/count)
max
、min
、count
這些字眼你一定不陌生,沒錯,在mysql中我們常用它們進行資料統計。Java stream中也引入了這些概念和用法,極大地方便了我們對集合、陣列的資料統計工作。
案例一:獲取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());
}
}
輸出結果:
最長的字串:weoujgsd
Process finished with exit code 0
案例二:獲取Integer
集合中的最大值。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 9, 4, 11, 6);
// 自然排序
Optional<Integer> max = list.stream().max(Integer::compareTo);
// 自定義排序
Optional<Integer> max2 = list.stream().max(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
System.out.println("自然排序的最大值:" + max.get());
System.out.println("自定義排序的最大值:" + max2.get());
}
}
輸出結果:
自然排序的最大值:11
自定義排序的最大值:11
Process finished with exit code 0
案例三:計算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);
}
}
輸出結果:
list中大於6的元素個數:4
Process finished with exit code 0
對映(map/flatMap)
對映,可以將一個流的元素按照一定的對映規則對映到另一個流中。分為map
和flatMap
:
map
:接收一個函式作為引數,該函式會被應用到每個元素上,並將其對映成一個新的元素。flatMap
:接收一個函式作為引數,將流中的每個值都換成另一個流,然後把所有流連線成一個流。
案例一:英文字串陣列的元素全部改為大寫。整數陣列每個元素+3。
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);
}
}
輸出結果:
每個元素大寫:[ABCD, BCDD, DEFDE, FTR]
每個元素+3:[4, 6, 8, 10, 12, 14]
Process finished with exit code 0
案例二:將兩個字元陣列合併成一個新的字元陣列。
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);
}
}
輸出結果:
處理前的集合:[m,k,l,a, 1,3,5,7]
處理後的集合:[m, k, l, a, 1, 3, 5, 7]
Process finished with exit code 0
歸約(reduce)
歸約,也稱縮減,顧名思義,是把一個流縮減成一個值,能實現對集合求和、求乘積和求最值操作。
案例一:求Integer
集合的元素之和、乘積和最大值。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4);
// 求和方式1
Optional<Integer> sum = list.stream().reduce(Integer::sum);
// 求和方式2
Optional<Integer> sum2 = list.stream().reduce(Integer::sum);
// 求和方式3
Integer sum3 = list.stream().reduce(0, Integer::sum);
// 求乘積
Optional<Integer> product = list.stream().reduce((x, y) -> x * y);
// 求最大值方式1
Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y);
// 求最大值寫法2
Integer max2 = list.stream().reduce(1, Integer::max);
System.out.println("list求和:" + sum.get() + "," + sum2.get() + "," + sum3);
System.out.println("list求積:" + product.get());
System.out.println("list求和:" + max.get() + "," + max2);
}
}
輸出結果:
list求和:29,29,29
list求積:2112
list求和:11,11
Process finished with exit code 0
歸集(toList/toSet/toMap)
因為流不儲存資料,那麼在流中的資料完成處理後,需要將流中的資料重新歸集到新的集合裡。toList
、toSet
和toMap
比較常用,另外還有toCollection
、toConcurrentMap
等複雜一些的用法。
下面用一個案例演示toList
、toSet
和toMap
:
public class Person {
private String name; // 姓名
private int salary; // 薪資
private int age; // 年齡
private String sex; //性別
private String area; // 地區
// 構造方法
public Person(String name, int salary, int age,String sex,String area) {
this.name = name;
this.salary = salary;
this.age = age;
this.sex = sex;
this.area = area;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getArea() {
return area;
}
public void setArea(String area) {
this.area = area;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", salary=" + salary +
", age=" + age +
", sex='" + sex + '\'' +
", area='" + area + '\'' +
'}';
}
}
public class StreamTest {
public static void main(String[] args) {
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"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
personList.add(new Person("Anni", 8200, 24, "female", "New York"));
Map<?, Person> map = personList.stream().filter(p -> p.getSalary() > 8000)
.collect(Collectors.toMap(Person::getName, p -> p));
System.out.println("toList:" + listNew);
System.out.println("toSet:" + set);
System.out.println("toMap:" + map);
}
}
輸出結果:
toList:[6, 4, 6, 6, 20]
toSet:[4, 20, 6]
toMap:{Tom=Person{name='Tom', salary=8900, age=23, sex='male', area='New York'}, Anni=Person{name='Anni', salary=8200, age=24, sex='female', area='New York'}}
Process finished with exit code 0
統計(count/averaging)
Collectors
提供了一系列用於資料統計的靜態方法:
- 計數:
count
- 平均值:
averagingInt
、averagingLong
、averagingDouble
- 最值:
maxBy
、minBy
- 求和:
summingInt
、summingLong
、summingDouble
- 統計以上所有:
summarizingInt
、summarizingLong
、summarizingDouble
案例:統計員工人數、平均工資、工資總額、最高工資。
public class StreamTest {
public static void main(String[] args) {
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"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
// 求總數
long count = personList.size();
// 求平均工資
Double average = personList.stream().collect(Collectors.averagingDouble(Person::getSalary));
// 求最高工資
Optional<Integer> max = personList.stream().map(Person::getSalary).max(Integer::compare);
// 求工資之和
int sum = personList.stream().mapToInt(Person::getSalary).sum();
// 一次性統計所有資訊
DoubleSummaryStatistics collect = personList.stream().collect(Collectors.summarizingDouble(Person::getSalary));
System.out.println("員工總數:" + count);
System.out.println("員工平均工資:" + average);
System.out.println("員工最高工資:" + max.get());
System.out.println("員工工資總和:" + sum);
System.out.println("員工工資所有統計:" + collect);
}
}
輸出結果:
員工總數:3
員工平均工資:7900.0
員工最高工資:8900
員工工資總和:23700
員工工資所有統計:DoubleSummaryStatistics{count=3, sum=23700.000000, min=7000.000000, average=7900.000000, max=8900.000000}
Process finished with exit code 0
分組(partitioningBy/groupingBy)
- 分割槽:將
stream
按條件分為兩個Map
,比如員工按薪資是否高於8000分為兩部分。 - 分組:將集合分為多個Map,比如員工按性別分組。有單級分組和多級分組。
案例:將員工按薪資是否高於8000分為兩部分;將員工按性別和地區分組
public class StreamTest {
public static void main(String[] args) {
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)));
System.out.println("員工按薪資是否大於8000分組情況:" + part);
System.out.println("員工按性別分組情況:" + group);
System.out.println("員工按性別、地區:" + group2);
}
}
輸出結果:
員工按薪資是否大於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'}]}}
Process finished with exit code 0
接合(joining)
joining
可以將stream中的元素用特定的連線符(沒有的話,則直接連線)連線成一個字串。
public class StreamTest {
public static void main(String[] args) {
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"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
String names = personList.stream().map(Person::getName).collect(Collectors.joining(","));
System.out.println("所有員工的姓名:" + names);
List<String> list = Arrays.asList("A", "B", "C");
String string = list.stream().collect(Collectors.joining("-"));
System.out.println("拼接後的字串:" + string);
}
}
輸出結果:
所有員工的姓名:Tom,Jack,Lily
拼接後的字串:A-B-C
Process finished with exit code 0
排序(sorted)
sorted
,中間操作。有兩種排序:
sorted()
:自然排序,流中元素需實現Comparable
介面sorted(Comparator com)
:Comparator
排序器自定義排序
案例:將員工按工資由高到低(工資一樣則按年齡由大到小)排序
public class StreamTest {
public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Sherry", 9000, 24, "female", "New York"));
personList.add(new Person("Tom", 8900, 22, "male", "Washington"));
personList.add(new Person("Jack", 9000, 25, "male", "Washington"));
personList.add(new Person("Lily", 8800, 26, "male", "New York"));
personList.add(new Person("Alisa", 9000, 26, "female", "New York"));
// 按工資升序排序(自然排序)
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());
System.out.println("按工資升序排序:" + newList);
System.out.println("按工資降序排序:" + newList2);
System.out.println("先按工資再按年齡升序排序:" + newList3);
System.out.println("先按工資再按年齡自定義降序排序:" + newList4);
}
}
輸出結果:
按工資升序排序:[Lily, Tom, Sherry, Jack, Alisa]
按工資降序排序:[Sherry, Jack, Alisa, Tom, Lily]
先按工資再按年齡升序排序:[Lily, Tom, Sherry, Jack, Alisa]
先按工資再按年齡自定義降序排序:[Alisa, Jack, Sherry, Tom, Lily]
Process finished with exit code 0
提取/組合
流也可以進行合併、去重、限制、跳過等操作。
public class StreamTest {
public static void main(String[] args) {
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());
System.out.println("流合併:" + newList);
System.out.println("limit:" + collect);
System.out.println("skip:" + collect2);
}
}
輸出結果:
流合併:[a, b, c, d, e, f, g]
limit:[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
skip:[3, 5, 7, 9, 11]
Process finished with exit code 0
分頁操作
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());
輸出結果:
[2, 2, 3, 3, 3, 5, 6, 7, 10, 20]
Process finished with exit code 0
並行操作
所謂並行,指的是多個任務在同一時間點發生,並由不同的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並行!反之,採用序列操作更可靠!
集合轉Map操作
在實際的開發過程中,還有一個使用最頻繁的操作就是,將集合元素中某個主鍵欄位作為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='北京'}}
Process finished with exit code 0
開啟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
,將第一個匹配的元素作為內容,第二個捨棄!
結尾
本文主要,圍繞 jdk stream api 操作,結合實際的日常開發需求,做了簡單總結和分享。希望你也能跟著一起敲一遍加深印象,相信都能掌握這些操作符的初步用法;後續文章我會帶大家一步步深入Stream。看完了,希望你能點個贊,哈哈。