Java8的Stream API使用

紀莫發表於2019-06-17

前言

這次想介紹一下Java Stream的API使用,最近在做一個新的專案,然後終於可以從老專案的祖傳程式碼坑裡跳出來了。專案用公司自己的框架搭建完成後,我就想著把JDK版本也升級一下吧(之前的專案,最高就能用JDK7),但是後來發現公司的專案部署打包平臺最高只支援到JDK8。那好吧,既然就支援到JDK8,也能滿足日常需求了(要啥自行車),升級到JDK8後,在搭建完專案架構後,就開始寫一些基礎邏輯。其中就用到了一些JDK8的Stream。但是我的同事在看我的程式碼的時候表示看不懂。確實,這個我也承認,Lambda表示式雖然程式碼簡潔,但是不會用的人會覺得它的可讀性不是太好。所以這次就結合自己使用經驗來介紹一下Java Stream的一些功能。

從遍歷到Stream操作

Oracle 公司於 2014 年 3 月 18 日釋出 Java 8,Java8主要是在原來物件導向的基礎上增加了函數語言程式設計的能力。這樣就出現了在Java中使用Lambda表示式,將一個函式作為方法的引數來進行傳遞。Java8的Stream就是典型的例子,Stream API可以極大提高Java程式設計師的生產力,讓程式設計師寫出高效率、乾淨、簡潔的程式碼。

例子:

List<Integer> numbers = new ArrayList<>();
numbers.add(3);
numbers.add(4);
numbers.add(8);
numbers.add(16);
numbers.add(19);
numbers.add(27);
numbers.add(23);
numbers.add(99);
numbers.add(15);
numbers.add(32);
numbers.add(5);
numbers.add(232);
numbers.add(56);
int count = 0;
for(Integer i:numbers){
if(i>20){
count++;
}
}
System.out.println("count:"+count);

如上遍歷的程式碼轉換成使用Stream的API來實現如下:

long count = numbers.stream().filter(i->i>20).count();
System.out.println("count:"+count);

正常的遍歷用Stream一行就可以實現了。

下面是一個使用了Stream API實現的流程圖。

轉換成Java程式碼就是

Integer transactionsIds =
                roomList.stream()
                        .filter(b -> b.getLength() == 10)
                        .sorted((x,y) -> x.getHigh() - y.getHigh())
                        .mapToInt(Room::getWidth).sum();

建立Stream

Arrays.stream()

當在日常程式設計中面對的是一個陣列,也可以使用Arrays.stream()方法來使用Stream

Integer[] array = new Integer[]{3,4,8,16,19,27,23,99,76,232,33,96};
long count = Arrays.stream(array).filter(i->i>20).count();

Stream.of()

當面對陣列時除了可以使用Arrays.stream()方法外,還可以使用Stream將需要的陣列轉成Stream。這個方法不但支援傳入陣列,將陣列轉成Stream,也支援傳入多個引數,將引數最終轉成Stream

Integer[] array = new Integer[]{3,4,8,16,19,27,23,99,76,232,33,96};
long count = Stream.of(array).filter(i->i>20).count();
long sum = Stream.of(12,77,59,3,654).filter(i->i>20).mapToInt(Integer::intValue).sum();
System.out.println("count:"+count+",sum:"+sum);

其實Stream.of()也是呼叫的Stream.of()方法來實現的。

Stream.generate()

Stream介面有兩個用來建立無限Stream的靜態方法。generate()方法接受一個引數函式,可以使用類似如下程式碼來建立一個你需要的Stream。

Stream<String> stream = Stream.generate(() -> "test").limit(10);
String[] strArr = stream.toArray(String[]::new);
System.out.println(Arrays.toString(strArr));

執行結果

[test, test, test, test, test, test, test, test, test, test]

Stream.iterate()

Stream介面的另一用來建立無限Stream的靜態方法就是iterate()方法。iterate()方法也是接受一個引數函式,可以用類似如下程式碼來建立一個你需要的Stream。

Stream<BigInteger> bigIntStream = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.TEN)).limit(10);
BigInteger[] bigIntArr = bigIntStream.toArray(BigInteger[]::new);
System.out.println(Arrays.toString(bigIntArr));

