【如何亮劍】用例子來學(xué)習(xí) Stream
本文轉(zhuǎn)載自微信公眾號「你呀不牛」,作者不牛。轉(zhuǎn)載本文請聯(lián)系你呀不牛公眾號。
1引言
先從一個例子開始,看看為什么在Java8中要引入流(Stream)?
比如實現(xiàn)這么一個需求:在學(xué)生集合中查找男生的數(shù)量。
傳統(tǒng)的寫法為:
- public long getCountsOfMaleStudent(List<Student> students) {
- long count = 0;
- for (Student student : students) {
- if (student.isMale()) {
- count++;
- }
- }
- return count;
- }
看似沒什么問題,因為我們寫過太多類似的**”樣板”代碼**,盡管智能的IDE通過code template功能讓這一枯燥過程變得簡化,但終究不能改變?nèi)哂啻a的本質(zhì)。
再看看使用流的寫法:
- public long getCountsOfMaleStudent(List<Student> students) {
- return students.stream().filter(Student::isMale).count();
- }
一行代碼就把問題解決了!
雖然讀者可能還不太熟悉流的語法特性,但這正是函數(shù)式編程思想的體現(xiàn):
- 回歸問題本質(zhì),按照心智模型思考問題。
- 延遲加載。
- 簡化代碼。
下面正式進(jìn)入流的介紹。
2創(chuàng)建流
創(chuàng)建流的方式可以有很多種,其中最常見的方式是通過Collection的Stream()方法或者Arrays的Stream()方法來生成流。比如:
- List numbers = Arrays.asList(1, 2, 3);
- Stream numberStream = numbers.stream();
- String[] words = new String[]{"one", "two"};
- Stream wordsStream = Arrays.stream(words);
當(dāng)然Stream接口本身也提供了許多和流相關(guān)的操作。
- // 創(chuàng)建流
- Stream<Integer> numbers = Stream.of(1, 2, 3);
- // 創(chuàng)建空流
- Stream<String> emptyStream = Stream.empty();
- // 創(chuàng)建一個元素為“hi”的無限流
- Stream<String> infiniteString = Stream.generate(() -> "hi");
- // 創(chuàng)建一個從0開始的遞增無限流
- Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));
其中Stream.generate()和Stream.iterate()產(chǎn)生的都是無限流,如果要把他們截取為有限流,可以使用limit()方法, 比如:
- Stream<Double> top10 = Stream.generate(Math::random).limit(10);
另外,可以通過skip()方法跳過元素,concat()方法連接兩個流。
- // 3, 4
- Stream<Integer> skipedStream = Stream.of(1, 2, 3, 4).skip(2);
- // hello,world
- Stream<String> concatedStream = Stream.concat(Stream.of("hello"), Stream.of(",world"));
3常用的流操作
filter
filter()方法的作用就是根據(jù)輸入的條件表達(dá)式過濾元素。
接口定義如下:
- Stream filter(Predicate predicate);
從中可以看出,輸入?yún)?shù)是一個Predicate,也即是一個條件表達(dá)式。
一個例子:
- Stream.of("a", "1b", "c", "0x").filter(value -> isDigit(value.charAt(0)));
過濾出第一個字符是數(shù)字的元素。
輸出結(jié)果為:
1b, 0x
map
map()的主要作用是通過映射函數(shù)轉(zhuǎn)換成新的數(shù)據(jù)。接口定義如下:
- <R> Stream<R> map(Function<? super T, ? extends R> mapper);
從中可以看出,輸入?yún)?shù)是一個Function。一個例子:
- Stream.of("a", "b", "c").map(String::toUpperCase);
把字符串轉(zhuǎn)換成大寫。輸出結(jié)果:
A, B, C
flatMap
flatMap()的作用類似于map(),但它通過Function返回的依然是一個Stream,也即是把多個Stream轉(zhuǎn)換成一個扁平的Stream。接口定義如下:
- <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
一個例子:
- Stream<List<Integer>> listStream = Stream.of(asList(1, 2), asList(3, 4));
- Stream<Integer> integerStream = listStream.flatMap(numbers -> numbers.stream());
它把兩個list組成的Stream轉(zhuǎn)成一個包含全部元素的Stream。輸出:
[1, 2, 3, 4]
4有狀態(tài)的轉(zhuǎn)換
在前面介紹的函數(shù)中,無論是map還是filter,都不會改變流的狀態(tài),也即結(jié)果并不依賴之前的元素。除此之外,Java8也提供了有狀態(tài)的轉(zhuǎn)換,常用的操作是distinct和sorted。
distinct
distinct()的主要作用是去除流中的重復(fù)元素。和Oracle的distinct一個作用。舉例如下:
- Stream distinctStream = Stream.of("one", "one", "two", "three").distinct();
去除字符串中的重復(fù)元素,返回結(jié)果為:
one, two, three
sorted
sorted()的主要作用是對流按照指定的條件進(jìn)行排序。接口定義如下:
- Stream<T> sorted(Comparator<? super T> comparator);
從中可以看出,入?yún)⑹且粋€Comparator,也即是一個函數(shù)式接口。一個例子:
- Stream<String> sortedStream = Stream.of("one", "two","three").sorted(Comparator.comparing(String::length).reversed());
對字符串按照長度進(jìn)行降序排列。
注意,這里使用了Comparator.comparing方法來簡化調(diào)用。
輸出結(jié)果為:
[three, one, two]
5Optional類型
在介紹下個主題前,先介紹一個Java8新增的數(shù)據(jù)結(jié)構(gòu):Optional。Optional的主要作用是對結(jié)果進(jìn)行了封裝,結(jié)果可能有值,也可能沒有值,并且對結(jié)果可以進(jìn)行后續(xù)處理,比如添加默認(rèn)值,映射其他值,拋出異常等。
下面是常用的操作舉例:
- // 生成了一個Optional數(shù)據(jù)
- Optional<String> maxStrOpt = Stream.of("one", "two", "three").max(String::compareToIgnoreCase);
- // 如果值存在的情況下,把數(shù)據(jù)添加到List中
- ArrayList<String> result = new ArrayList<String>();maxStrOpt.ifPresent(result::add);
- // 把結(jié)果映射為大寫,然后取出。
- Optional<String> upperResult = maxStrOpt.map(String::toUpperCase);System.out.println(upperResult.get());
- // 值為空的情況下的后續(xù)處理
- maxStrOpt.orElse("");
- // 添加默認(rèn)值""
- maxStrOpt.orElseGet(() -> System.getProperty("user.dir"));
- // 通過表達(dá)式返回結(jié)果
- maxStrOpt.orElseThrow(RuntimeException::new); // 拋出異常
6聚合操作
之前介紹的函數(shù)都是返回的Stream,根據(jù)Stream延遲加載的特性,它是不會真正執(zhí)行的,只有在做了本節(jié)的聚合操作以及后續(xù)章節(jié)介紹的收集操作后,才會真正執(zhí)行。
所謂聚合操作就是把一組數(shù)據(jù)通過操作聚合為一個結(jié)果的過程。
下面介紹常用的聚合操作:
count
count()的作用是統(tǒng)計元素的總數(shù),很多時候需要配合filter一起使用。一個例子:
- long count = Stream.of("one", "two", "three").filter(word->word.contains("o")).count();
統(tǒng)計字符流中包含字符o的單詞數(shù)量。結(jié)果:
2
max/min
max/min()的主要作用是取得元素的最大值/最小值。接口定義如下:
- Optional<T> max(Comparator<? super T> comparator);
- Optional<T> min(Comparator<? super T> comparator);
從中可以看出,入?yún)⑹且粋€Comparator函數(shù)式結(jié)果,返回的是一個Optional。一個例子:
- Optional<String> maxStrOpt = Stream.of("one", "two", "three").max(String::compareToIgnoreCase);System.out.println(maxStrOpt.get());
按照字母表比較,統(tǒng)計最大值,結(jié)果為:
two
findFirst/findAny
findFirst()的主要作用是找到第一個匹配的結(jié)果。findAny()的主要作用是找到找到任意匹配的一個結(jié)果。它在并行流中特別有效,因為只要在任何分片上找到一個匹配元素,整個計算就會結(jié)束。
返回結(jié)果都是Optional。
接口定義如下:
- Optional<T> findFirst();
- Optional<T> findAny();
一個例子:
- Optional<String> findFirstResult = Stream.of("one", "two", "three").filter(word -> word.contains("o").findFirst();
- System.out.println(findFirstResult.get());
- Optional<String> findAnyResult = Stream.of("one", "two", "three").filter(word -> word.contains("t").findAny();
- System.out.println(findAnyResult.get());
結(jié)果為:
one two
anyMatch/allMatch/noneMatch
如果只關(guān)心是否匹配成功,即返回boolean結(jié)果,則可以使用anyMatch/allMatch/noneMatch函數(shù)。接口定義如下:
- boolean anyMatch(Predicate predicate);
- boolean allMatch(Predicate predicate);
- boolean noneMatch(Predicate predicate);
其中, anyMatch表示任意匹配(or); allMatch表示全部匹配(and); noneMatch表示不匹配(not)。
一個例子:
- boolean anyMatch = Stream.of("one", "two", "three").anyMatch(word -> word.contains("o"));
- boolean allMatch = Stream.of("one", "two", "three").allMatch(word -> word.contains("o"));
- boolean noneMatch = Stream.of("one", "two", "three").noneMatch(word -> word.contains("o");
- System.out.println(anyMatch + ", " + allMatch + ", " + noneMatch);
結(jié)果為:
true, false, false
reduce
reduce()主要進(jìn)行歸約操作,它提供了三種不同的用法。
用法1:接口定義:
- Optional<T> reduce(BinaryOperator<T> accumulator);
它主要接收一個BinaryOperator的累加器,返回Optional類型。
一個例子:
- Optional<Integer> sum1 = Stream.of(1, 2, 3).reduce((x, y) -> x + y);System.out.println(sum1.get());
對數(shù)字流求和,結(jié)果為:
6
用法2:接口定義:
- T reduce(T identity, BinaryOperator<T> accumulator);
和上一個方法不一樣的地方是:它提供了一個初始值identity,這樣就保證整個計算結(jié)果時不可能為空,所以不再返回Optional,直接返回對應(yīng)的類型T。
一個例子:
- Integer sum2 = Stream.of(1, 2, 3).reduce(10, (x, y) -> x + y);System.out.println(sum2);
結(jié)果為:
16
用法3:
接口定義:
- <U > U reduce(U identity, BiFunction < U, ? super T, U > accumulator, BinaryOperator < U > combiner);
這是最復(fù)雜的一種用法,它主要用于把元素轉(zhuǎn)換成不同的數(shù)據(jù)類型。accumulator是累加器,主要進(jìn)行累加操作,combiner是把不同分段的數(shù)據(jù)組合起來(并行流場景)。
一個例子:
- Integer sum3 = Stream.of("on", "off").reduce(0, (total, word) -> total + word.length(), (x, y) -> x + y);
- System.out.println(sum3);
統(tǒng)計元素的單詞長度,并累加在一起,結(jié)果為:
5
7收集操作 (collect)
collect()方法主要用于把流轉(zhuǎn)換成其他的數(shù)據(jù)類型。
轉(zhuǎn)換成集合
可以通過Collectors.toList()/toSet()/toCollection()方法轉(zhuǎn)成List,Set,以及指定的集合類型。一個例子:
- List<Integer> numbers = asList(1, 2, 3, 4, 5);
- // 轉(zhuǎn)換成List
- List<Integer> numberList = numbers.stream().collect(toList());
- // 轉(zhuǎn)換成Set
- Set<Integer> numberSet = numbers.stream().collect(toSet());
- // 通過toCollection轉(zhuǎn)成TreeSet
- TreeSet<Integer> numberTreeSet = numbers.stream().collect(Collectors.toCollection(TreeSet::new));
注:
- 這里對類似Collectors.toList的方法實施了靜態(tài)導(dǎo)入。
- toList()默認(rèn)轉(zhuǎn)成ArrayList,toSet()默認(rèn)轉(zhuǎn)成HashSet,如果這兩種數(shù)據(jù)類型都不滿足要求的話,可以通過toCollectio()方法轉(zhuǎn)成需要的集合類型。
轉(zhuǎn)換成值
除了轉(zhuǎn)成集合外,還可以把結(jié)果轉(zhuǎn)成值。常用的轉(zhuǎn)換函數(shù)包括:
- Collectors.summarizingInt()/summarizingLong()/summarizingDouble() // 獲取統(tǒng)計信息,進(jìn)行求和、平均、數(shù)量、最大值、最小值。
- Collectors.maxBy()/minBy() // 求最大值/最小值
- Collectors.counting() // 求數(shù)量
- Collectors.summingInt()/summingLong()/summingDouble() // 求和
- Collectors.averagingInt()/averagingDouble()/averagingDouble() // 求平均
- Collectors.joining() // 對字符串進(jìn)行連接操作
一個例子:
- List<String> wordList = Arrays.asList("one", "two", "three");
- // 獲取統(tǒng)計信息,打印平均和最大值
- IntSummaryStatistics summary = wordList.stream().collect(summarizingInt(String::length));
- System.out.println(summary.getAverage() + ", " + summary.getMax());
- // 獲取單詞的平均長度
- Double averageLength = wordList.stream().collect(averagingInt(String::length));
- // 獲取最大的單詞長度
- Optional<String> maxLength = wordList.stream().collect(maxBy(Comparator.comparing(String::length)));
這些方法的共同特點是:返回的數(shù)據(jù)類型都是Collector。雖然可以單獨在Collect()方法中使用,但實際卻很少這樣用(畢竟Stream本身也提供了類似的方法),它更常用的用法是配合groupingBy()方法一起使用,以便對分組后的數(shù)據(jù)進(jìn)行二次加工。
8分區(qū)操作(partitioningBy)
partitioningBy操作是基于collect操作完成的,它會根據(jù)條件對流進(jìn)行分區(qū)操作,返回一個Map,Key是boolean型,Value是對應(yīng)分區(qū)的List,也就是說結(jié)果只有符合條件和不符合條件兩種。接口定義如下:
- public static <T> Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate)
一個例子:
- public Map<Boolean, List<Student>> maleAndFemaieStudents(Stream<Student> students) { return students.collect(Collectors.partitioningBy(student -> student.isMale()));
- }
按性別對學(xué)生流進(jìn)行分區(qū),結(jié)果保存在Map中。
9分組操作(groupingBy)
groupingBy操作也是基于collect操作完成的,功能是根據(jù)條件進(jìn)行分組操作,他和partitioningBy不同的一點是,它的輸入是一個Function,這樣返回結(jié)果的Map中的Key就不再是boolean型,而是符合條件的分組值,使用場景會更廣泛。
接口定義如下:
- public static Collector>> groupingBy(Function classifier)
一個例子
- public Map> studentByName(Stream students) {
- return students.collect(Collectors.groupingBy(student -> student.getName()));
- }
按照學(xué)生的姓名進(jìn)行分組。之前也提過,groupingBy函數(shù)可以配合聚合函數(shù)做更復(fù)雜的操作。下面介紹幾種常見的使用場景:
按照城市所在的州進(jìn)行分組,再統(tǒng)計數(shù)量。
- public Map stateToCount(Stream cities) {
- return cities.collect(groupingBy(City::getState, counting()));
- }
按照城市所在的州進(jìn)行分組,再統(tǒng)計人口總數(shù)。
- public Map stateToCityPopulation(Stream cities) {
- return cities.collect(groupingBy(City::getState, summingInt(City::getPopulation)));
- }
按照城市所在的州進(jìn)行分組,再找出每州人口最多的城市。
- public Map stateToLargestCity(Stream cities) {
- return cities.collect(groupingBy(City::getState, maxBy(Comparator.comparing(City::getPopulation))));
- }
按照城市所在的州進(jìn)行分組,再找出每州城市名最長的名稱。
- public Map> stateToLongestCityName(Stream cities) {
- return cities.collect(groupingBy(City::getState, mapping(City::getName, maxBy(Comparator.comparing(String::length)))));
- }
按照城市所在的州進(jìn)行分組,再按照人口獲取統(tǒng)計信息。利用統(tǒng)計信息可以執(zhí)行求和、平均、數(shù)量、最大/最小值
- public Map stateToCityPopulationSummary(Stream cities) {
- return cities.collect(groupingBy(City::getState, summarizingInt(City::getPopulation)));
- }
按照城市所在的州進(jìn)行分組,再把每州的城市名連接起來
- public Map stateToCityNames(Stream cities) {
- return cities.collect(groupingBy(City::getState, reducing("", City::getName, (s, t) -> s.length() == 0 ? t : s + ", " + t)));
- }
按照城市所在的州進(jìn)行分組,再把每州的城市名連接起來,使用joining函數(shù)。
- public Map<String, String> stateToCityNames2(Stream<City> cities) {
- return cities.collect(groupingBy(City::getState, mapping(City::getName, joining(", "))));
- }
從以上例子可以看出,groupingBy函數(shù)配合聚合函數(shù)可以組成表示出很復(fù)雜的應(yīng)用場景。
10基本類型流(IntStream,LongStream,DoubleStream)
在前面介紹的流中,都是使用的Stream配合泛型來標(biāo)示元素類型的。Java8中還為基本數(shù)據(jù)類型提供了更直接的流方式,以簡化使用。
對于byte,short,int,char,booelan類型可以使用IntStream;
對于long類型可以使用LongStream;
對于float和Double類型可以使用DoubleStream。
創(chuàng)建基本類型流的例子:
- IntStream intStream = IntStream.of(1, 2, 3);
- // 不包含上限10
- IntStream rangeStream = IntStream.range(1, 10);
- // 包含上限10
- IntStream rangeClosedStream = IntStream.rangeClosed(1, 10);
基本類型流還直接提供了sum, average, max, min等在Stream中并沒有的方法。還有一個mapToInt/mapToLong/mapToDouble方法把流轉(zhuǎn)成基本類型流。利用這兩個個特性,可以方便執(zhí)行某些操作,再看一個例子。
- Stream<String> twoWords = Stream.of("one", "two");
- int twoWordsLength = twoWords.mapToInt(String::length).sum();
對原始字符串流統(tǒng)計字符總長度。
11在文件操作中使用流
文件操作也是我們平時用的比較多的一種操作,利用流也可以幫助我們簡化操作。
訪問目錄和過濾
- Files.list(Paths.get(".")).forEach(System.out::println);Files.list(Paths.get(".")).filter(Files::isDirectory);
按擴(kuò)展名過濾文件
- Files.newDirectoryStream(Paths.get("."), path -> path.toString().endsWith("md")).forEach(System.out::println);File[] textFiles = new File(".").listFiles(path -> path.toString().endsWith("txt"));
訪問子目錄
- List<File> allFiles = Stream.of(new File(".").listFiles()).flatMap(file -> file.listFiles() == null ? Stream.of(file) : Stream.of(file.listFiles())).collect(toList());
12小結(jié)
Stream 是 Java8 中處理集合的關(guān)鍵抽象概念,它可以指定對集合進(jìn)行的操作,可以執(zhí)行非常復(fù)雜的查找、過濾和映射數(shù)據(jù)等操作。使用Stream API 對集合數(shù)據(jù)進(jìn)行操作,就類似于使用 SQL 執(zhí)行的數(shù)據(jù)庫查詢。也可以使用 Stream API 來并行執(zhí)行操作。簡而言之,Stream API 提供了一種高效且易于使用的處理數(shù)據(jù)的方式。詳細(xì)的API可以參見下面腦圖: