1. 程式人生 > >深入學習java原始碼之stream.peek()與stream.concat()

深入學習java原始碼之stream.peek()與stream.concat()

深入學習java原始碼之stream.peek()與stream.concat()

Java8為集合類引入了另一個重要概念:流(stream)。一個流通常以一個集合類例項為其資料來源,然後在其上定義各種操作。流的API設計使用了管道(pipelines)模式。對流的一次操作會返回另一個流。如同IO的API或者StringBuffer的append方法那樣,從而多個不同的操作可以在一個語句裡串起來。

 

Function介面

Function<T, R>

T—函式的輸入型別 
R-函式的輸出型別

該函式式介面唯一的抽象方法apply接收一個引數,有返回值

Function介面定義中有兩個泛型,按著介面文件說明第一個泛型是輸入型別,第二泛型是結果型別。
compose方法接收一個Function引數before,該方法說明是返回一個組合的函式,首先會應用before,然後應用當前物件,換句話說就是先執行before物件的apply,再執行當前物件的apply,將兩個執行邏輯串起來。
andThen方法接收一個Function引數after,與compose方法相反,它是先執行當前物件的apply方法,再執行after物件的方法。

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
}

該函式式介面唯一的抽象方法apply接收一個引數,有返回值。

我們在FunctionTest中定義了compute方法,方法的第一個引數是要運算的資料,第二個引數是函式式介面Function的例項,當執行compute方法時,會將第一個引數交給第二個引數Function中的apply方法處理,然後返回結果。
這樣我們可以將方法定義的更抽象,程式碼重用性也就越高,每次將要計算的資料和計算邏輯一起作為引數傳遞給compute方法就可以。是不是有點體驗到函數語言程式設計的靈活之處。
注:因為表示式只有一行語句 num -> num + 2 可以省略了return 關鍵字 如果為了更加直觀可以寫成 num -> return num + 2

public class FunctionTest {
    public static void main(String[] args) {
        FunctionTest functionTest = new FunctionTest();
        int i2 = functionTest.add2(2);
        int i3 = functionTest.add3(2);
        int i4 = functionTest.add4(2);
    }

    //邏輯提前定義好
    public int add2(int i){
        return i + 2;
    }

    //邏輯提前定義好
    public int add3(int i){
        return i + 3;
    }

    //邏輯提前定義好
    public int add4(int i){
        return i + 4;
    }
}
//函式式介面代替
public class FunctionTest {
    public static void main(String[] args) {
        FunctionTest functionTest = new FunctionTest();

        int result2 = functionTest.compute(5, num -> num + 2);
        int result3 = functionTest.compute(5, num -> num + 2);
        int result4 = functionTest.compute(5, num -> num + 2);
        int results = functionTest.compute(5, num -> num * num);

    }

    //呼叫時傳入邏輯
    public int compute(int i, Function<Integer,Integer> function){
        Integer result = function.apply(i);
        return result;
    }
}

 

定義了compute1和compute2兩個方法,compute1方法第一個引數是要計算的資料,第二個引數是後執行的函式,第一個是先執行的函式,因為輸入輸出都是數字型別,所以泛型都指定為Integer型別,通過after.compose(before);將兩個函式串聯起來然後執行組合後的Funtion方法apply(i)。當呼叫compute1(5,i -> i 2,i -> i i)時,先平方再乘以2所以結果是50。而compute2方法對兩個Function的呼叫正好相反,所以結果是100。

public class FunctionTest {
    public static void main(String[] args) {
        FunctionTest functionTest = new FunctionTest();
        System.out.println(functionTest.compute1(5,i -> i * 2,i -> i * i));//50
        System.out.println(functionTest.compute2(5,i -> i * 2,i -> i * i));//100
    }

    public int compute1(int i, Function<Integer,Integer> after,Function<Integer,Integer> before){
        return after.compose(before).apply(i);
    }

    public int compute2(int i, Function<Integer,Integer> before,Function<Integer,Integer> after){
        return before.andThen(after).apply(i);
    }
}

 

BiFunction介面
另一個很常用的函式式介面BiFunction

@FunctionalInterface
public interface BiFunction<T, U, R> {

    R apply(T t, U u);

    default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t, U u) -> after.apply(apply(t, u));
    }
}

BiFunction介面實際上就是可以有兩個引數的Function,同樣前兩個泛型代表著入參的型別,第三個代表結果型別。

看下compute方法,前兩個引數是待計算資料,第三個是一個BiFunction,因為入參和結果都是陣列所以三個泛型都定義為Integer。最後一個引數是Function。計算邏輯是先執行BiFunction然後將結果傳給Funciton在計算最後返回結果,所以使用了andThen方法。我們想一下,BiFunction的andThen方法為什麼接收的是Function型別的引數而不是BiFunction,答案很簡單,因為BiFunction的apply方法接收兩個引數,但是任何一個方法不可能有兩個返回值,所以也沒辦法放在BiFunction前面執行,這也是為什麼BiFunction沒有compose方法的原因。

public class BiFunctionTest {
    public static void main(String[] args) {
        BiFunctionTest2 biFunctionTest2 = new BiFunctionTest2();
        System.out.println(biFunctionTest2.compute(4,5,(a,b) -> a * b,a -> a * 2));
    }

    public int compute(int a, int b, BiFunction<Integer,Integer,Integer> biFunction,
                       Function<Integer,Integer> function){
        return biFunction.andThen(function).apply(a,b);
    }
}

通過BiFunction這個函式,可以將一個型別轉換為另一個型別,比如下面的例子:

//定義一個function 輸入是String型別,輸出是 EventInfo 型別,  EventInfo是一個類。           
Function<String, EventInfo> times2 = fun -> { EventInfo a = new EventInfo(); a.setName(fun); return a;};

String[] testintStrings={"1","2","3","4"}; 

//將String的Array轉換成map,呼叫times2函式進行轉換
Map<String,EventInfo> eventmap1=Stream.of(testintStrings).collect(Collectors.toMap(inputvalue->inputvalue, inputvalue->times2.apply(inputvalue)));

如果Collectors.toMap的轉換過程很簡單,比如輸入和輸出型別相同,則不需要另外定義Function,例如:

Map<String,String> eventmap2=Stream.of(testStrings).collect(Collectors.toMap(inputvalue->inputvalue, inputvalue->(inputvalue+”a”)));


Supplier介面
預設抽象方法get不接收引數,有返回值

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

類似工廠模式
這裡使用構造方法引用的方式建立Supplier例項,通過get直接返回String物件