執行結果

[0, 10, 20, 30, 40, 50, 60, 70, 80, 90]

Collection.stream()

這個就是最常見的Stream了。因為Collection是Java中集合介面的父介面,Java中的集合都繼承或實現了此介面。所以Java中的集合都可以使用此方法來建立一個Stream;

      /**
        * @see     Set
        * @see     List
        * @see     Map
        * @see     SortedSet
        * @see     SortedMap
        * @see     HashSet
        * @see     TreeSet
        * @see     ArrayList
        * @see     LinkedList
        * @see     Vector
        * @see     Collections
        * @see     Arrays
        * @see     AbstractCollection
        * @since 1.2
        */
        public interface Collection<E> extends Iterable<E> {
            /**
             * Returns a sequential {@code Stream} with this collection as its source.
             *
             * <p>This method should be overridden when the {@link #spliterator()}
             * method cannot return a spliterator that is {@code IMMUTABLE},
             * {@code CONCURRENT}, or <em>late-binding</em>. (See {@link #spliterator()}
             * for details.)
             *
             * @implSpec
             * The default implementation creates a sequential {@code Stream} from the
             * collection's {@code Spliterator}.
             *
             * @return a sequential {@code Stream} over the elements in this collection
             * @since 1.8
             */
            default Stream<E> stream() {
                return StreamSupport.stream(spliterator(), false);
            }
        }

例子

List<Integer> numbers = new ArrayList<>();
numbers.add(3);
numbers.add(4);
numbers.add(8);
numbers.add(16);   
numbers.stream().forEach(number->{
    System.out.println(number);
});

StreamSupport.stream()

通過檢視Collection.stream()的方法,我們可以看出來,Colleciton.stream()其實是呼叫了StreamSupport.stream()來實現的。所以我們也可以使用StreamSupport.stream()來建立一個Stream。當我們面對的是一個迭代器的時候,使用StreamSupport.stream()就可以建立一個Stream。第一個引數是傳入一個迭代器,第二個引數是true代表使用並行來進行處理。false代表序列來處理Stream。

List<Integer> numbers = new ArrayList<>();
numbers.add(
3); numbers.add(4); numbers.add(8); numbers.add(16); numbers.add(19); numbers.add(27); numbers.add(23); Spliterator<Integer> integers = numbers.spliterator(); StreamSupport.stream(integers,false).forEach(number->{   System.out.println(number); });

流的轉換

filter方法

從名字上就能看出來,這是一個Stream的過濾轉換,此方法會生成一個新的流,其中包含符合某個特定條件的所有元素。

List<Integer> integerList = Lists.newArrayList();
integerList.add(15);
integerList.add(32);
integerList.add(5);
integerList.add(232);
integerList.add(56);
List<Integer> after = integerList.stream()
                    .filter(i->i>50)
                    .collect(Collectors.toList());

System.out.println(after);

執行結果:

[232, 56]

map方法

map方法指對一個流中的值進行某種形式的轉換。需要傳遞給它一個轉換的函式作為引數。

List<Integer> integerList = Lists.newArrayList();
integerList.add(15);
integerList.add(32);
integerList.add(5);
integerList.add(232);
integerList.add(56);
//將Integer型別轉換成String型別
List<String> afterString = integerList.stream()
                .map(i->String.valueOf(i)).collect(Collectors.toList());
System.out.println(afterString);

flatMap方法

上面用map方法進行流轉換的時候,是對每個元素應用一個函式,並將返回的值收集到一個新的流中。但是如果有一個函式,它返回的不是一個值,而是一個包含多個值的流。但是你需要的是一個包含多個流中的元素的集合。

例如

List<Integer> oneList = Lists.newArrayList(),
twoList = Lists.newArrayList();
oneList.add(34);
oneList.add(23);
oneList.add(87);

twoList.add(29);
twoList.add(48);
twoList.add(92);
Map<String,List<Integer>> testMap = Maps.newHashMap();
testMap.put("1",oneList);
testMap.put("2",twoList);
//返回的是一個流的集合,但是我需要的是List<Integer>這樣一個集合
List<Stream<Integer>> testList = testMap.values().stream()
                    .map(number->number.stream()).collect(Collectors.toList());

