天天看點

Java 8系列之Stream的基本文法詳解

本文轉至:https://blog.csdn.net/io_field/article/details/54971761

概述

Stream 是用函數式程式設計方式在集合類上進行複雜操作的工具,其內建了Java 8中的衆多新特性之一的聚合操作,開發者可以更容易地使用Lambda表達式,并且更友善地實作對集合的查找、周遊、過濾以及常見計算等。

聚合操作

為了學習聚合的使用,在這裡,先定義一個資料類:

public class Student {
    int no;
    String name;
    String sex;
    float height;

    public Student(int no, String name, String sex, float height) {
        this.no = no;
        this.name = name;
        this.sex = sex;
        this.height = height;
    }

    ****
}

Student stuA = new Student(1, "A", "M", 184);
Student stuB = new Student(2, "B", "G", 163);
Student stuC = new Student(3, "C", "M", 175);
Student stuD = new Student(4, "D", "G", 158);
Student stuE = new Student(5, "E", "M", 170);
List<Student> list = new ArrayList<>();
list.add(stuA);
list.add(stuB);
list.add(stuC);
list.add(stuD);
list.add(stuE);           

現有一個List list裡面有5個Studeng對象,假如我們想擷取Sex=“G”的Student,并列印出來。如果按照我們原來的處理模式,必然會想到一個for循環就搞定了,而在for循環其實是一個封裝了疊代的文法塊。在這裡,我們采用Iterator進行疊代:

Iterator<Student> iterator = list.iterator();
while(iterator.hasNext()) {
    Student stu = iterator.next();
    if (stu.getSex().equals("G")) {

        System.out.println(stu.toString());
    }
}           

整個疊代過程是這樣的:首先調用iterator方法,産生一個新的Iterator對象,進而控制整

個疊代過程,這就是外部疊代 疊代過程通過顯式調用Iterator對象的hasNext和next方法完成疊代

而在Java 8中,我們可以采用聚合操作:

list.stream()
    .filter(student -> student.getSex().equals("G"))
    .forEach(student -> System.out.println(student.toString()));
           

首先,通過stream方法建立Stream,然後再通過filter方法對源資料進行過濾,最後通過foeEach方法進行疊代。在聚合操作中,與Labda表達式一起使用,顯得代碼更加的簡潔。這裡值得注意的是,我們首先是stream方法的調用,其與iterator作用一樣的作用一樣,該方法不是傳回一個控制疊代的 Iterator 對象,而是傳回内部疊代中的相應接口: Stream,其一系列的操作都是在操作Stream,直到feach時才會操作結果,這種疊代方式稱為内部疊代。

外部疊代和内部疊代(聚合操作)都是對集合的疊代,但是在機制上還是有一定的差異:

  1. 疊代器提供next()、hasNext()等方法,開發者可以自行控制對元素的處理,以及處理方式,但是隻能順序處理;
  2. stream()方法傳回的資料集無next()等方法,開發者無法控制對元素的疊代,疊代方式是系統内部實作的,同時系統内的疊代也不一定是順序的,還可以并行,如parallelStream()方法。并行的方式在一些情況下,可以大幅提升處理的效率。

Stream

如何使用Stream?

聚合操作是Java 8針對集合類,使程式設計更為便利的方式,可以與Lambda表達式一起使用,達到更加簡潔的目的。

前面例子中,對聚合操作的使用可以歸結為3個部分:

  1. 建立Stream:通過stream()方法,取得集合對象的資料集。
  2. Intermediate:通過一系列中間(Intermediate)方法,對資料集進行過濾、檢索等資料集的再次處理。如上例中,使用filter()方法來對資料集進行過濾。
  3. Terminal通過最終(terminal)方法完成對資料集中元素的處理。如上例中,使用forEach()完成對過濾後元素的列印。

在一次聚合操作中,可以有多個Intermediate,但是有且隻有一個Terminal。也就是說,在對一個Stream可以進行多次轉換操作,并不是每次都對Stream的每個元素執行轉換。并不像for循環中,循環N次,其時間複雜度就是N。轉換操作是lazy(惰性求值)的,隻有在Terminal操作執行時,才會一次性執行。可以這麼認為,Stream 裡有個操作函數的集合,每次轉換操作就是把轉換函數放入這個集合中,在 Terminal 操作的時候循環 Stream 對應的集合,然後對每個元素執行所有的函數。

Stream的操作分類

剛才提到的Stream的操作有Intermediate、Terminal和Short-circuiting:

  • Intermediate:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 skip、 parallel、 sequential、 unordered
  • Terminal:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、iterator
  • Short-circuiting:

    anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

惰性求值和及早求值方法

像filter這樣隻描述Stream,最終不産生新集合的方法叫作惰性求值方法;而像count這樣最終會從Stream産生值的方法叫作及早求值方法。