public class SupplierTest {
    public static void main(String[] args) {
        Supplier<String> supplier = String::new;
        String s = supplier.get();
    }
}

 

Predicate函式式介面

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
}

接收一個引數,返回布林型別,使用方式
定義了一個接收一個引數返回布林值的lambda表示式,賦值給predicate,就可以直接對傳入引數進行校驗。

這段程式的邏輯是找到集合裡大於5的資料,列印到控制檯。

public class PredicateTest {
    public static void main(String[] args) {
        Predicate<String> predicate = s -> s.length() > 5;
        System.out.println(predicate.test("hello"));
    }
}

我們具體分析一下conditionFilter方法,第一個引數是待遍歷的集合,第二個引數是Predicate型別的例項,還記得Predicate介面中的抽象方法定義嗎,接收一個引數返回布林型別。list

public class PredicateTest {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        PredicateTest predicateTest = new PredicateTest();
        List<Integer> result = predicateTest.conditionFilter(list, integer -> integer > 5);
        result.forEach(System.out::println);
    }

    public List<Integer> conditionFilter(List<Integer> list, Predicate<Integer> predicate){
        return list.stream().filter(predicate).collect(Collectors.toList());
    }
}

呼叫conditionFilter方法,方法引用例項化一個Consumer物件,把結果輸出到控制檯。

List<Integer> result = predicateTest.conditionFilter(list, integer -> integer > 5).forEach(System.out::println);

Predicate在stream api中進行一些判斷的時候非常常用。

public class PredicateTest {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        PredicateTest predicateTest = new PredicateTest();
        //輸出大於5的數字
        List<Integer> result = predicateTest.conditionFilter(list, integer -> integer > 5);
        result.forEach(System.out::println);
        System.out.println("-------");
        //輸出大於等於5的數字
        result = predicateTest.conditionFilter(list, integer -> integer >= 5);
        result.forEach(System.out::println);
        System.out.println("-------");
        //輸出小於8的數字
        result = predicateTest.conditionFilter(list, integer -> integer < 8);
        result.forEach(System.out::println);
        System.out.println("-------");
        //輸出所有數字
        result = predicateTest.conditionFilter(list, integer -> true);
        result.forEach(System.out::println);
        System.out.println("-------");
    }
    //高度抽象的方法定義,複用性高
    public List<Integer> conditionFilter(List<Integer> list, Predicate<Integer> predicate){
        return list.stream().filter(predicate).collect(Collectors.toList());
    }
}

stream()會將當前list作為源建立一個Stream物件,collect(Collectors.toList())是將最終的結果封裝在ArrayList中
filter方法接收一個Predicate型別引數用於對目標集合進行過濾。裡面並沒有任何具體的邏輯,提供了一種更高層次的抽象化,我們可以把要處理的資料和具體的邏輯通過引數傳遞給conditionFilter即可

Predicate還提供了另外三個預設方法和一個靜態方法

and方法接收一個Predicate型別,也就是將傳入的條件和當前條件以並且的關係過濾資料。or方法同樣接收一個Predicate型別,將傳入的條件和當前的條件以或者的關係過濾資料。negate就是將當前條件取反。

public List<Integer> conditionFilterNegate(List<Integer> list, Predicate<Integer> predicate){
    return list.stream().filter(predicate.negate()).collect(Collectors.toList());
}

public List<Integer> conditionFilterAnd(List<Integer> list, Predicate<Integer> predicate,Predicate<Integer> predicate2){
    return list.stream().filter(predicate.and(predicate2)).collect(Collectors.toList());
}

public List<Integer> conditionFilterOr(List<Integer> list, Predicate<Integer> predicate,Predicate<Integer> predicate2){
    return list.stream().filter(predicate.or(predicate2)).collect(Collectors.toList());
}

大於5並且是偶數

result = predicateTest.conditionFilterAnd(list, integer -> integer > 5, integer1 -> integer1 % 2 == 0);
result.forEach(System.out::println);//6 8 10
System.out.println("-------");

大於5或者是偶數

result = predicateTest.conditionFilterOr(list, integer -> integer > 5, integer1 -> integer1 % 2 == 0);
result.forEach(System.out::println);//2 4 6 8 9 10
System.out.println("-------");

條件取反

result = predicateTest.conditionFilterNegate(list,integer2 -> integer2 > 5);
result.forEach(System.out::println);// 1 2 3 4 5
System.out.println("-------");

isEqual方法返回型別也是Predicate,也就是說通過isEqual方法得到的也是一個用來進行條件判斷的函式式介面例項。而返回的這個函式式介面例項是通過傳入的targetRef的equals方法進行判斷的。我們看一下具體用法

System.out.println(Predicate.isEqual("test").test("test"));//true

 

Optional類

Optional並不是一系列函式式介面,它是一個class,主要作用就是解決Java中的NPE(NullPointerException)。空指標異常在程式執行中出現的頻率非常大,我們經常遇到需要在邏輯處理前判斷一個物件是否為null的情況。
Optional類如何避免空指標問題,首先,ofNullable方法接收一個可能為null的引數,將引數的值賦給Optional類中的成員變數value,ifPresent方法接收一個Consumer型別函式式介面例項,再將成員變數value交給Consumer的accept方法處理前,會校驗成員變數value是否為null,如果value是null,則什麼也不會執行,避免了空指標問題。

如果傳入的內容是空,則什麼也不會執行,也不會有空指標異常

public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}

String str = "hello";
Optional<String> optional = Optional.ofNullable(str);
optional.ifPresent(s -> System.out.println(s));//value為hello,正常輸出

如果為空時想返回一個預設值

orElseGet方法接收一個Supplier,還記得前面介紹的Supplier麼,不接受引數通過get方法直接返回結果,類似工廠模式,上面程式碼就是針對傳入的str變數,如果不為null那正常輸出,如果為null,那返回一個預設值"welcome"

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

String str = null;
Optional<String> optional = Optional.ofNullable(str);
System.out.println(optional.orElseGet(() -> "welcome"));

 

方法引用
(方法引用和lambda一樣是Java8新語言特性)

方法引用是lambda表示式的一種特殊形式,如果正好有某個方法滿足一個lambda表示式的形式,那就可以將這個lambda表示式用方法引用的方式表示,但是如果這個lambda表示式的比較複雜就不能用方法引用進行替換。實際上方法引用是lambda表示式的一種語法糖。
方法引用共分為四類:
1.類名::靜態方法名
2.物件::例項方法名
3.類名::例項方法名 
4.類名::new

