Java8的Stream API使用

前言

此次想介紹一下Java Stream的API使用,最近在作一個新的項目,而後終於能夠從老項目的祖傳代碼坑裏跳出來了。項目用公司本身的框架搭建完成後,我就想着把JDK版本也升級一下吧(以前的項目,最高就能用JDK7),可是後來發現公司的項目部署打包平臺最高只支持到JDK8。那好吧,既然就支持到JDK8,也能知足平常需求了(要啥自行車),升級到JDK8後,在搭建完項目架構後,就開始寫一些基礎邏輯。其中就用到了一些JDK8的Stream。可是個人同事在看個人代碼的時候表示看不懂。確實,這個我也認可,Lambda表達式雖然代碼簡潔,可是不會用的人會以爲它的可讀性不是太好。因此此次就結合本身使用經驗來介紹一下Java Stream的一些功能。java

從遍歷到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()方法來使用Streamapp

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()方法來實現的。ide

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));
});

 

 

 

 

文章會同步到個人公衆號上面,歡迎關注。

相關文章
相關標籤/搜索