這個時候就應該使用flatMap將多個流進行合併,然後再收集到一個集合中。

List<Integer> testList = testMap.values().stream()
                .flatMap(number->number.stream()).collect(Collectors.toList());

limit方法和skip方法

limit(n)方法會返回一個包含n個元素的新的流(若總長小於n則返回原始流)。

List<Integer> myList = Lists.newArrayList();
myList.add(1);
myList.add(2);
myList.add(3);
myList.add(4);
myList.add(5);
myList.add(6);
List<Integer> afterLimit = myList.stream().limit(4).collect(Collectors.toList());
System.out.println("afterLimit:"+afterLimit);

skip(n)方法正好相反,它會丟棄掉前面的n個元素。

List<Integer> afterSkip = myList.stream().skip(4).collect(Collectors.toList());
System.out.println("afterSkip:"+afterSkip);

執行結果:

afterLimit:[1, 2, 3, 4]
afterSkip:[5, 6]

用limit和skip方法一起使用就可以實現日常的分頁功能:

List<Integer> pageList = myList.stream()
                  .skip(pageNumber*pageSize)
                  .limit(pageSize).collect(Collectors.toList());

distinct方法和sorted方法

上面介紹的流的轉換方法都是無狀態的。即從一個已經轉換的流中取某個元素時,結果並不依賴於之前的元素。除此之外還有兩個方法在轉換流時是需要依賴於之前流中的元素的。一個是distinct方法一個是sorted方法。

distinct方法會根據原始流中的元素返回一個具有相同順序、去除了重複元素的流,這個操作顯然是需要記住之前讀取的元素。

List<Integer> myTestList = Lists.newArrayList();
myTestList.add(10);
myTestList.add(39);
myTestList.add(10);
myTestList.add(78);
myTestList.add(10);
List<Integer> distinctList = myTestList.stream()
                        .distinct().collect(Collectors.toList());
System.out.println("distinctList:"+distinctList);

執行結果:

distinctList:[10, 39, 78]

sorted方法是需要遍歷整個流的,並在產生任何元素之前對它進行排序。因為有可能排序後集合的第一個元素會在未排序集合的最後一位。

List<Integer> myTestList = Lists.newArrayList();
myTestList.add(39);
myTestList.add(78);
myTestList.add(10);
myTestList.add(22);
myTestList.add(56);
List<Integer> sortList = myTestList.stream()
                .sorted(Integer::compareTo).collect(Collectors.toList()); System.out.println(
"sortList:"+sortList);

執行結果:

sortList:[10, 22, 39, 56, 78]

聚合操作

前面已經介紹了流的建立和轉換,下面介紹流的聚合,聚合是指將流匯聚為一個值,以便在程式中使用。聚合方法都是終止操作。

max方法min方法

在前面的程式碼例子中使用的count方法sum方法都屬於流從聚合方法。還有兩個聚合方法是max方法min方法,分別返回流中最大值和最小值。


List<Integer> hearList = Lists.newArrayList();
hearList.add(15);
hearList.add(32);
hearList.add(5);
hearList.add(232);
hearList.add(56);
hearList.add(29);
hearList.add(94);
Integer maxItem = hearList.stream().max(Integer::compareTo).get();
Integer minItem = hearList.stream().min(Integer::compareTo).get();
System.out.println("max:"+maxItem+",min:"+minItem);

執行結果:

max:232,min:5

findFirst方法

findFirst方法返回非空集合中的第一個值,它通常與filter方法結合起來使用。

List<Integer> hearList = Lists.newArrayList();
hearList.add(15);
hearList.add(32);
hearList.add(5);
hearList.add(232);
hearList.add(56);
hearList.add(29);
hearList.add(104);
Integer first = hearList.stream().filter(i->i>100).findFirst().get();

findAny方法

findAny方法可以在集合中只要找到任何一個所匹配的元素,就返回,此方法在對流並行執行時十分有效(任何片段中發現第一個匹配元素都會結束計算,序列流中和findFirst返回一樣)。

Integer anyItem = hearList.parallelStream().filter(i->i>100).findAny().get();

anyMatch方法