前兩種方式類似,等同於把lambda表示式的引數直接當成instanceMethod|staticMethod的引數來呼叫。比如System.out::println等同於x->System.out.println(x);Math::max等同於(x, y)->Math.max(x,y)。

類名::靜態方法名

Student類有兩個屬性name和score並提供了初始化name和score的構造方法,並且在最下方提供了兩個靜態方法分別按score和name進行比較先後順序。
接下來的需求是,按著分數由小到大排列並輸出

sort方法接收一個Comparator函式式介面,介面中唯一的抽象方法compare接收兩個引數返回一個int型別值,下方是Comparator介面定義

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}
public class Student {
    private String name;
    private int score;

    public Student(){

    }

    public Student(String name,int score){
        this.name = name;
        this.score = score;
    }
    public static int compareStudentByScore(Student student1,Student student2){
        return student1.getScore() - student2.getScore();
    }
}

Student student1 = new Student("zhangsan",60);
Student student2 = new Student("lisi",70);
Student student3 = new Student("wangwu",80);
Student student4 = new Student("zhaoliu",90);
List<Student> students = Arrays.asList(student1,student2,student3,student4);

students.sort((o1, o2) -> o1.getScore() - o2.getScore());
students.forEach(student -> System.out.println(student.getScore()));

compareStudentByScore靜態方法,同樣是接收兩個引數返回一個int型別值,而且是對Student物件的分數進行比較,所以我們這裡就可以 使用類名::靜態方法名 方法引用替換lambda表示式

students.sort(Student::compareStudentByScore);
students.forEach(student -> System.out.println(student.getScore()));

我們再自定義一個用於比較Student元素的類

public class StudentComparator {
    public int compareStudentByScore(Student student1,Student student2){
        return student2.getScore() - student1.getScore();
    }
}

StudentComparator中定義了一個非靜態的,例項方法compareStudentByScore,同樣該方法的定義滿足Comparator介面的compare方法定義,所以這裡可以直接使用 物件::例項方法名 的方式使用方法引用來替換lambda表示式。

StudentComparator studentComparator = new StudentComparator();
students.sort(studentComparator::compareStudentByScore);
students.forEach(student -> System.out.println(student.getScore()));


第三種,類名::例項方法名 。這種方法引用的方式較之前兩種稍微有一些不好理解,因為無論是通過類名呼叫靜態方法還是通過物件呼叫例項方法這都是符合Java的語法,使用起來也比較清晰明瞭。

等同於把lambda表示式的第一個引數當成instanceMethod的目標物件,其他剩餘引數當成該方法的引數。比如String::toLowerCase等同於x->x.toLowerCase()。
可以這麼理解,前兩種是將傳入物件當引數執行方法,第三種是呼叫傳入物件的方法。

Student類中靜態方法的定義改進

作為一個工具正常使用,但是有沒有覺得其在設計上是不合適的或者是錯誤的。這樣的方法定義放在任何一個類中都可以正常使用,而不只是從屬於Student這個類,那如果要定義一個只能從屬於Student類的比較方法下面這個例項方法更合適一些

public int compareByScore(Student student){
    return this.getScore() - student.getScore();
}

接收一個Student物件和當前呼叫該方法的Student物件的分數進行比較即可。現在我們就可以使用 類名::例項方法名 這種方式的方法引用替換lambda表示式了。

sort方法接收的lambda表示式不應該是兩個引數麼,為什麼這個例項方法只有一個引數也滿足了lambda表示式的定義(想想這個方法是誰來呼叫的)。這就是 類名::例項方法名 這種方法引用的特殊之處:當使用 類名::例項方法名 方法引用時,一定是lambda表示式所接收的第一個引數來呼叫例項方法,如果lambda表示式接收多個引數,其餘的引數作為方法的引數傳遞進去。

students.sort(Student::compareByScore);
students.forEach(student -> System.out.println(student.getScore()));

最初的lambda表示式是這樣的

那使用 類名::例項方法名 方法引用時,一定是o1來呼叫了compareByScore例項方法,並將o2作為引數傳遞進來進行比較。是不是就符合了compareByScore的方法定義。

students.sort((o1, o2) -> o1.getScore() - o2.getScore());

將列表中的字串轉換為全小寫

List<String> proNames = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
List<String> lowercaseNames3 = proNames.stream().map(String::toLowerCase).collect(Collectors.toList());
等價於
List<String> lowercaseNames1 = proNames.stream().map(name -> {return name.toLowerCase();}).collect(Collectors.toList());

 

第四種是構造器引用,構造器引用語法如下:類名::new,把lambda表示式的引數當成ClassName構造器的引數 。

例如BigDecimal::new等同於x->new BigDecimal(x)。

和前面類似只要符合lambda表示式的定義即可。

Supplier函式式介面的get方法,不接收引數有返回值,正好符合無參構造方法的定義

@FunctionalInterface
public interface Supplier<T> {

T get();
}

Supplier<Student> supplier = Student::new;

上面就是使用了Student類構造方法引用建立了supplier例項,以後通過supplier.get()就可以獲取一個Student型別的物件,前提是Student類中存在無參構造方法。
我們給Test1新添加了一個構造方法,該構造方法接收一個引數,不返回值,編譯通過。(僅為展示構造方法引用的用法)

public class Test1 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        //構造方法引用
        list.forEach(Test1::new);
    }
    
    Test1(Integer i){
        System.out.println(i);
    }
}

 

Stream語法

Java8中的Stream與lambda表示式可以說是相伴相生的,通過Stream我們可以更好的更為流暢更為語義化的操作集合。Stream api都位於java.util.stream包中。其中就包含了最核心的Stream介面,一個Stream例項可以序列或者並行操作一組元素序列。

Java8中,所有的流操作會被組合到一個 stream pipeline中,這點類似linux中的pipeline概念,將多個簡單操作連線在一起組成一個功能強大的操作。一個 stream pileline首先會有一個數據源,這個資料來源可能是陣列、集合、生成器函式或是IO通道,流操作過程中並不會修改源中的資料;然後還有零個或多箇中間操作,每個中間操作會將接收到的流轉換成另一個流(比如filter);最後還有一個終止操作,會生成一個最終結果(比如sum)。流是一種惰性操作,所有對源資料的計算只在終止操作被初始化的時候才會執行。

總結一下流操作由3部分組成
1.源
2.零個或多箇中間操作
3.終止操作 (到這一步才會執行整個stream pipeline計算)

兩句話理解Stream:

