這篇文章我們一起來學習java8最重要的新特性之一,強大的StreamAPI,在本文中的代碼示例中我們會大量使用lambda表達式,如果對lambda表達式還不太熟悉,可以看一下上一篇文章。
java8中Java8中有兩大最爲重要的改變。第一個是 Lambda 表達式;另外一個則是 Stream API(java.util.stream.*)。Stream 是 Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執行非常複雜的查找、過濾和映射數據等操作。
使用Stream API 對集合數據進行操作,就類似於使用 SQL 執行的數據庫查詢。也可以使用 Stream API 來並行執行操作。簡而言之,Stream API 提供了一種高效且易於使用的處理數據的方式。
一、首先我們來思考一個問題,什麼是Stream(流)?
流是一個數據渠道,用於操作數據源(集合,數組等)所生成的元素序列,總之,集合主要是數據,流主要是計算。
二、Stream操作的三個步驟:
- 創建Stream
通過一個數據源獲取一個流,例如,List中的stream()方法可以直接返回一個Stream對象。
- 中間操作
我們需要對流中的數據進行的操作,比如循環處理(map),過濾(filter)等
- 終止操作
流都是惰性求值的,這個我們在後面會講到,需要進行一個終止操作,這樣纔會返回中間操作後的數據。
如下圖:
注意:
Stream只是一個計算通道,自己不會存儲元素;
Stream不會改變源對象,相反,他們會返回一個新的Stream對象。
Stream操作是延時的,只有在執行終止操作時纔會執行。
三、創建Steam的方式:
1、由Collection子類(List,Set)來創建流:
java8擴展了Collection接口,提供了stream(返回順序流)和parallelStream(返回並行流)兩個方法。並行流我們後面在詳細討論,示例代碼:
@Test
public void test(){
List<String> list = Arrays.asList("a","b","c");
Stream stram = list.stream();
Stream parallelSteam = list.parallelStream();
}
2、由數據來創建流:
數組可以通過Arrays工具類的stream方法來獲得一個Steam對象,示例代碼:
public void test2(){
String[] strArr = new String[]{"a","b","c"};
Stream stram = Arrays.stream(strArr);
//還有許多重載形式的方法,可以返回帶類型的Stream,例如:
IntStream stram2 = Arrays.stream(new int []{1,2,3,4,5});
}
3、通過具體值來創建流:
通過Stream的靜態方法Stream.of(T...values)可以創建一個流,它可以接受任意個值,代碼示例:
@Test
public void test3() {
Stream s = Stream.of("1","2",3,new String[]{"a", "b", "c"});
}
4、通過函數來創建流(無限流)
通過Stream.iterate()和Stream.generate()方法可以創建無限流,什麼是無限流,我們來看一下代碼示例就明白了:
@Test
public void test4() {
//Stream.iterate方法第一個方法表示開始值得,第二個參數需要提供一個一元操作函數,我們用lambda表達式傳遞給它
Stream stream1 = Stream.iterate(0, (x) -> x + 2);
stream1.forEach(System.out::println); //輸出的是0,2,4,6,8,10....將會一直循環下去,永遠不停息
//Stream.generate需要一個供給型函數接口
Stream stream2 = Stream.generate(() -> 1);
stream2.forEach(System.out::println); //輸出無數個1,將會一直循環下去,永遠不停息
}
備註:實際運用中,我們肯定不會生成一個無限流,除非你想要死循環,我們會結合Stream的終止操作,如limit來獲取有指定個數元素的流:
@Test
public void test5(){
//我們從0開始獲取前50個偶數
Stream stream1 = Stream.iterate(0, (x) -> x + 2).limit(50);
stream1.forEach(System.out::println);//輸出0,2,4,6,8。。。。98
}
四、Stream的中間操作:
Stream可以進行一系列的流水線式的中間操作,除非流水線上觸發終止操作,否則,這些中間操作不會進行任何處理,而在終止操作時一次性處理,這個我們叫做Stream的惰性求值。
記住,中間操作不管做多少次,都不會改變原來的流,只會返回一個新的流;
Stream的中間操作可以分爲以下幾類:
中間操作:篩選與切片
方法 描述 filter(Predicate d) 接受一個斷言型函數,對Stream流中的元素進行處理,過濾掉不滿足條件的元素 distinct 篩選元素,通過Stream元素中的hasCode和equals方法來去除重複元素 limit(long maxSize) 截斷流,使元素不超過manSize指定的數量 skip(Long n) 跳過元素,返回一個扔掉了前n個元素的流,若流中的元素不足n個,則會返回一個空流
中間操作:映射
方法 描述 map(Function f) 接受一個函數型接口作爲參數,該函數會對流中的每個元素進行處理,返回處理後的流 mapToDouble(ToDoubleFunction f) 接口一個函數型接口作爲參數,該函數會對流中的每個元素進行處理,並返回一個Double值,最終得到一個Stream<Double> mapToInt(ToIntFunction f) 接口一個函數型接口作爲參數,該函數會對流中的每個元素進行處理,並返回一個Int值,最終得到一個Stream<Int> mapToLong(ToLongFunction f) 接口一個函數型接口作爲參數,該函數會對流中的每個元素進行處理,並返回一個Long值,最終得到一個Stream<Long> flatMap(Function f) 接受一個函數作爲參數,將流中的每個值都轉換成一個新流,最後再將這些流連接到一起
中間操作:排序
方法 描述 sorted 返回一個新流,流中的元素按照自然排序進行排序 sorted(Comparator comp) 返回一個新流,並且Comparator指定的排序方式進行排序
中間操作練習:
/**
* Stream的中間操作練習
*/
public class StreamTest2 {
List<Employee> emps = Arrays.asList(
new Employee("張三", 18, 6666.66),
new Employee("李四", 20, 7777.77),
new Employee("王五", 36, 8888.88),
new Employee("田七", 55, 11111.11),
new Employee("趙六", 55, 9999.99),
new Employee("趙六", 45, 12222.22)
);
/**
* 篩選與切片 filter, distinct limit skip
*/
@Test
public void test1() {
//1.過濾掉年齡小於25的員工
emps.stream().filter((e) -> e.getAge() > 25).forEach(System.out::println);
//2.過濾掉姓名重複的員工
emps.stream().distinct().forEach(System.out::println);
//3.獲取前三名員工
emps.stream().limit(3).forEach(System.out::println);
//4.獲取第三名以後的員工
emps.stream().skip(3).forEach(System.out::println);
//5.先獲取前3名員工,再獲取其中年齡大於25的員工。(中間操作可以任意次)
emps.stream().limit(3).filter(e -> e.getAge() > 25);
}
/**
* 映射操作 map, mapToDouble, mapToInt, mapToLong, flatMap
*/
@Test
public void test2() {
//1. 獲取所有員工的姓名
emps.stream().map(e -> e.getName()).forEach(System.out::println);
//2. 獲取所有員工的工資,這裏工資是Double類型,我們可以用mapToDouble方法
emps.stream().mapToDouble(e -> e.getSalary()).forEach(System.out::println);
//3. 獲取所有員工的年齡,用mapToInt方法
emps.stream().mapToInt(e -> e.getAge()).forEach(System.out::println);
//4. 將員工年齡轉換成Long型並輸出,PS:這裏就算不用longValue系統也會自動將getAge轉換成Long類型
emps.stream().mapToLong(e -> e.getAge().longValue()).forEach(System.out::println);
/**
* 5.將所有員工的 姓名,年齡,工資轉換成一個流並返回
* 首先我們用map方法來處理,這種方式返回的是六個Stream對象,數據結構可以類似於:
* [{"張三",18, 6666.66},{"李四",20, 7777.77}, {"王五", 36, 8888.88}, {"趙六", 24, 9999.99}, {"田七", 55, 11111.11}, {"趙六", 45, 12222.22}]
* 然後我們用flatMap方法來處理,返回的是一個Stream對象,將所有元素連接到了一起,數據結構類似於:
* ["張三",18, 6666.66,"李四",20, 7777.77, "王五", 36, 8888.88, "趙六", 24, 9999.99, "田七", 55, 11111.11, "趙六", 45, 12222.22]
*/
emps.stream().map(e -> {
return Stream.of(e.getName(), e.getAge(), e.getSalary());
}).forEach(System.out::println);
emps.stream().flatMap(e -> {
return Stream.of(e.getName(), e.getAge(), e.getSalary());
}).forEach(System.out::println);
}
/**
* 排序操作 sorted
*/
@Test
public void test3() {
//1.按照自然排序,注意,需要進行自然排序則對象必須實現Comparable接口
emps.stream().sorted().forEach(System.out::println);
//2.按照給定規則進行排序,(按照工資高低進行排序)
emps.stream().sorted((x,y) -> Double.compare(x.getSalary(),y.getSalary())).forEach(System.out::println);
}
}
五、Stream的終止操作:
Stream的終止操作用來獲取一系列流水線操作的最終結果,這個結果可以是任何值,例如boolean,List,Integer甚至可以是void,終止操作也分爲以下幾大類:
終止操作:查找與匹配
方法 描述 allMatch(Predicate p) 傳入一個斷言型函數,對流中所有的元素進行判斷,如果都滿足返回true,否則返回false。 anyMatch(Predicate p) 傳入一個斷言型函數,對流中所有的元素進行判斷,只要有一個滿足條件就返回true,都不滿足返回false。 noneMatch(Predicate p) 所有條件都不滿足,返回true,否則返回false。 findFirst() 返回流中的第一個元素。 findAny() 返回流中的任意一個元素。 count() 返回流中元素的個數。 max(Comparator c) 按照給定的排序規則進行排序後,返回流中最大值的元素 min(Comparator c) 按照給定的排序規則進行排序後,返回流中最小值的元素 forEach(Consumer c) 內部迭代。
終止操作:規約
方法 描述 reduce(T iden, BinaryOperator bo) 可以將流中的元素反覆結合起來,得到一個值,返回T reduce(BinaryOperator bo) 可以將流中的元素反覆結合起來,得到一個值,返回Optional。(Optional我們後面再聊)
終止操作:收集
方法 描述 collect(Collector c) 將流中的元素轉換成其他形式,接受一個Collector接口的實現,用於處理Stream流中的元素,將流轉換成其他形式的對象。
collector接口中方法的實現決定了如何對流執行收集操作(如收集到List,Set,Map)。java8中的Collectors類提供了很多靜態方法,可以非常方便的創建常用的收集器實例,具體方法與實例:
方法 | 返回類型 | 作用 |
---|---|---|
toList | List<T> | 將流中的元素收集到List中,例如:list.stream.collect(Collectors.toList()); |
toSet | Set<T> | 將流中的元素收集到set中:Set<Employee> emps= list.stream().collect(Collectors.toSet()) |
toCollection | Collection<T> | 將流中的元素收集到創建的集合:list.stream().collect(Collectors.toCollection(ArrayList::new)) |
counting | Integer | 計算流中元素的個數:long count = list.stream().collect(Collectors.counting()); |
summingInt | Long | 對流中的整數屬性進行求和:inttotal=list.stream().collect(Collectors.summingInt(Employee::getSalary)); |
averagingInt | Double | 計算流中元素的平均值:doubleavg= list.stream().collect(Collectors.averagingInt(Employee::getSalary)); |
summarizingInt | IntSummaryStatistics | 返回一個IntSummaryStatistics,可以通過這個對象獲取統計值,如平均值:IntSummaryStatistics iss=list.stream().collect(Collectors.summarizingInt(Employee::getSalary)) |
joining | String | 連接流中的每個字符串:String str= list.stream().map(Employee::getName).collect(Collectors.joining()); |
maxBy | Optional<T> | 根據比較器選擇最大值:Optional<Employee> min = emps.stream().collect(Collectors.minBy((x ,y) -> Double.compare(x.getSalary(), y.getSalary()))); |
minBy | Optional<T> | 根據比較器選擇最小值:Optional<Employee> max = emps.stream().collect(Collectors.maxBy((x ,y) -> Double.compare(x.getSalary(), y.getSalary()))); |
reducing | 規約產生的類型 | 從一個作爲累加器的初始值開始,利用BinaryOperator與流中元素逐個結合,從而歸約成單個值。Integer totalAge = emps.stream().collect(Collectors.reducing(0, Employee::getAge, Integer::sum)); |
colectingAndThen | 轉換函數返回的類型 | 包裹一個收集器,對其結果進行轉換:int inthow= emps.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size)); |
groupingBy | Map<K, List<T>> | 根據某屬性對結果進行分組,屬性爲K,結果爲V:Map<String, List<Employee>> kv= emps.stream().collect(Collectors.groupingBy(Employee::getName)); |
partitioningBy | Map<boolean, ListM<T>> | 根據true或false進行分區:Map<Boolean,List<Employee>> vd = emps.stream().collect(Collectors.partitioningBy(e -> e.getAge() > 30)); |
終止操作練習:
/**
* Stream的終止操作練習
*/
public class StreamTest3 {
List<Employee> emps = Arrays.asList(
new Employee("張三", 17, 6666.66),
new Employee("李四", 20, 7777.77),
new Employee("王五", 36, 8888.88),
new Employee("田七", 55, 11111.11),
new Employee("趙六", 55, 9999.99),
new Employee("趙六", 45, 12222.22)
);
/**
* 終止操作:查找與匹配 allMatch, anyMatch, noneMatch, findFirst, findAny, count, max, min ,forEach
*/
@Test
public void test1() {
//1.查看是否有員工年齡是否都大於18
boolean flag1 = emps.stream().allMatch(e -> e.getAge() > 18);
System.out.println(flag1); // false
//2.先去掉張三,然後再判斷所有員工年齡是否都大於18
boolean flag2 = emps.stream().filter(e -> !"張三".equals(e.getName())).allMatch(e -> e.getAge() > 18);
System.out.println(flag2); //true
//3.是否有員工年齡大於50
boolean flag3 = emps.stream().filter(e -> !"張三".equals(e.getName())).anyMatch(e -> e.getAge() > 50);
System.out.println(flag3); //true
//4.沒有員工的年齡大於50?
boolean flag4 = emps.stream().filter(e -> !"張三".equals(e.getName())).noneMatch(e -> e.getAge() > 50);
System.out.println(flag4); //false
//5.先按照年齡進行排序,然後返回第一個員工。optional是java8用來包裝可能出現空指針的對象的對象
Optional<Employee> op1 = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge())).findFirst();
System.out.println(op1.get()); //Employee{name='張三', age=17, salary=6666.66}
//6. 查找任意一名員工的姓名,當使用順序流時,返回的是第一個對象,當使用並行流時,會隨機返回一名員工的姓名
Optional<String> op2 = emps.parallelStream().map(e -> e.getName()).findAny();
System.out.println(op2.get()); //會隨機獲取一名員工
//7. 查詢員工人數
Long count = emps.stream().count();
System.out.println(count);
//8.查詢員工工資最大的員工信息。PS: 這個也可以通過先按照工資排序,然後取第一個元素來實現
Optional<Employee> maxSalary = emps.stream().max((x, y) -> Double.compare(x.getSalary(), y.getSalary()));
System.out.println(maxSalary.get());
//9.查詢員工最小年齡
Optional<Employee> minAge = emps.stream().max((x, y) -> -Integer.compare(x.getAge(), y.getAge()));
System.out.println(minAge.get());
//10.循環輸出所有員工的信息
emps.stream().forEach(System.out::println);
}
/**
* 終止操作:規約 reduce
* 規約操作兩個重載方法的區別是:
* 一個未指定起始值,有可能返回null,所以返回的是一個Optional對象
* 一個指定了起始值,不可能返回null,所以返回值可以確定是一個Employee
*/
@Test
public void test2() {
//1.將所有員工的名字加上 -> 下一個員工的名字: 如 張三 -> 李四
Optional<Employee> op1 = emps.stream().reduce((x,y) -> {x.setName(x.getName() + "->" + y.getName()); return x;});
System.out.println(op1.get().getName()); //張三->李四->王五->田七->趙六->趙六
//2.將所有員工的名字加上 -> 下一個員工的名字,並且開始以王八開始;
// PS:測試時,請將例子1註釋掉,不然會影響emps對象
Employee emp = emps.stream()
.reduce(new Employee("王八", 65, 8888.88)
, (x,y) -> {
x.setName(x.getName() + "->" + y.getName());
return x;
});
System.out.println(emp.getName()); //王八->張三->李四->王五->田七->趙六->趙六
}
/**
* 終止操作:收集
*/
@Test
public void test3(){
//1.按年齡排序後收集成一個list並返回
List<Employee> list = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge()))
.collect(Collectors.toList());
list.forEach(System.out::println);
//2.按工資高低排序後收集成一個Set返回
Set<Employee> set = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge()))
.collect(Collectors.toSet());
set.forEach(System.out::println);
//3.按工資排序後收集到指定的集合中,這裏指定LinkedList
LinkedList<Employee> linkedList = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge()))
.collect(Collectors.toCollection(LinkedList::new));
linkedList.forEach(System.out::println);
//4.計算流中元素的個數:
long count = emps.stream().collect(Collectors.counting());
System.out.println(count);
//5.對所有員工的年齡求和:
int inttotal = emps.stream().collect(Collectors.summingInt(Employee::getAge));
System.out.println(inttotal);
//6.計算所有員工工資的平均值:
Double doubleavg= emps.stream().collect(Collectors.averagingDouble(Employee::getSalary));
System.out.println(doubleavg);
//7.返回一個IntSummaryStatistics,可以通過這個對象獲取統計值,如平均值:
IntSummaryStatistics iss =emps.stream().collect(Collectors.summarizingInt(Employee::getAge));
System.out.println(iss.getAverage());
System.out.println(iss.getMax());
System.out.println(iss.getMin());
//8.連接所有員工的名字:
String str= emps.stream().map(Employee::getName).collect(Collectors.joining());
System.out.println(str);
//9.相當於先按照工資進行排序,再取出排在第一位的員工
Optional<Employee> min = emps.stream().collect(Collectors.minBy((x ,y) -> Double.compare(x.getSalary(), y.getSalary())));
System.out.println(min);
//10.相當於先按照工資進行排序,再取出排在最後一位的員工
Optional<Employee> max = emps.stream().collect(Collectors.maxBy((x ,y) -> Double.compare(x.getSalary(), y.getSalary())));
System.out.println(max);
//11.從一個作爲累加器的初始值開始,利用BinaryOperator與流中元素逐個結合,從而歸約成單個值。
Integer totalAge = emps.stream().collect(Collectors.reducing(0, Employee::getAge, Integer::sum));
System.out.println(totalAge);
//12.包裹一個收集器,對其結果進行轉換:
int inthow = emps.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
System.out.println(inthow);
//13.根據某屬性對結果進行分組,屬性爲K,結果爲V:
Map<String, List<Employee>> kv = emps.stream().collect(Collectors.groupingBy(Employee::getName));
System.out.println(emps);
//14.根據true或false進行分區,年齡大於30的分在true區,小於30的分在false區
Map<Boolean,List<Employee>> vd = emps.stream().collect(Collectors.partitioningBy(e -> e.getAge() > 30));
System.out.println(vd);
}
}
總結:
Stream的總結其實就是一句話,記住Stream操作的三個步驟,創建流 -> 一系列中間操作 -> 終止操作拿到返回結果。
PS:上面練習的代碼放在了我的git倉庫中,感興趣的可以down下來再練練:https://github.com/caishi13202/jdk8.git