anyMatch方法可以判定集合中是否還有匹配的元素。返回結果是一個boolean型別值。

boolean isHas = hearList.parallelStream().anyMatch(i->i>100);

allMatch方法noneMatch方法

allMatch方法noneMatch方法,分別在所有元素匹配和沒有元素匹配時返回true。

boolean allHas = hearList.parallelStream().allMatch(i->i>100);
boolean noHas = hearList.parallelStream().noneMatch(i->i>100);

雖然這些方法總是會檢查整個流,但是仍然可以通過並行執行來提高速度。 

reduce方法

reduce方法是將流中的元素進行進一步計算的方法。

List<Integer> hearList = Lists.newArrayList();
hearList.add(15);
hearList.add(32);
hearList.add(5);
hearList.add(232);
hearList.add(56);
hearList.add(29);
hearList.add(104);
//求和
Integer sum = hearList.stream().reduce((x,y)->x+y).get();
System.out.println("sum:"+sum);
//簡化一下,求和
sum = hearList.stream().reduce(Integer::sum).get();
System.out.println("sum:"+sum);
//含有初始標識的,求和
sum = hearList.stream().reduce(0,(x,y)->x+y);
System.out.println("sum:"+sum);
//對元素的長度進行求和( (total,y)->total+y.toString().length(),類似於一個累加器,會被重複呼叫)
sum = hearList.stream().reduce(0,(total,y)->total+y.toString().length(),(total1,total2)->total1+total2);
System.out.println("sum:"+sum);
//簡化一下,對元素長度進行求和。
sum = hearList.stream().map(Objects::toString).mapToInt(String::length).sum();
System.out.println("sum:"+sum);

執行結果

sum:473
sum:473
sum:473
sum:15
sum:15

收集結果

當處理完流之後,通常是想檢視一下結果,而不是將他們聚合為一個值。Collectorts類為我們提供了常用的收集類的各個工廠方法。

收集到集合

例如前面的例子用的要將一個流收集到一個List中,只需要這樣寫就可以。

List<Integer> thereList = hereList.stream().collect(Collectors.toList());

收集到Set中可以這樣用

Set<Integer> thereSet = hereList.stream().collect(Collectors.toSet());

收集到Set時,控制Set的型別,可以這樣。

TreeSet<Integer> treeSet = hereList.stream()
                    .collect(Collectors.toCollection(TreeSet::new));

拼接

將字流中的字串連線並收集起來。

String resultString = stringList.stream().collect(Collectors.joining());

在將流中的字串連線並收集起來時,想在元素中介新增分隔符,傳遞個joining方法即可。

String resultString = stringList.stream().collect(Collectors.joining(","));

當流中的元素不是字串時,需要先將流轉成字串流再進行拼接。

String hereResultString = hereList.stream()
                .map(String::valueOf).collect(Collectors.joining(","));

收集聚合

分別收集流的總和、平均值、最大值或者最小值。

List<Integer> hereList = Lists.newArrayList();
hereList.add(15);
hereList.add(32);
hereList.add(5);
hereList.add(232);
hereList.add(56);
hereList.add(29);
hereList.add(104);

//總和、平均值,最大值,最小值
int sum = hereList.stream().collect(Collectors.summingInt(Integer::intValue));
Double ave = hereList.stream().collect(Collectors.averagingInt(Integer::intValue));
Integer max = hereList.stream().collect(Collectors.maxBy(Integer::compare)).get();
Integer min = hereList.stream().collect(Collectors.minBy(Integer::compare)).get();
System.out.println("sum:"+sum+",ave:"+ave+",max:"+max+",min:"+min);

執行結果:

sum:473,ave:67.57142857142857,max:232,min:5

一次性收集流中的結果,聚合為一個總和,平均值,最大值或最小值的物件。

IntSummaryStatistics summaryStatistics = hereList.stream()
                          .collect(Collectors.summarizingInt(Integer::intValue)); System.out.println(summaryStatistics);

執行結果:

IntSummaryStatistics{count=7, sum=473, min=5, average=67.571429, max=232}

將結果集收集到Map

當我們希望將集合中的元素收集到Map中時,可以使用Collectors.toMap方法。這個方法有兩個引數,用來生成Map的key和value。