1.Stream是元素的集合,這點讓Stream看起來用些類似Iterator;
2.可以支援順序和並行的對原Stream進行匯聚的操作;

public interface Stream<T> extends BaseStream<T, Stream<T>> {
    
    Stream<T> filter(Predicate<? super T> predicate);
    
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    
    void forEach(Consumer<? super T> action);
    .
    .省略
    .
}

大家可以把Stream當成一個裝飾後的Iterator。原始版本的Iterator,使用者只能逐個遍歷元素並對其執行某些操作;包裝後的Stream,使用者只要給出需要對其包含的元素執行什麼操作,比如“過濾掉長度大於10的字串”、“獲取每個字串的首字母”等,具體這些操作如何應用到每個元素上,就給Stream就好了!原先是人告訴計算機一步一步怎麼做,現在是告訴計算機做什麼,計算機自己決定怎麼做。當然這個“怎麼做”還是比較弱的。

import com.google.common.collect.Lists;

//Lists是Guava中的一個工具類
List<Integer> nums = Lists.newArrayList(1,null,3,4,null,6);
nums.stream().filter(num -> num != null).count();

上面這段程式碼是獲取一個List中,元素不為null的個數。這段程式碼雖然很簡短,但是卻是一個很好的入門級別的例子來體現如何使用Stream,正所謂“麻雀雖小五臟俱全”。我們現在開始深入解刨這個例子,完成以後你可能可以基本掌握Stream的用法!

可以很清楚的看見:原本一條語句被三種顏色的框分割成了三個部分。紅色框中的語句是一個Stream的生命開始的地方,負責建立一個Stream例項;綠色框中的語句是賦予Stream靈魂的地方,把一個Stream轉換成另外一個Stream,紅框的語句生成的是一個包含所有nums變數的Stream,進過綠框的filter方法以後,重新生成了一個過濾掉原nums列表所有null以後的Stream;藍色框中的語句是豐收的地方,把Stream的裡面包含的內容按照某種演算法來匯聚成一個值,例子中是獲取Stream中包含的元素個數。如果這樣解析以後,還不理解,那就只能動用“核武器”–圖形化,一圖抵千言!

使用Stream的基本步驟:

1.建立Stream;
2.轉換Stream,每次轉換原有Stream物件不改變,返回一個新的Stream物件(**可以有多次轉換**);
3.對Stream進行聚合(Reduce)操作,獲取想要的結果;

存在一個字串集合,我們想把所有長度大於5的字串轉換成大寫輸出到控制檯,之前我們可能會直接這麼做

List<String> list = Arrays.asList("hello","world","helloworld");
for (int i = 0; i < list.size(); i++) {
    if(list.get(i).length() > 5){
      System.out.println(list.get(i).toUpperCase());
   }
}

換成使用stream api

List<String> list = Arrays.asList("hello","world","helloworld");
list.stream().filter(s -> s.length() > 5).map(s -> s.toUpperCase()).forEach(System.out::println);

1行程式碼直接搞定,而且這種鏈式程式設計風格從語義上看邏輯很清晰。
stream方法先構造了一個該集合的Stream物件,filter方法取出長度大於5的字串,map方法將所有字串轉大寫,forEach輸出到控制檯。

filter方法,接收一個Predicate函式式介面型別作為引數,並返回一個Stream物件,從上一篇我們知道可以由一個接收一個引數返回布林型別的lambda表示式來建立Predicate函式式介面例項,所以看到filter接收的引數是s -> s.length() > 5

map方法,接收Function函式式介面型別,接收一個引數,有返回值s -> s.toUpperCase() 正是做了這件事情

forEach方法,接收Consumer函式式介面型別,接收一個引數,不返回值 這裡使用方法引用的其中一種形式System.out::println來建立了Consumer例項。

所以通過上面的例子可以看出函數語言程式設計和stream api結合的非常緊密。大家應該也注意到了在介紹每個方法時,我們提到了有中間操作和終止操作,終止操作意味著我們需要一個結果了,當程式遇到終止操作時才會真正執行。中間操作是指在終止操作之前所有的方法,這些方法以方法鏈的形式組織在一起處理一些列邏輯,如果只有中間操作而沒有終止操作的話即使執行程式,程式碼也不會執行的

實際上map方法中可以使用另一種方法引用的形式來處理,類方法引用。語法:類名::方法名

List<String> list = Arrays.asList("hello","world","helloworld");
list.stream().filter(s -> s.length() > 5).map(String::toUpperCase).forEach(System.out::println);

map方法接收一個Function函式式介面的實現,那就肯定需要一個輸入並且有一個輸出,但是我們看下toUpperCase方法的定義

public String toUpperCase() {
    return toUpperCase(Locale.getDefault());
}

有返回值,但是沒有入參,乍一看也不符合Function介面中apply方法的定義啊。這也是類方法引用的特點,雖然toUpperCase沒有明確的入參,因為此時toUpperCase的輸入是呼叫它的那個物件,編譯器會把呼叫toUpperCase方法的那個物件當做引數,也就是lambda表示式s -> s.toUpperCase()中的s引數。所以也滿足一個輸入一個輸出的定義。

 

最常用的建立流的幾種方式:

//第一種 通過Stream介面的of靜態方法建立一個流
Stream<String> stream = Stream.of("hello", "world", "helloworld");
//第二種 通過Arrays類的stream方法,實際上第一種of方法底層也是呼叫的Arrays.stream(values);
String[] array = new String[]{"hello","world","helloworld"};
Stream<String> stream3 = Arrays.stream(array);
//第三種 通過集合的stream方法,該方法是Collection介面的預設方法,所有集合都繼承了該方法
Stream<String> stream2 = Arrays.asList("hello","world","helloworld").stream();
//第四種
通過Stream介面的靜態工廠方法(注意:Java8裡介面可以帶靜態方法);
//第五種
通過Collection介面的預設方法(預設方法:Default method,也是Java8中的一個新特性,就是介面中的一個帶有實現的方法)–stream(),把一個Collection物件轉換成Stream

1. of方法:有兩個overload方法,一個接受變長引數,一個介面單一值

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

2. generator方法:生成一個無限長度的Stream,其元素的生成是通過給定的Supplier(這個介面可以看成一個物件的工廠,每次呼叫返回一個給定型別的物件)

Stream.generate(new Supplier<Double>() {
    @Override
    public Double get() {
         return Math.random();
    }
});
Stream.generate(() -> Math.random());
Stream.generate(Math::random);