long count = allArtists.stream()
    .filter(artist -> {
        System.out.println(artist.getName());
            return artist.isFrom("London");
        })
    .count();
           

如何判斷一個操作是惰性求值還是及早求值,其實很簡單,隻需要看其傳回值即可:如果傳回值是Stream,那麼就是惰性求值;如果傳回值不是Stream或者是void,那麼就是及早求值。上面的示例中,隻是包含兩步:一個惰性求值-filter和一個及早求值-count。

前面,已經說過,在一個Stream操作中,可以有多次惰性求值,但有且僅有一次及早求值。

建立Stream

我們有多種方式生成Stream:

  1. Stream接口的靜态工廠方法(注意:Java8裡接口可以帶靜态方法);
  2. Collection接口和數組的預設方法(預設方法,也使Java的新特性之一,後續介紹),把一個Collection對象轉換成Stream
  3. 其他
    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()

靜态工廠方法

of

of方法,其生成的Stream是有限長度的,Stream的長度為其内的元素個數。

- of(T... values):傳回含有多個T元素的Stream
- of(T t):傳回含有一個T元素的Stream           

示例:

Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<String> stringStream = Stream.of("A");
           

generator

generator方法,傳回一個無限長度的Stream,其元素由Supplier接口的提供。在Supplier是一個函數接口,隻封裝了一個get()方法,其用來傳回任何泛型的值,該結果在不同的時間内,傳回的可能相同也可能不相同,沒有特殊的要求。

- generate(Supplier<T> s):傳回一個無限長度的Stream           
  1. 這種情形通常用于随機數、常量的 Stream,或者需要前後元素間維持着某種狀态資訊的 Stream。
  2. 把 Supplier 執行個體傳遞給 Stream.generate() 生成的 Stream,預設是串行(相對 parallel 而言)但無序的(相對 ordered 而言)。
Stream<Double> generateA = Stream.generate(new Supplier<Double>() {
    @Override
    public Double get() {
        return java.lang.Math.random();
    }
});

Stream<Double> generateB = Stream.generate(()-> java.lang.Math.random());
Stream<Double> generateC = Stream.generate(java.lang.Math::random);
           

以上三種形式達到的效果是一樣的,隻不過是下面的兩個采用了Lambda表達式,簡化了代碼,其實際效果就是傳回一個随機值。一般無限長度的Stream會與filter、limit等配合使用,否則Stream會無限制的執行下去,後果可想而知,如果你有興趣,不妨試一下。

iterate

iterate方法,其傳回的也是一個無限長度的Stream,與generate方法不同的是,其是通過函數f疊代對給指定的元素種子而産生無限連續有序Stream,其中包含的元素可以認為是:seed,f(seed),f(f(seed))無限循環。

- iterate(T seed, UnaryOperator<T> f)
           
Stream.iterate(1, item -> item + 1)
        .limit(10)
        .forEach(System.out::println); 
        // 列印結果:1,2,3,4,5,6,7,8,9,10
           

上面示例,種子為1,也可認為該Stream的第一個元素,通過f函數來産生第二個元素。接着,第二個元素,作為産生第三個元素的種子,進而産生了第三個元素,以此類推下去。需要主要的是,該Stream也是無限長度的,應該使用filter、limit等來截取Stream,否則會一直循環下去。

empty

empty方法傳回一個空的順序Stream,該Stream裡面不包含元素項。

Collection接口和數組的預設方法

在Collection接口中,定義了一個預設方法stream(),用來生成一個Stream。

public interface Collection<E> extends Iterable<E> {


        ***

        default Stream<E> stream() {
            return StreamSupport.stream(spliterator(), false);
        }

        ***
    }
           

在Arrays類,封裝了一些列的Stream方法,不僅針對于任何類型的元素采用了泛型,更對于基本類型作了相應的封裝,以便提升Stream的處理效率。

public class Arrays {
    ***
    public static <T> Stream<T> stream(T[] array) {
        return stream(array, 0, array.length);
    }

   public static LongStream stream(long[] array) {
        return stream(array, 0, array.length);
    }
    ***
}           
int ids[] = new int[]{1, 2, 3, 4};
Arrays.stream(ids)
        .forEach(System.out::println);           

Intermediate

Intermediate主要是用來對Stream做出相應轉換及限制流,實際上是将源Stream轉換為一個新的Stream,以達到需求效果。

concat

concat方法将兩個Stream連接配接在一起,合成一個Stream。若兩個輸入的Stream都時排序的,則新Stream也是排序的;若輸入的Stream中任何一個是并行的,則新的Stream也是并行的;若關閉新的Stream時,原兩個輸入的Stream都将執行關閉處理。

Stream.concat(Stream.of(1, 2, 3), Stream.of(4, 5))
       .forEach(integer -> System.out.print(integer + "  "));
// 列印結果
// 1  2  3  4  5             