例如將一個Room物件的high作為鍵width作為值

Map<Integer,Integer> hwMap = roomList.stream()
                        .collect(Collectors.toMap(Room::getHigh, Room::getWidth));

但是通常還是以具體元素作為值的情況多,可以使用Function.identity()來獲取實際元素。

Map<Integer,Room> roomMap = roomList.stream()
                        .collect(Collectors.toMap(Room::getHigh, Function.identity()));

如果多個元素擁有相同的鍵,在收集結果時會丟擲java.lang.IllegalStateException異常。可以使用第三個引數來解決,第三個引數用來確定當出現鍵衝突時,該如何處理結果,如果當出現鍵衝突時只保留一個並且是保留已經存在的值時,就是如下方式。

Map<Integer,Room> rMap = roomList.stream()
                .collect(Collectors.toMap(Room::getHigh, Function.identity(),(nowValue,newValue)->nowValue));

如果想指定生成的Map型別,則還需要第三個引數。

TreeMap<Integer,Room> roomTreeMap = roomList.stream()
                .collect(Collectors.toMap(Room::getHigh, 
            Function.identity(),(nowValue,newValue)
->newValue,TreeMap::new));

注意:每個toMap方法,都會有一個對應的toConCurrentMap方法,用來生成一個併發Map。

分組分片

在一個集合中,對具有相同特性的值進行分組是一個很常見的功能,在Stream的API中也提供了相應的方法。

分組

還是上面的例子,將一個Room物件集合按照高度分組。

List<Room> roomList = Lists.newArrayList(
new Room(11,23,56),
new Room(11,84,48),
new Room(22,46,112),
new Room(22,75,62),
new Room(22,56,75),
new Room(33,92,224));

Map<Integer,List<Room>> groupMap = roomList.stream().collect(Collectors.groupingBy(Room::getHigh));
System.out.println("groupMap:"+groupMap);

執行結果:

groupMap:{33=[Room(high=33, width=92, length=224)], 
22=[Room(high=22, width=46, length=112), Room(high=22, width=75, length=62), Room(high=22, width=56, length=75)],
11=[Room(high=11, width=23, length=56), Room(high=11, width=84, length=48)]}

分片 

當分類函式是一個返回布林值的函式時,流元素會被分為兩組列表:一組是返回true的元素集合,另一組是返回false的元素集合。這種情況適用partitoningBy方法會比groupingBy更有效率。

例如我們將房間集合分為兩組,一組是高度為22的房間,另一組是其他房間。

Map<Boolean,List<Room>> partitionMap = roomList.stream()
                .collect(Collectors.partitioningBy(room->room.getHigh()==22));

執行結果:

partitionMap:{false=[Room(high=11, width=23, length=56), Room(high=11, width=84, length=48), Room(high=33, width=92, length=224)],
true=[Room(high=22, width=46, length=112), Room(high=22, width=75, length=62), Room(high=22, width=56, length=75)]}

擴充套件功能

下面要介紹的這些方法功能,無論是groupingBy方法還是partitioningBy方法都是支援的。

counting方法會返回收集元素的總個數。

Map<Integer,Long> countMap = roomList.stream()
           .collect(Collectors.groupingBy(Room::getHigh,Collectors.counting()));

summing(Int|Long|Double)方法接受一個取值函式作為引數,來計算總和。

Map<Integer,Integer> sumMap = roomList.stream().
                collect(Collectors.groupingBy(Room::getHigh,Collectors.summingInt(Room::getWidth)));

maxBy方法和minBy方法接受比較器作為引數來計算最大值和最小值。

取出分組中寬度最大和最小的房間。

Map<Integer, Optional<Room>> maxMap = roomList.stream().
                collect(Collectors.groupingBy(Room::getHigh,
                        Collectors.maxBy(Comparator.comparing(Room::getWidth))
                ));
Map<Integer, Optional<Room>> minMap = roomList.stream().
                collect(Collectors.groupingBy(Room::getHigh,
                        Collectors.maxBy(Comparator.comparing(Room::getWidth))
                ));