三條語句的作用都是一樣的,只是使用了lambda表示式和方法引用的語法來簡化程式碼。每條語句其實都是生成一個無限長度的Stream,其中值是隨機的。這個無限長度Stream是懶載入,一般這種無限長度的Stream都會配合Stream的limit()方法來用。
3. iterate方法:也是生成無限長度的Stream,和generator不同的是,其元素的生成是重複對給定的種子值(seed)呼叫使用者指定函式來生成的。其中包含的元素可以認為是:seed,f(seed),f(f(seed))無限迴圈

Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);

這段程式碼就是先獲取一個無限長度的正整數集合的Stream,然後取出前10個列印。千萬記住使用limit方法,不然會無限列印下去。

Collection介面有一個stream方法,所以其所有子類都都可以獲取對應的Stream物件。

public interface Collection<E> extends Iterable<E> {
      //其他方法省略
     default Stream<E> stream() {
          return StreamSupport.stream(spliterator(), false);
     }
}

2. filter: 對於Stream中包含的元素使用給定的過濾函式進行過濾操作,新生成的Stream只包含符合條件的元素;

同時獲取最大 最小 平均值等資訊

List<Integer> list1 = Arrays.asList(1, 3, 5, 7, 9, 11);
IntSummaryStatistics statistics = list1.stream().filter(integer -> integer > 2).mapToInt(i -> i * 2).skip(2).limit(2).summaryStatistics();
System.out.println(statistics.getMax());//18
System.out.println(statistics.getMin());//14
System.out.println(statistics.getAverage());//16

將list1中的資料取出大於2的,每個數進行平方計算,skip(2)忽略前兩個,limit(2)再取出前兩個,summaryStatistics對取出的這兩個數計算統計資料。mapToInt接收一個ToIntFunction型別,也就是接收一個引數返回值是int型別。

3. map: 對於Stream中包含的元素使用給定的轉換函式進行轉換操作,新生成的Stream只包含轉換生成的元素。這個方法有三個對於原始型別的變種方法,分別是:mapToInt,mapToLong和mapToDouble。這三個方法也比較好理解,比如mapToInt就是把原始Stream轉換成一個新的Stream,這個新生成的Stream中的元素都是int型別。之所以會有這樣三個變種方法,可以免除自動裝箱/拆箱的額外消耗;

stream().map(),您可以將物件轉換為其他物件。

Stream的map方法,map方法接收一個Function函式式介面例項,這裡的map和Hadoop中的map概念完全一致,對每個元素進行對映處理。然後傳入lambda表示式將每個元素轉換大寫,通過collect方法將結果收集到ArrayList中。

<R> Stream<R> map(Function<? super T, ? extends R> mapper);//map函式定義

4. flatMap:和map類似,不同的是其每個元素轉換得到的是Stream物件,會把子Stream中的元素壓縮到父集合中;

map方法是將一個容器裡的元素對映到另一個容器中。

flatMap方法,可以將多個容器的元素全部對映到一個容器中,即為扁平的map。

Stream<List<Integer>> inputStream = Stream.of(
 Arrays.asList(1),
 Arrays.asList(2, 3),
 Arrays.asList(4, 5, 6)
 );
Stream<Integer> outputStream = inputStream.
flatMap((childList) -> childList.stream());

flatMap 把 input Stream 中的層級結構扁平化,就是將最底層元素抽出來放到一起,最終 output 的新 Stream 裡面已經沒有 List 了,都是直接的數字。

求每個元素平方的例子

Stream<List<Integer>> listStream =
                Stream.of(Arrays.asList(1), Arrays.asList(2, 3), Arrays.asList(4, 5, 6));
List<Integer> collect1 = listStream.flatMap(theList -> theList.stream()).
                map(integer -> integer * integer).collect(Collectors.toList());

首先我們建立了一個Stream物件,Stream中的每個元素都是容器List<Integer>型別,並使用三個容器list初始化這個Stream物件,然後使用flatMap方法將每個容器中的元素對映到一個容器中,這時flatMap接收的引數Funciton的泛型T就是List<Integer>型別,返回型別就是T對應的Stream。最後再對這個容器使用map方法求出買個元素的平方。

peek: 生成一個包含原Stream的所有元素的新Stream,同時會提供一個消費函式(Consumer例項),新Stream每個元素被消費的時候都會執行給定的消費函式;

limit: 對一個Stream進行截斷操作,獲取其前N個元素,如果原Stream中包含的元素個數小於N,那就獲取其所有的元素;

skip: 返回一個丟棄原Stream的前N個元素後剩下元素組成的新Stream,如果原Stream中包含的元素個數小於N,那麼返回空Stream;

List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
System.out.println(“sum is:”+nums.stream().filter(num -> num != null).distinct().mapToInt(num -> num * 2).peek(System.out::println).skip(2).limit(4).sum());


2
4
6
8
10
12
sum is:36

這段程式碼演示了上面介紹的所有轉換方法(除了flatMap),簡單解釋一下這段程式碼的含義:給定一個Integer型別的List,獲取其對應的Stream物件,然後進行過濾掉null,再去重,再每個元素乘以2,再每個元素被消費的時候列印自身,在跳過前兩個元素,最後去前四個元素進行加和運算(解釋一大堆,很像廢話,因為基本看了方法名就知道要做什麼了。這個就是宣告式程式設計的一大好處!)。

在對於一個Stream進行多次轉換操作,每次都對Stream的每個元素進行轉換,而且是執行多次,這樣時間複雜度就是一個for迴圈裡把所有操作都做掉的N(轉換的次數)倍啊。其實不是這樣的,轉換操作都是lazy的,多個轉換操作只會在匯聚操作的時候融合起來,一次迴圈完成。我們可以這樣簡單的理解,Stream裡有個操作函式的集合,每次轉換操作就是把轉換函式放入這個集合中,在匯聚操作的時候迴圈Stream對應的集合,然後對每個元素執行所有的函式。

 

Stream中的一個靜態方法,generate方法

generate接收一個Supplier

