java1.8新特性之stream
什麼是Stream?
Stream字面意思是流,在java中是指一個來自數據源的元素隊列並支援聚合操作,存在於java.util
包中,又或者說是能應用在一組元素上一次執行的操作序列。(stream是一個由特定類型對象組成的一個支援聚合操作的隊列。)注意Java中的Stream並不會存儲元素,而是按需計算。關於這個概念需要以下幾點解釋:1、數據源流的來源。 它可以是列表,集合,數組(java.util.Collection
的子類),I/O channel
, 產生器generator
等(注意Map是不支援的);2、聚合操作。類似於SQL語句一樣的操作, 如filter, map, reduce, find, match, sorted等。因此stream流和以前的Collection操作是完全不同, Stream操作還有兩個非常基礎的特徵:Pipelining
和內部迭代。
Pipelining
也就是中間操作,它都會返迴流對象本身。 這樣多個操作的設計可以串聯起不同的運算操作,進而形成一個管道, 如同流式風格(fluent style)。 這樣做還可以對操作進行優化, 比如延遲執行(laziness)和短路( short-circuiting)等。內部迭代, 以前對集合遍歷都是通過Iterator
或者For-Each
的方式來顯式的在集合外部進行迭代, 這種方式叫做外部迭代。而我們的Stream則提供了內部迭代方式, 是通過訪問者模式(Visitor)來實現的。
也就是說Stream操作分為中間操作和最終操作兩種。其中最終操作用於返回特定類型的計算結果,而中間操作則返回Stream對象本身,這樣就可以將多個操作依次串起來且使得操作優化成為可能。
生成流
在Java1.8 中, 集合介面提供了兩個方法來生成流:stream()串列流
和parallelStream()並行流
,即Stream的操作可以分為串列stream()
和並行parallelStream()
。舉個例子來說:
List<String> strings = Arrays.asList("who","what","when","why","which");
List<String> filterd = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
流的各種運算操作
接下來介紹流的各種操作運算,使得你在適當的時候可以選擇相應的流運算。
1、forEach 循環
Stream提供了新的方法forEach
來迭代流中的每個數據。舉個例子來說:
List<String> stringList = Arrays.asList("who","what","when","why","which");
// 方式一:JDK1.8之前的循環方式
for(String string:stringList){
System.out.println(string);
}
// 方式二:使用Stream的forEach方法
stringList.stream().forEach(e -> System.out.println(e));
// 方式三:方式二的簡化形式,因為方法引用也屬於函數式介面,因此Lambda表達式可以用方法引用來代替
stringList.stream().forEach(System.out::println);
2、filter 過濾
filter方法用於通過設置條件來過濾出滿足條件的元素。舉個例子來說,下面就是用於輸出字元串列表中的空字元串的個數:
List<String> stringList = Arrays.asList("","welcome","","to","visit","my","","website");
long count = stringList.stream().filter(e -> e.isEmpty()).count();
System.out.println(count);
3、map 映射
請注意這裡的map不是指地圖map,而是一種函數,用於映射每個元素執行某些操作得到對應的結果。舉個例子來說,下面就是使用map來輸出元素對應的平方數:
List<Integer> integerList = Arrays.asList(2,3,4,5,6);
List<Integer> integers = integerList.stream().map(i->i*i).collect(Collectors.toList());
integerList.stream().forEach(System.out::println);
上面介紹的只是map的最基本用法。map對於Stream中包含的元素使用給定的轉換函數進行轉換操作,新生成的Stream只包含轉換生成的元素。這個方法有三個對於原始類型的變種方法,分別是:mapToInt
,mapToLong
和mapToDouble
。顧名思義像mapToInt就是將原始Stream轉換成一個新的Stream,不過新生成的Stream中的元素都是int類型。三個變種方法可以免除自動裝箱/拆箱的額外消耗。map方法示意圖:
4、flatMap 映射
flatMap映射和map映射類似,不過它的每個元素轉換得到的是Stream對象,會把子Stream中的元素壓縮到父集合中,說白了就是將幾個小的list合併成一個大的list。flatMap方法示意圖:
合併的過程可以參看下面這張圖片:
舉個例子來說,下面是jdk1.8之前的合併方式,需要先構造一個複合類型List,然後通過兩次遍歷循環來實現將複合類型List轉為單一類型List,這個過程其實挺複雜的:
List<String> fruitList = Arrays.asList("banana","orange","watermelon");
List<String> vegetableList = Arrays.asList("kale","leek","carrot");
List<String> transportList = Arrays.asList("car","bike","train");
//將多個元素合成一個複合類型集合,元素類型List<String>
List<List<String>> lists = new ArrayList<>();
lists.add(fruitList);
lists.add(vegetableList);
lists.add(transportList);
//將多個元素合成一個單一類型集合,元素類型String
List<String> newList = new ArrayList<>();
for(List<String> list:lists){
for(String item:list){
newList.add(item);
}
}
那麼使用jdk1.8提供的stream流,同時輔助of、collect和flatMap就可以直接進行轉換:
List<String> fruitList = Arrays.asList("banana","orange","watermelon");
List<String> vegetableList = Arrays.asList("kale","leek","carrot");
List<String> transportList = Arrays.asList("car","bike","train");
//將多個元素合成一個複合類型集合,元素類型List<String>
List<List<String>> lists = Stream.of(fruitList,vegetableList,transportList).collect(Collectors.toList());
//將多個元素合成一個單一類型集合,元素類型String
List<String> flatMap = Stream.of(fruitList,vegetableList,transportList)
.flatMap(list ->list.stream())
.collect(Collectors.toList());
System.out.println(flatMap);
5、sorted 排序
sorted方法用於對流進行排序。舉個例子來說,下面的程式碼就是用於對字元串按照給定的規則進行排序並輸出:
List<String> stringList = Arrays.asList("c","a","f","d","b","e");
stringList.stream().sorted((s1,s2) -> s1.compareTo(s2)).forEach(System.out::println);
再舉個例子,對10個隨機數進行排序並輸出:
Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);
6、distinct 去除重複
distinct方法用於去除流中重複的元素,缺點就是不能設置去重的條件。舉個例子來說:
List<String> stringList = Arrays.asList("do","what","you","want","to","do","and","do","it");
stringList.stream().distinct().forEach(System.out::println);
7、of 生成Stream對象
of方法用於生成Stream對象,注意它是Stream對象的方法。舉個例子來說:
Stream<Object> objectStream= Stream.of("do","what","you","want","to","do","and","do","it");
objectStream.forEach(System.out::println);
8、count 計算總數
count方法用於計算流中元素的總數。舉個例子來說:
Stream<Object> objectStream = Stream.of("do","what","you","want","to","do","and","do","it");
long count = objectStream.count();
System.out.println(count);
9、min和max 最小/最大
min/max方法用於返迴流中那個元素最小(最大)的,注意返回的是一個Optional對象。舉個例子來說:
List<String> integerList = Arrays.asList("1","2","3","4","5","6","7");
Optional<String> optionalInteger = integerList.stream().max((a,b) -> a.compareTo(b));
String result = optionalInteger.get();
System.out.println(result); //結果為7
10、collect
collect
方法的使用較為複雜,這裡僅僅介紹一些常用的方法即可。collect方法可以將Stream轉為Collection對象或者是Object類型的數組等,舉個例子來說:
List<String> stringList= Arrays.asList("do","what","you","want","to","do","and","do","it");
//Stream轉Collection
stringList.stream().collect(Collectors.toList());
//Stream轉Object[]數組
Object[] objects = stringList.stream().toArray();
11、concat
concat
方法用於合併流對象,注意這時Stream對象的方法。舉個例子來說:
List<String> fruitList = Arrays.asList("banana","orange","watermelon");
List<String> vegetableList = Arrays.asList("kale","leek","carrot");
Stream<String> stringStream = Stream.concat(fruitList.stream(),vegetableList.stream());
stringStream.forEach(System.out::println);
12、skip和limit
通常大家都會將skip和limit放在一塊進行學習和對比,那是因為兩者具有類似的作用,都是對流進行裁剪的中間方法。
skip方法。先來看skip方法,顧名思義skip(n)
用於跳過前面n個元素,然後再返回新的流,如圖所示:
為了驗證上面圖片的作用,這裡舉一個例子來進行說明:
public static void skipTest(long n){
Stream<Integer> integerStream = Stream.of(1,2,3,4,5,6);
integerStream.skip(n).forEach(System.out::println);
}
方法skip()
中的參數n不同將會導致不同的結果,具體情況如下:
(1)、當n<0時,運行結果會拋出IllegalArgumentException
異常;(2)、當n=0時,相當沒有跳過任何元素,原封不動地截取流中的元素(這種通常沒有意義,基本不會這樣操作);(3)、當0<n<length時,表示跳過n個元素後(不包括元素n),結果返回含有剩下的元素的流(使用頻率較多);(4)、當n>=length時,表示跳過所有元素,結果返回空流,你可以使用count方法來判斷此時流中元素的總數必定為0。
limit方法。說完了skip()
方法,接下來聊聊limit()
方法。顧名思義這個就是限制流中的元素,即用於將前n個元素返回新的流,如圖所示:
同樣也通過舉一個例子來進行說明:
public static void limitTest(long n){
Stream<Integer> integerStream = Stream.of(1,2,3,4,5,6);
integerStream.limit(n).forEach(System.out::println);
}
方法limit()
中的參數n不同將會導致不同的結果,具體情況如下:
(1)、當n<0時,運行結果會拋出IllegalArgumentException
異常;(2)、當n=0時,相當不取元素,結果返回空流;(3)、當0<n<length時,表示取前n個元素,結果返回新的流(使用頻率較多);(4)、當n>=length時,表示取所有元素,結果返迴流本身,你可以使用count方法來判斷此時流中元素的總數必定為length。
區別:注意這裡談skip
和limit
方法的區別是局限於有限流。skip
和limit
方法都是對流進行截取操作,區別在於skip
方法必須時刻監測流中元素的狀態,才能判斷是否需要丟棄,因此skip
屬於狀態操作。而limit
只關心截取的是否是其length,是就立馬中斷操作返迴流,因此limit
屬於中斷操作。
13、並行(parallel)執行
parallelStream
是流並行處理程式的代替方法。舉個例子來說,下面是使用 parallelStream
並行流來輸出空字元串的數量:
List<String> stringList= Arrays.asList("a","","b","","e","","c","","f");
//獲取空字元串的數量
long count = stringList.parallelStream().filter(string -> string.isEmpty()).count();
System.out.println(count); // 4
14、anyMatch、allMatch和noneMatch
anyMatch
方法用於判斷流中是否存在滿足特定條件的元素,返回類型是boolean類型。(只要有一個條件滿足即返回true)
List<String> stringList = Arrays.asList("hello","the","fruit","name","is","banana");
Boolean result = stringList.parallelStream().anyMatch(item -> item.equals("name"));
System.out.println(result); // true
allMatch
方法用於判斷流中是否存在滿足特定條件的元素,返回類型是boolean類型。(必須全部滿足才會返回true)
List<String> stringList = Arrays.asList("hello","the","fruit","name","is","banana");
Boolean result = stringList.parallelStream().allMatch(item -> item.equals("name"));
System.out.println(result); // false
noneMatch
方法用於判斷流中是否存在滿足特定條件的元素,返回類型是boolean類型。(全都不滿足才會返回true)
List<String> stringList = Arrays.asList("hello","the","fruit","name","is","banana");
Boolean result = stringList.parallelStream().noneMatch(item -> item.equals("name"));
System.out.println(result); // false
上面這個例子就是因為有一個滿足條件就返回了false。
15、reduce
reduce的意思是減少,而Stream中reduce方法就是用於實現這個目的,它根據一定的規則將Stream中的元素進行計算後返回一個唯一的值。舉個例子來說:
Stream<String> stringStream = Stream.of("my","name","is","envy");
Optional<String> stringOptional = stringStream.reduce((before, after) -> before+"、"+after);
stringOptional.ifPresent(System.out::println); // my、name、is、envy
16、findFirst和findAny
findFirst
方法用於返回list列表中第一個元素,注意如果元素不存在則拋異常。舉個例子來說:
List<String> stringList = Arrays.asList("do","what","you","want","to","do","and","do","it");
Optional<String> result = stringList.parallelStream().findFirst();
System.out.println(result.get()); // do
注意若Optional
為空,則get方法會拋出異常,但是你可以使用orElse(defaultVal);
或使用orElseGet(() -> {// doSomething; return defaultVal;});
來返回默認值。舉個例子來說:
List<String> stringList = Arrays.asList();
Optional<String> result = stringList.parallelStream().findFirst();
System.out.println(result.orElse("沒有元素")); // 沒有元素
List<String> stringList = Arrays.asList();
Optional<String> result = stringList.parallelStream().findFirst();
System.out.println(result.orElseGet(() ->{return "沒有元素";})); // 沒有元素
17、summaryStatistics統計
summaryStatistics
方法用於產生統計結果的收集器,舉個例子來說:
List<Integer> integerList = Arrays.asList(3,2,3,5,6,8,9);
IntSummaryStatistics result = integerList.stream().mapToInt((x)->x).summaryStatistics();
System.out.println("列表中最大的數:"+result.getMax());
System.out.println("列表中最小的數:"+result.getMin());
System.out.println("列表中所有數之和:"+result.getSum());
System.out.println("列表中所有數的平均數:"+result.getAverage());
System.out.println("列表中元素的個數:"+result.getCount());
18、Joining集合元素的拼接
集合元素的拼接,其實就是指定分隔符將列表中的元素合併成一個字元串,注意joining
方法是存在於Collectors
中的。舉個例子來說:
List<String> stringList = Arrays.asList("my","name","is");
System.out.println(stringList); // [my, name, is]
String result = stringList.stream().collect(Collectors.joining(","));
System.out.println(result); // my,name,is
String newString = Stream.of("I","come","from bei").collect(
Collectors.collectingAndThen(
Collectors.joining(","),x-> x+"jing"));
System.out.println(newString); // I,come,from beijing
19、Collectors之流轉換成集合
Collectors
類實現了很多歸約操作,例如將流轉換成集合和聚合元素等,Collectors
可用於返回列表或字元串。下面通過舉一些經常會使用到的例子來進行說明:
先在外部新建一個Student實體類,後續會使用到:
public class Student {
private String name;
private Long score;
//getter/setter/toString/AllArgsConstructor
}
然後看下面的例子程式碼:
List<Integer> integerList = Arrays.asList(1,2,3,4,5);
//流轉列表
List<Integer> newList = integerList.stream().map(i -> i*10).collect(Collectors.toList());
System.out.println("新列表:"+newList); //[10, 20, 30, 40, 50]
//流轉集合
Set<Integer> integerSet = integerList.stream().map(i -> i*10).collect(Collectors.toSet());
System.out.println("新集合:"+integerSet); //[50, 20, 40, 10, 30]
//流轉映射
Map<String,String> stringStringMap = integerList.stream().map(i ->i*10).collect(
Collectors.toMap(key -> "key"+key/10,value -> "value:"+value)
);
System.out.println("新映射:"+stringStringMap); //{key1=value:10, key2=value:20, key5=value:50, key3=value:30, key4=value:40}
//流轉有序集合TreeSet
TreeSet<Integer> integerTreeSet = Stream.of(1,6,3,7,2).collect(Collectors.toCollection(TreeSet::new));
System.out.println("新有序集合:"+integerTreeSet); //[1, 2, 3, 6, 7]
//自定義對象流
List<Student> studentList = Arrays.asList(
new Student("envy",100L),
new Student("movie",90L),
new Student("book",80L)
);
//獲得對象
Map<String,Student> studentAndModelMap = studentList.stream().collect(Collectors.toMap(
Student::getName, Function.identity()
));
Student student = studentAndModelMap.get("envy");
System.out.println(student); //Student{name='envy', score=100}
//獲得屬性
Map<String,Long> studentAndStudentScoreMap = studentList.stream().collect(Collectors.toMap(
Student::getName, Student::getScore
));
Long score = studentAndStudentScoreMap.get("envy");
System.out.println(score); //100
20、Collectors之元素聚合
其實這個元素聚合歸根結底還是Collectors
類中的方,下面就來介紹聚合元素這個操作,Collectors
可用於返回列表或字元串。下面通過舉一些經常會使用到的例子來進行說明:
//元素聚合
List<Integer> integerList = Arrays.asList(1,5,8,3,6,2,9,7,4);
//求最大值
Integer maxValue = integerList.stream().collect(
Collectors.collectingAndThen(
Collectors.maxBy((a,b) -> a-b), Optional::get
));
System.out.println(maxValue); // 9
//求最小值
Integer minValue = integerList.stream().collect(
Collectors.collectingAndThen(
Collectors.minBy((a,b) -> a-b), Optional::get
));
System.out.println(minValue); // 1
//求和
Integer sumValue = integerList.stream().collect(
Collectors.summingInt(item ->item)
);
System.out.println(sumValue); // 45
//平均值
Double avgValue = integerList.stream().collect(
Collectors.averagingDouble(x -> x)
);
System.out.println(avgValue); // 5.0
//集合轉映射
String listToMap = Stream.of("my","name","is","envy").collect(
Collectors.mapping(
x->x.toUpperCase(),Collectors.joining(",")
)
);
System.out.println(listToMap); // MY,NAME,IS,ENVY
21、累計操作
reducing
累計操作,也是Collectors
類中的方法,用於進行元素的累計操作。先來看一個例子,用於計算出[2,3,5,6]
這個列表中所有元素各加1之後的所有元素之和,很簡單口算都可以知道答案是20。你可能有很多種想法,這裡提供幾種以供你參考:
//方法一,不使用stream
int[] ints = {2,3,5,6};
int resultSum =0;
for(int i:ints){
i++;
resultSum+=i;
}
System.out.println(resultSum); //20
//方法二,使用stream流的map+summingInt方法
List<Integer> integerList = Arrays.asList(2,3,5,6);
Integer integerOne = integerList.stream().map(i ->i+1).collect(
Collectors.summingInt(x ->x)
);
System.out.println(integerOne); //20
//方法三,使用stream流的reducing方法
Integer integerTwo = integerList.stream().collect(
Collectors.reducing(0,x->x+1,(sum,b) -> {
return sum+b;
})
);
System.out.println(integerTwo); //20
// reducing還可以用於更複雜的累計計算,不局限於加減乘除等操作
Integer integerThree = integerList.stream().collect(
Collectors.reducing(1,x->x+1,(result,b) -> {
return result*b;
})
);
System.out.println(integerThree); // 3*4*6*7=504
22、groupingBy 分組
groupingBy
分組這個功能在實際開發中用的非常多,因此有必須要好好用一下,它也是存在於Collectors
類中的。來看一下這個Collectors.groupingBy
方法的源碼,它有三個重載方法,這裡就以只有一個參數的方法為例進行說明:
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
可以發現它的參數只有一個:Function<? super T, ? extends K> classifier
,類型是Function類型也就是個函數,Function的返回值可以是要分組的條件,或者是要分組的欄位。groupingBy
方法的返回的結果是一個Map,其中key的數據類型為Function體中的計算類型(也就是參數類型),value是List
接下來通過一個例子來介紹如何使用它,這個例子也非常簡單,給定[1,2,3,4,5,6,7,8,9]
這個列表,如何將其按照奇數和偶數來劃分為兩組。用以往的知識你可能會這樣操作:
List<Integer> oneList = new ArrayList<>(); //奇數列表
List<Integer> twoList = new ArrayList<>(); //偶數列表
for(Integer item:integerList){
if(item%2==0){
twoList.add(item);
}else {
oneList.add(item);
}
}
System.out.println(oneList); // [1, 3, 5, 7, 9]
System.out.println(twoList); // [2, 4, 6, 8]
但是如果你使用了stream那就變得簡單多了:
//方法二,使用stream
Map<Boolean, List<Integer>> resultMap = integerList.stream().collect(
Collectors.groupingBy(item -> item%2 ==0)
);
System.out.println(resultMap); // {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8]}
Map<Boolean, List<Integer>> twoPartition = integerList.stream().collect(
Collectors.partitioningBy(item -> item%2 ==0)
);
System.out.println(twoPartition); //twoPartition就是將結果為為兩組
//還可以自定義分組的條件
List<Student> studentList = Arrays.asList(
new Student("book",100L,1),
new Student("movie",90L,2),
new Student("fruit",80L,2),
new Student("vegetable",70L,4)
);
//根據某個欄位進行分組
Map<Integer,List<Student>> studentMap = studentList.stream().collect(
Collectors.groupingBy(item ->item.getId())
);
System.out.println(studentMap);
//{1=[Student{name='book', score=100}], 2=[Student{name='movie', score=90}, Student{name='fruit', score=80}], 4=[Student{name='vegetable', score=70}]}
//還可以結合前面的統計結果處理器來對結果進行分析
Map<Integer, LongSummaryStatistics> summaryStatisticsMap = studentList.stream().collect(
Collectors.groupingBy(
Student::getId, Collectors.summarizingLong(Student::getScore)
)
);
LongSummaryStatistics statisticsOne = summaryStatisticsMap.get(1);
LongSummaryStatistics statisticsTwo = summaryStatisticsMap.get(2);
System.out.println(statisticsOne.getMax()); //100
System.out.println(statisticsOne.getMin()); //100
System.out.println(statisticsOne.getAverage()); //100.0
System.out.println(statisticsOne.getCount()); //1
System.out.println(statisticsOne.getSum()); //100.0
System.out.println("*********");
System.out.println(statisticsTwo.getMax()); //90
System.out.println(statisticsTwo.getMin()); //80
System.out.println(statisticsTwo.getAverage()); //85.0
System.out.println(statisticsTwo.getCount()); //2
System.out.println(statisticsTwo.getSum()); //170
}
上面基本上把日常開發過程中可能會遇到的場景都進行了介紹,但是我覺得這是做了第一步如何使用它,後續會出一些文章來好好研究裡面的源碼,同時會對上面的一些方法進行更深層次的研究和使用。
獲取更多內容可以關注微信公眾號:餘思部落格。