distinct

distinct方法以達到去除掉原Stream中重複的元素,生成的新Stream中沒有沒有重複的元素。

Stream.of(1,2,3,1,2,3)
        .distinct()
        .forEach(System.out::println); // 列印結果:1,2,3
           

建立了一個Stream(命名為A),其含有重複的1,2,3等六個元素,而實際上列印結果隻有“1,2,3”等3個元素。因為A經過distinct去掉了重複的元素,生成了新的Stream(命名為B),而B

中隻有“1,2,3”這三個元素,是以也就呈現了剛才所說的列印結果。

filter

filter方法對原Stream按照指定條件過濾,在建立的Stream中,隻包含滿足條件的元素,将不滿足條件的元素過濾掉。

Stream.of(1, 2, 3, 4, 5)
        .filter(item -> item > 3)
        .forEach(System.out::println);// 列印結果:4,5
           

建立了一個含有1,2,3,4,5等5個整型元素的Stream,filter中設定的過濾條件為元素值大于3,否則将其過濾。而實際的結果為4,5。

filter傳入的Lambda表達式必須是Predicate執行個體,參數可以為任意類型,而其傳回值必須是boolean類型。

map

map方法将對于Stream中包含的元素使用給定的轉換函數進行轉換操作,新生成的Stream隻包含轉換生成的元素。為了提高處理效率,官方已封裝好了,三種變形:mapToDouble,mapToInt,mapToLong。其實很好了解,如果想将原Stream中的資料類型,轉換為double,int或者是long是可以調用相對應的方法。

Stream.of("a", "b", "hello")
        .map(item-> item.toUpperCase())
        .forEach(System.out::println);
        // 列印結果
        // A, B, HELLO
           

傳給map中Lambda表達式,接受了String類型的參數,傳回值也是String類型,在轉換行數中,将字母全部改為大寫

map傳入的Lambda表達式必須是Function執行個體,參數可以為任意類型,而其傳回值也是任性類型,javac會根據實際情景自行推斷。

flatMap

flatMap方法與map方法類似,都是将原Stream中的每一個元素通過轉換函數轉換,不同的是,該換轉函數的對象是一個Stream,也不會再建立一個新的Stream,而是将原Stream的元素取代為轉換的Stream。如果轉換函數生産的Stream為null,應由空Stream取代。flatMap有三個對于原始類型的變種方法,分别是:flatMapToInt,flatMapToLong和flatMapToDouble。

Stream.of(1, 2, 3)
    .flatMap(integer -> Stream.of(integer * 10))
    .forEach(System.out::println);
    // 列印結果
    // 10,20,30
           

傳給flatMap中的表達式接受了一個Integer類型的參數,通過轉換函數,将原元素乘以10後,生成一個隻有該元素的流,該流取代原流中的元素。

flatMap傳入的Lambda表達式必須是Function執行個體,參數可以為任意類型,而其傳回值類型必須是一個Stream。

peek

peek方法生成一個包含原Stream的所有元素的新Stream,同時會提供一個消費函數(Consumer執行個體),新Stream每個元素被消費的時候都會執行給定的消費函數,并且消費函數優先執行

Stream.of(1, 2, 3, 4, 5)
        .peek(integer -> System.out.println("accept:" + integer))
        .forEach(System.out::println);
// 列印結果
// accept:1
//  1
//  accept:2
//  2
//  accept:3
//  3
//  accept:4
//  4
//  accept:5
//  5           

skip

skip方法将過濾掉原Stream中的前N個元素,傳回剩下的元素所組成的新Stream。如果原Stream的元素個數大于N,将傳回原Stream的後(原Stream長度-N)個元素所組成的新Stream;如果原Stream的元素個數小于或等于N,将傳回一個空Stream。

示例:

Stream.of(1, 2, 3,4,5)

.skip(2)

.forEach(System.out::println);

// 列印結果

// 3,4,5

sorted

sorted方法将對原Stream進行排序,傳回一個有序列的新Stream。sorterd有兩種變體sorted(),sorted(Comparator),前者将預設使用Object.equals(Object)進行排序,而後者接受一個自定義排序規則函數(Comparator),可按照意願排序。

Stream.of(5, 4, 3, 2, 1)
        .sorted()
        .forEach(System.out::println);
        // 列印結果
        // 1,2,3,4,5

Stream.of(1, 2, 3, 4, 5)
        .sorted()
        .forEach(System.out::println);
        // 列印結果
        // 5, 4, 3, 2, 1           

Terminal

collect

Java 8系列之Stream的強大工具Collector

Java 8系列之重構和定制收集器

count

count方法将傳回Stream中元素的個數。

long count = Stream.of(1, 2, 3, 4, 5)
        .count();
System.out.println("count:" + count);// 列印結果:count:5
           

forEach