public static<T> Stream<T> generate(Supplier<T> s) {
    Objects.requireNonNull(s);
    return StreamSupport.stream(
            new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
}

適合生成連續不斷的流或者一個全部是隨機數的流

Stream.generate(UUID.randomUUID()::toString).findFirst().ifPresent(System.out::println);

使用UUID.randomUUID()::toString 方法引用的方式建立了Supplier,然後取出第一個元素,這裡的findFirst返回的是 Optional,因為流中有可能沒有元素,為了避免空指標,在使用前 ifPresent 進行是否存在的判斷。

 

另一個靜態方法,iterate

public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {
    Objects.requireNonNull(f);
    final Iterator<T> iterator = new Iterator<T>() {
        @SuppressWarnings("unchecked")
        T t = (T) Streams.NONE;

        @Override
        public boolean hasNext() {
            return true;
        }

        @Override
        public T next() {
            return t = (t == Streams.NONE) ? seed : f.apply(t);
        }
    };
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
            iterator,
            Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
}

iterate方法有兩個引數,第一個是seed也可以稱作種子,第二個是一個UnaryOperator,UnaryOperator實際上是Function的一個子介面,和Funciton區別就是引數和返回型別都是同一種類型

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

}

iterate方法第一次生成的元素是UnaryOperator對seed執行apply後的返回值,之後所有生成的元素都是UnaryOperator對上一個apply的返回值再執行apply,不斷迴圈。
f(f(f(f(f(f(n))))))......

//從1開始,每個元素比前一個元素大2,最多生成10個元素
Stream.iterate(1,item -> item + 2).limit(10).forEach(System.out::println);

我們在使用stream api時也要注意一些陷阱,比如下面這個例子

//Stream陷阱 distinct()會一直等待產生的結果去重,將distinct()和limit(6)調換位置,先限制結果集再去重就可以了
IntStream.iterate(0,i -> (i + 1) % 2).distinct().limit(6).forEach(System.out::println);

如果distinct()一直等待那程式會一直執行不斷生成資料,所以需要先限制結果集再去進行去重操作就可以了。

 

匯聚操作(也稱為摺疊)接受一個元素序列為輸入,反覆使用某個合併操作,把序列中的元素合併成一個彙總的結果。比如查詢一個數字列表的總和或者最大值,或者把這些數字累積成一個List物件。Stream介面有一些通用的匯聚操作,比如reduce()和collect();也有一些特定用途的匯聚操作,比如sum(),max()和count()。注意:sum方法不是所有的Stream物件都有的,只有IntStream、LongStream和DoubleStream是例項才有。

下面會分兩部分來介紹匯聚操作:

可變匯聚:把輸入的元素們累積到一個可變的容器中,比如Collection或者StringBuilder;
其他匯聚:除去可變匯聚剩下的,一般都不是通過反覆修改某個可變物件,而是通過把前一次的匯聚結果當成下一次的入參,反覆如此。比如reduce,count,allMatch;

可變匯聚對應的只有一個方法:collect,正如其名字顯示的,它可以把Stream中的要有元素收集到一個結果容器中(比如Collection)。先看一下最通用的collect方法的定義(還有其他override方法):

<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);

先來看看這三個引數的含義:Supplier supplier是一個工廠函式,用來生成一個新的容器;BiConsumer accumulator也是一個函式,用來把Stream中的元素新增到結果容器中;BiConsumer combiner還是一個函式,用來把中間狀態的多個結果容器合併成為一個(併發的時候會用到)。看暈了?來段程式碼!

List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
collect(() -> new ArrayList<Integer>(),
(list, item) -> list.add(item),
(list1, list2) -> list1.addAll(list2));

上面這段程式碼就是對一個元素是Integer型別的List,先過濾掉全部的null,然後把剩下的元素收集到一個新的List中。進一步看一下collect方法的三個引數,都是lambda形式的函式。

第一個函式生成一個新的ArrayList例項;
第二個函式接受兩個引數,第一個是前面生成的ArrayList物件,二個是stream中包含的元素,函式體就是把stream中的元素加入ArrayList物件中。第二個函式被反覆呼叫直到原stream的元素被消費完畢;
第三個函式也是接受兩個引數,這兩個都是ArrayList型別的,函式體就是把第二個ArrayList全部加入到第一個中;
但是上面的collect方法呼叫也有點太複雜了,沒關係!我們來看一下collect方法另外一個override的版本,其依賴[Collector]

<R, A> R collect(Collector<? super T, A, R> collector);
這樣清爽多了!Java8還給我們提供了Collector的工具類–[Collectors],其中已經定義了一些靜態工廠方法,比如:Collectors.toCollection()收集到Collection中, Collectors.toList()收集到List中和Collectors.toSet()收集到Set中。這樣的靜態方法還有很多,這裡就不一一介紹了,大家可以直接去看JavaDoc。下面看看使用Collectors對於程式碼的簡化:

List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
collect(Collectors.toList());

其他匯聚

– reduce方法:reduce方法非常的通用,後面介紹的count,sum等都可以使用其實現。reduce方法有三個override的方法,本文介紹兩個最常用的。先來看reduce方法的第一種形式,其方法定義如下:

Optional<T> reduce(BinaryOperator<T> accumulator);
接受一個BinaryOperator型別的引數,在使用的時候我們可以用lambda表示式來。

List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().reduce((sum, item) -&gt; sum + item).get());
可以看到reduce方法接受一個函式,這個函式有兩個引數,第一個引數是上次函式執行的返回值(也稱為中間結果),第二個引數是stream中的元素,這個函式把這兩個值相加,得到的和會被賦值給下次執行這個函式的第一個引數。要注意的是:**第一次執行的時候第一個引數的值是Stream的第一個元素,第二個引數是Stream的第二個元素**。這個方法返回值型別是Optional,這是Java8防止出現NPE的一種可行方法,這裡就簡單的認為是一個容器,其中可能會包含0個或者1個物件。
這個過程視覺化的結果如圖:

reduce方法還有一個很常用的變種:

T reduce(T identity, BinaryOperator<T> accumulator);
這個定義上上面已經介紹過的基本一致,不同的是:它允許使用者提供一個迴圈計算的初始值,如果Stream為空,就直接返回該值。而且這個方法不會返回Optional,因為其不會出現null值。下面直接給出例子,就不再做說明了。

List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().reduce(0, (sum, item) -> sum + item));

– count方法:獲取Stream中元素的個數。比較簡單,這裡就直接給出例子,不做解釋了。

List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().count());

– 搜尋相關
– allMatch:是不是Stream中的所有元素都滿足給定的匹配條件
– anyMatch:Stream中是否存在任何一個元素滿足匹配條件
– findFirst: 返回Stream中的第一個元素,如果Stream為空,返回空Optional
– noneMatch:是不是Stream中的所有元素都不滿足給定的匹配條件
– max和min:使用給定的比較器(Operator),返回Stream中的最大|最小值
下面給出allMatch和max的例子,剩下的方法讀者當成練習。