System.out.println("maxMap:"+ JSON.toJSONString(maxMap));
System.out.println("minMap:"+JSON.toJSONString(minMap));

執行結果:

maxMap:{33:{"high":33,"length":224,"width":92},22:{"high":22,"length":62,"width":75},11:{"high":11,"length":48,"width":84}}
minMap:{33:{"high":33,"length":224,"width":92},22:{"high":22,"length":62,"width":75},11:{"high":11,"length":48,"width":84}}

mapping方法會將結果應用到另一個收集器上。

取出分組中寬度最大的房間的寬度。

Map<Integer, Optional<Integer>> collect = roomList.stream().collect(Collectors.groupingBy(Room::getHigh,
                Collectors.mapping(Room::getWidth,
                        Collectors.maxBy(Comparator.comparing(Integer::valueOf)))));

System.out.println("collect:"+JSON.toJSONString(collect));

執行結果:

collect:{33:92,22:75,11:84}

無論groupingBy或是mapping函式,如果返回型別是int、long、double都可以將元素收集到一個summarystatistics物件中,然後從每組的summarystatistics物件中取出函式值的總和、平均值、總數、最大值和最小值。

Map<Integer,IntSummaryStatistics> summaryStatisticsMap = roomList.stream()
                .collect(Collectors.groupingBy(Room::getHigh,
                Collectors.summarizingInt(Room::getWidth)));

System.out.println("summaryStatisticsMap:"+summaryStatisticsMap);

執行結果:

summaryStatisticsMap:{33=IntSummaryStatistics{count=1, sum=92, min=92, average=92.000000, max=92}, 
22=IntSummaryStatistics{count=3, sum=177, min=46, average=59.000000, max=75},
11=IntSummaryStatistics{count=2, sum=107, min=23, average=53.500000, max=84}}

多級分組

上面的例子我們都是按一個條件進行的一級分組,其實groupingBy是支援多級分組的。

例如第一級我們將房間按照高度分組,第二級按照寬度分組。

Map<Integer,Map<Integer,List<Room>>> multistageMap = roomList.stream().collect(
          Collectors.groupingBy(Room::getHigh,Collectors.groupingBy(Room::getWidth))); System.out.println(
"multistageMap:"+JSON.toJSONString(multistageMap));

執行結果:

{
    "11": {
        "23": [
            {"high": 11,"length": 56,"width": 23}
        ],
        "84": [
            {"high": 11,"length": 48,"width": 84}
        ]
    },
    "22": {
        "46": [
            {"high": 22,"length": 112,"width": 46}
        ],
        "56": [
            {"high": 22,"length": 75,"width": 56}
        ],
        "75": [
            {"high": 22,"length": 62,"width": 75}
        ]
    },
    "33": {
        "92": [
            {"high": 33,"length": 224,"width": 92}
        ]
    }
}

並行流

Stream的建立,使得平行計算變得容易,但是並行流在使用的時候也是需要注意的。

首先,必須是一個並行流,只要在終止方法執行時,流處於並行模式,那麼所有的流操作就都會並行執行。

Stream.of(roomList).parallel();

parallel方法可以將任意的序列流轉換為一個並行流。

其次要確保傳遞給並行流操作的函式是執行緒安全的。

int[] words = new int[23];
Stream.of(roomList).parallel().forEach(s->{
     if(s.size()<10){
           words[s.size()]++;
     }
});

上面這個例子中的程式碼就是錯誤的,傳遞給並行流的操作並不是執行緒安全的。可以改為AtomicInteger的物件陣列來作為計數器。

我們使在處理集合資料量較大的時候才能體現出並行流的優勢,並且目的是為了在保證執行緒安全的情況下,提升效率,利用多核CPU的資源。

 

小擴充套件

使用Stream的API時,在遍歷或處理流的過程中當引用外部變數的時候會預設的將變數當成fianl變數來處理。所以有些同學就會覺得在遍歷的過程中取不出來集合的索引。其實可以換一種思想可以只遍歷集合索引,然後在遍歷中取值。

IntStream.range(0,roomList.size()).forEach(i->{
       System.out.println(roomList.get(i));
});

 

 

 

 

文章會同步到我的公眾號上面,歡迎關注。

 

相關文章