forEach方法前面已經用了好多次,其用于周遊Stream中的所元素,避免了使用for循環,讓代碼更簡潔,邏輯更清晰。

Stream.of(5, 4, 3, 2, 1)
    .sorted()
    .forEach(System.out::println);
    // 列印結果
    // 1,2,3,4,5
           

forEachOrdered

forEachOrdered方法與forEach類似,都是周遊Stream中的所有元素,不同的是,如果該Stream預先設定了順序,會按照預先設定的順序執行(Stream是無序的),預設為元素插入的順序。

Stream.of(5,2,1,4,3)
        .forEachOrdered(integer -> {
            System.out.println("integer:"+integer);
        }); 
        // 列印結果
        // integer:5
        // integer:2
        // integer:1
        // integer:4
        // integer:3
           

max

max方法根據指定的Comparator,傳回一個Optional,該Optional中的value值就是Stream中最大的元素。至于Optional是啥,後續再做介紹吧。

原Stream根據比較器Comparator,進行排序(升序或者是降序),所謂的最大值就是從新進行排序的,max就是取重新排序後的最後一個值,而min取排序後的第一個值。
Optional<Integer> max = Stream.of(1, 2, 3, 4, 5)
        .max((o1, o2) -> o2 - o1);
System.out.println("max:" + max.get());// 列印結果:max:1
           

對于原Stream指定了Comparator,實際上是找出該Stream中的最小值,不過,在max方法中找最小值,更能展現出來Comparator的作用吧。max的值不言而喻,就是1了。

min

min方法根據指定的Comparator,傳回一個Optional,該Optional中的value值就是Stream中最小的元素。至于Optional是啥,後續再做介紹吧。

Optional<Integer> max = Stream.of(1, 2, 3, 4, 5)
        .max((o1, o2) -> o1 - o2);
System.out.println("max:" + max.get());// 列印結果:min:5
           

剛才在max方法中,我們找的是Stream中的最小值,在min中我們找的是Stream中的最大值,不管是最大值還是最小值起決定作用的是Comparator,它決定了元素比較大小的原則。

reduce

Java 8系列之Stream中萬能的reduce

Short-circuiting

allMatch

allMatch操作用于判斷Stream中的元素是否全部滿足指定條件。如果全部滿足條件傳回true,否則傳回false。

boolean allMatch = Stream.of(1, 2, 3, 4)
    .allMatch(integer -> integer > 0);
System.out.println("allMatch: " + allMatch); // 列印結果:allMatch: true 
           

anyMatch

anyMatch操作用于判斷Stream中的是否有滿足指定條件的元素。如果最少有一個滿足條件傳回true,否則傳回false。

boolean anyMatch = Stream.of(1, 2, 3, 4)
    .anyMatch(integer -> integer > 3);
System.out.println("anyMatch: " + anyMatch); // 列印結果:anyMatch: true 
           

findAny

findAny操作用于擷取含有Stream中的某個元素的Optional,如果Stream為空,則傳回一個空的Optional。由于此操作的行動是不确定的,其會自由的選擇Stream中的任何元素。在并行操作中,在同一個Stram中多次調用,可能會不同的結果。在串行調用時,Debug了幾次,發現每次都是擷取的第一個元素,個人感覺在串行調用時,應該預設的是擷取第一個元素。

Optional<Integer> any = Stream.of(1, 2, 3, 4).findAny();
           

findFirst

findFirst操作用于擷取含有Stream中的第一個元素的Optional,如果Stream為空,則傳回一個空的Optional。若Stream并未排序,可能傳回含有Stream中任意元素的Optional。

Optional<Integer> any = Stream.of(1, 2, 3, 4).findFirst();           

limit

limit方法将截取原Stream,截取後Stream的最大長度不能超過指定值N。如果原Stream的元素個數大于N,将截取原Stream的前N個元素;如果原Stream的元素個數小于或等于N,将截取原Stream中的所有元素。

Stream.of(1, 2, 3,4,5)
        .limit(2)
        .forEach(System.out::println);
        // 列印結果
        // 1,2
           

傳入limit的值為2,也就是說被截取後的Stream的最大長度為2,又由于原Stream中有5個元素,是以将截取原Stream中的前2個元素,生成一個新的Stream。

noneMatch

noneMatch方法将判斷Stream中的所有元素是否滿足指定的條件,如果所有元素都不滿足條件,傳回true;否則,傳回false.

boolean noneMatch = Stream.of(1, 2, 3, 4, 5)
        .noneMatch(integer -> integer > 10);
    System.out.println("noneMatch:" + noneMatch); // 列印結果 noneMatch:true

    boolean noneMatch_ = Stream.of(1, 2, 3, 4, 5)
            .noneMatch(integer -> integer < 3);
    System.out.println("noneMatch_:" + noneMatch_); // 列印結果 noneMatch_:false