List<Integer&gt; ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println(ints.stream().allMatch(item -> item < 100));
ints.stream().max((o1, o2) -&gt; o1.compareTo(o2)).ifPresent(System.out::println);

給出一個String型別的陣列,求其中所有不重複素數的和

 List<String> l = Arrays.asList(numbers);
        int sum = l.stream()
            .map(e -> new Integer(e))
            .filter(e -> Primes.isPrime(e))
            .distinct()
            .reduce(0, (x,y) -> x+y); // equivalent to .sum()
        System.out.println("distinctPrimarySum result is: " + sum);

reduce方法用來產生單一的一個最終結果,根據一定的規則將Stream中的元素進行計算後返回一個唯一的值。 
流有很多預定義的reduce操作,如sum(),max(),min()等。

統計年齡在25-35歲的男女人數、比例

 Map<Integer, Integer> result = persons.parallelStream().filter(p -> p.getAge()>=25 && p.getAge()<=35).
            collect(
                Collectors.groupingBy(p->p.getSex(), Collectors.summingInt(p->1))
        );
        System.out.print("boysAndGirls result is " + result);
        System.out.println(", ratio (male : female) is " + (float)result.get(Person.MALE)/result.get(Person.FEMALE));

一個引數的Reduce

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5, 6);
/**
 * 也可以寫成Lambda語法:
 * Integer sum = s.reduce((a, b) -> a + b).get();
 */
Integer sum = s.reduce(new BinaryOperator<Integer>() {
    @Override
    public Integer apply(Integer integer, Integer integer2) {
        return integer + integer2;
    }
}).get();


/**
 * 求最大值,也可以寫成Lambda語法:
 * Integer max = s.reduce((a, b) -> a >= b ? a : b).get();
 */
Integer max = s.reduce(new BinaryOperator<Integer>() {
    @Override
    public Integer apply(Integer integer, Integer integer2) {
        return integer >= integer2 ? integer : integer2;
    }
}).get();

兩個引數的Reduce
相對於一個引數的方法來說,它多了一個T型別的引數;實際上就相當於需要計算的值在Stream的基礎上多了一個初始化的值。 

Stream<String> s = Stream.of("test", "t1", "t2", "teeeee", "aaaa", "taaa");
/**
 * 以下結果將會是: [value]testt1t2teeeeeaaaataaa
 * 也可以使用Lambda語法:
 * System.out.println(s.reduce("[value]", (s1, s2) -> s1.concat(s2)));
 */
System.out.println(s.reduce("[value]", new BinaryOperator<String>() {
    @Override
    public String apply(String s, String s2) {
        return s.concat(s2);
    }
})); 

三個引數的Reduce
分析下它的三個引數:
identity: 一個初始化的值;這個初始化的值其型別是泛型U,與Reduce方法返回的型別一致;注意此時Stream中元素的型別是T,與U可以不一樣也可以一樣,這樣的話操作空間就大了;不管Stream中儲存的元素是什麼型別,U都可以是任何型別,如U可以是一些基本資料型別的包裝型別Integer、Long等;或者是String,又或者是一些集合型別ArrayList等;後面會說到這些用法。
accumulator: 其型別是BiFunction,輸入是U與T兩個型別的資料,而返回的是U型別;也就是說返回的型別與輸入的第一個引數型別是一樣的,而輸入的第二個引數型別與Stream中元素型別是一樣的。
combiner: 其型別是BinaryOperator,支援的是對U型別的物件進行操作;
第三個引數combiner主要是使用在平行計算的場景下;如果Stream是非並行時,第三個引數實際上是不生效的。 
因此針對這個方法的分析需要分並行與非並行兩個場景。

非並行

 /**
 * 以下reduce生成的List將會是[aa, ab, c, ad]
 * Lambda語法:
 *  System.out.println(s1.reduce(new ArrayList<String>(), (r, t) -> {r.add(t); return r; }, (r1, r2) -> r1));
 */
Stream<String> s1 = Stream.of("aa", "ab", "c", "ad");
System.out.println(s1.reduce(new ArrayList<String>(),
        new BiFunction<ArrayList<String>, String, ArrayList<String>>() {
            @Override
            public ArrayList<String> apply(ArrayList<String> u, String s) {
                u.add(s);
                return u;
            }
        }, new BinaryOperator<ArrayList<String>>() {
            @Override
            public ArrayList<String> apply(ArrayList<String> strings, ArrayList<String> strings2) {
                return strings;
            }
        }));

 /**
 * 模擬Filter查詢其中含有字母a的所有元素,列印結果將是aa ab ad
 * lambda語法:
 * s1.reduce(new ArrayList<String>(), (r, t) -> {if (predicate.test(t)) r.add(t);  return r; },
        (r1, r2) -> r1).stream().forEach(System.out::println);
 */
Stream<String> s1 = Stream.of("aa", "ab", "c", "ad");
Predicate<String> predicate = t -> t.contains("a");
s1.reduce(new ArrayList<String>(), new BiFunction<ArrayList<String>, String, ArrayList<String>>() {
            @Override
            public ArrayList<String> apply(ArrayList<String> strings, String s) {
                if (predicate.test(s)) strings.add(s);
                return strings;
            }
        },
        new BinaryOperator<ArrayList<String>>() {
            @Override
            public ArrayList<String> apply(ArrayList<String> strings, ArrayList<String> strings2) {
                return strings;  
            }
        }).stream().forEach(System.out::println);

並行
當Stream是並行時,第三個引數就有意義了,它會將不同執行緒計算的結果呼叫combiner做彙總後返回。 
注意由於採用了平行計算,前兩個引數與非並行時也有了差異! 
舉個簡單點的例子,計算4+1+2+3的結果,其中4是初始值:

/**
 * lambda語法:
 * System.out.println(Stream.of(1, 2, 3).parallel().reduce(4, (s1, s2) -> s1 + s2
 , (s1, s2) -> s1 + s2));
 **/
System.out.println(Stream.of(1, 2, 3).parallel().reduce(4, new BiFunction<Integer, Integer, Integer>() {
            @Override
            public Integer apply(Integer integer, Integer integer2) {
                return integer + integer2;
            }
        }
        , new BinaryOperator<Integer>() {
            @Override
            public Integer apply(Integer integer, Integer integer2) {
                return integer + integer2;
            }
        }));
等價於
System.out.println(Stream.of(1, 2, 3).map(n -> n + 4).reduce((s1, s2) -> s1 * s2));

按非並行的方式來看它是分了三步的,每一步都要依賴前一步的運算結果!那應該是沒有辦法進行平行計算的啊!可實際上現在平行計算出了結果並且關鍵其結果與非並行時是不一致的! 
那要不就是理解上有問題,要不就是這種方式在平行計算上存在BUG。 
暫且認為其不存在BUG,先來看下它是怎麼樣出這個結果的。猜測初始值4是儲存在一個變數result中的;平行計算時,執行緒之間沒有影響,因此每個執行緒在呼叫第二個引數BiFunction進行計算時,直接都是使用result值當其第一個引數(由於Stream計算的延遲性,在呼叫最終方法前,都不會進行實際的運算,因此每個執行緒取到的result值都是原始的4),因此計算過程現在是這樣的:執行緒1:1 + 4 = 5;執行緒2:2 + 4 = 6;執行緒3:3 + 4 = 7;Combiner函式: 5 + 6 + 7 = 18! 

/**
 * lambda語法:
 * System.out.println(Stream.of(1, 2, 3).parallel().reduce(4, (s1, s2) -> s1 + s2
 , (s1, s2) -> s1 * s2));
 */
System.out.println(Stream.of(1, 2, 3).parallel().reduce(4, new BiFunction<Integer, Integer, Integer>() {
            @Override
            public Integer apply(Integer integer, Integer integer2) {
                return integer + integer2;
            }
        }
        , new BinaryOperator<Integer>() {
            @Override
            public Integer apply(Integer integer, Integer integer2) {
                return integer * integer2;
            }
        }));
等價於
System.out.println(Stream.of(1, 2, 3).map(n -> n + 4).reduce((s1, s2) -> s1 * s2));

以上示例輸出的結果是210! 
它表示的是,使用4與1、2、3中的所有元素按(s1,s2) -> s1 + s2(accumulator)的方式進行第一次計算,得到結果序列4+1, 4+2, 4+3,即5、6、7;然後將5、6、7按combiner即(s1, s2) -> s1 * s2的方式進行彙總,也就是5 * 6 * 7 = 210。 
使用函式表示就是:(4+1) * (4+2) * (4+3) = 210;
reduce的這種寫法可以與以下寫法結果相等(但過程是不一樣的,三個引數時會進行並行處理):
這種方式有助於理解並行三個引數時的場景,實際上就是第一步使用accumulator進行轉換(它的兩個輸入引數一個是identity, 一個是序列中的每一個元素),由N個元素得到N個結果;第二步是使用combiner對第一步的N個結果做彙總。如果第一個引數的型別是ArrayList等物件而非基本資料型別的包裝類或者String,第三個函式的處理上可能容易引起誤解

 

分組與分割槽

會經常使用到Collectors這個類,這個類實際上是一個封裝了很多常用的匯聚操作的一個工廠類。我們之前用到過

//將結果匯聚到ArrayList中
Collectors.toList();
//將結果匯聚到HashSet中
Collectors.toSet();

以及更為通用的

//將結果匯聚到一個指定型別的集合中
Collectors.toCollection(Supplier<C> collectionFactory);

Stream分組

在實際開發中,對於將一個集合的內容進行分組或分割槽這種需求也非常常見,所以我們繼續學習下Collectors類中的groupingBy和partitioningBy方法。

public static Collector groupingBy(Function<? super T, ? extends K> classifier){
    //...
}

groupingBy接收一個Function型別的變數classifier,classifier被稱作分類器,收集器會按著classifier作為key對集合元素進行分組,然後返回Collector收集器物件,假如現在有一個實體Student

public class Student {
    private String name;
    private int score;
    private int age;

    public Student(String name,int score,int age){
        this.name = name;
        this.score = score;
        this.age = age;
    }

    public String getName() {
        return name;
    }
}

我們現在按Student的name進行分組,如果使用sql來表示就是select * from student group by name; 再看下使用Stream的方式

Map<String, List<Student>> collect = students.stream().collect(Collectors.groupingBy(Student::getName));

這裡我們使用方法引用(類名::例項方法名)替代lambda表示式(s -> s.getName())的方式來指定classifier分類器,使集合按Student的name來分組。
注意到分組後的返回型別是Map<String, List<Student>>,結果集中會將name作為key,對應的Student集合作為value返回。
那如果按name分組後,想求出每組學生的數量,就需要藉助groupingBy另一個過載的方法

public static Collector groupingBy(Function<? super T, ? extends K> classifier,Collector<? super T, A, D> downstream){
    //...
}

第二個引數downstream還是一個收集器Collector物件,也就是說我們可以先將classifier作為key進行分組,然後將分組後的結果交給downstream收集器再進行處理

//按name分組 得出每組的學生數量 使用過載的groupingBy方法,第二個引數是分組後的操作
Map<String, Long> collect1 = students.stream().collect(Collectors.groupingBy(Student::getName, Collectors.counting()));

Collectors類這裡也幫我們封裝好了用於統計數量的counting()方法,這裡先了解一下counting()就是將收集器中元素求總數即可

我們還可以對分組後的資料求平均值

Map<String, Double> collect2 = students.stream().collect(Collectors.groupingBy(Student::getName, Collectors.averagingDouble(Student::getScore)));

averagingDouble方法接收一個ToDoubleFunction引數

@FunctionalInterface
public interface ToDoubleFunction<T> {

    double applyAsDouble(T value);
}

ToDoubleFunction實際上也是Function系列函式式介面中的其中一個特例,接收一個引數,返回Double型別(這裡是接收一個Student返回score)。因為分組後的集合中每個元素是Student型別的,所以我們無法直接對Student進行求平均值

//虛擬碼
Collectors.averagingDouble(Student))

所以需要將Student轉成score再求平均值,Collectors.averagingDouble(Student::getScore))。

給出一個String型別的陣列,找出其中所有不重複的素數,並統計其出現次數

    public void primaryOccurrence(String... numbers) {
        List<String> l = Arrays.asList(numbers);
        Map<Integer, Integer> r = l.stream()
            .map(e -> new Integer(e))
            .filter(e -> Primes.isPrime(e))
            .collect( Collectors.groupingBy(p->p, Collectors.summingInt(p->1)) );
        System.out.println("primaryOccurrence result is: " + r);

 Collectors.groupingBy(p->p, Collectors.summingInt(p->1))

它的意思是:把結果收集到一個Map中,用統計到的各個素數自身作為鍵,其出現次數作為值。

 

Stream分割槽

collect方法
1. <R, A> R collect(Collector<? super T, A, R> collector);
2. &l