Stream流式計算從入門到精通

摘要:Stream流式計算,本文講解了Stream流式計算的概念,具體的使用步驟以及源碼實現,最後講解了使用Stream過程中需要注意的事項。Stream在公司項目中被頻繁使用,在性能優化上具有廣泛的使用場景,通過少量的代碼即可優雅地實現並行計算。參考資料:劉超的《java性能調優實戰》,公衆號《java之間》

1、什麼是 Stream?

在 Java8 之前,我們通常是通過 for 循環或者 Iterator 迭代來重新排序合併數據,又或者通過重新定義 Collections.sorts 的 Comparator 方法來實現,這兩種方式對於大數據量系統來說,效率並不是很理想。

Java8 中添加了一個新的接口類 Stream,他和我們之前接觸的字節流概念不太一樣,Java8 集合中的 Stream 相當於高級版的 Iterator,他通過 Lambda 表達式對集合進行各種非常便利、高效的聚合操作(Aggregate Operation),或者大批量數據操作 (Bulk Data Operation)。

Stream原理將要處理的元素看做一種流,流在管道中傳輸,並且可以在管道的節點上處理,包括過濾篩選、去重、排序、聚合等。元素流在管道中進過中間操作的處理,最後由最終操作得到前面處理的結果

例子1:需求是過濾分組一所中學裏身高在 160cm 以上的男女同學

  • 方法1:使用傳統的迭代方式來實現
Map<String, List<Student>> stuMap = new HashMap<String, List<Student>>();
for (Student stu: studentsList) {
    if (stu.getHeight() > 160) { // 如果身高大於 160
        if (stuMap.get(stu.getSex()) == null) { // 該性別還沒分類
            List<Student> list = new ArrayList<Student>(); // 新建該性別學生的列表
            list.add(stu);// 將學生放進去列表
            stuMap.put(stu.getSex(), list);// 將列表放到 map 中
        } else { // 該性別分類已存在
            stuMap.get(stu.getSex()).add(stu);// 該性別分類已存在,則直接放進去即可
        }
    }
}
  • 方法2:Java8 中的 Stream API 進行實現
  • stream() - 爲集合創建串行流
Map<String, List<Student>> stuMap = stuList.stream().filter((Student s) -> s.getHeight() > 160) .collect(Collectors.groupingBy(Student ::getSex)); 
  • parallelStream() - 爲集合創建並行流
Map<String, List<Student>> stuMap = stuList.parallelStream().filter((Student s) -> s.getHeight() > 160) .collect(Collectors.groupingBy(Student ::getSex)); 

2、Stream 如何優化遍歷?

2.1.Stream 操作分類

Stream 的操作分爲兩大類:中間操作(Intermediate operations)和終結操作(Terminal operations)我們通常還會將中間操作稱爲懶操作,也正是由這種懶操作結合終結操作、數據源構成的處理管道(Pipeline),實現了 Stream 的高效

操作類型 詳情 備註
中間操作 只對操作進行了記錄,即只會返回一個流,不會進行計算操作 可以分爲無狀態(Stateless)與有狀態(Stateful)操作,前者是指元素的處理不受之前元素的影響,後者是指該操作只有拿到所有元素之後才能繼續下去
終結操作 終結操作是實現了計算操作 可以分爲短路(Short-circuiting)與非短路(Unshort-circuiting)操作,前者是指遇到某些符合條件的元素就可以得到最終結果,後者是指必須處理完所有元素才能得到最終結果
  • 操作分類詳情如下圖所示
    Stream操作類型
    舉例說明:
    1、新建一個Student類
@Data
public class Student {
    private Long id;
    private String name;
    private int age;
    private String address;
    public Student() {}
    public Student(Long id, String name, int age, String address) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.address = address;
    }
    @Override
    public String toString() {
        return "Student{" + "id=" + id + ", name='" + name + '\'' +
                ", age=" + age + ", address='" + address + '\'' + '}';
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age &&
                Objects.equals(id, student.id) &&
                Objects.equals(name, student.name) &&
                Objects.equals(address, student.address);
    }
    @Override
    public int hashCode() {
        return Objects.hash(id, name, age, address);
    }
}

2、執行Stream操作

public static void main(String [] args) {
        Student s1 = new Student(1L, "肖戰", 15, "浙江");
        Student s2 = new Student(2L, "王一博", 15, "湖北");
        Student s3 = new Student(3L, "楊紫", 17, "北京");
        Student s4 = new Student(4L, "李現", 17, "浙江");
        List<Student> students = new ArrayList<>();
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        List<Student> streamStudents = testFilter(students);
        streamStudents.forEach(System.out::println);
    }
    /**
     * 1、集合的篩選
     * @param students
     * @return
     */
    private static List<Student> testFilter(List<Student> students) {
        //篩選年齡大於15歲的學生
        //return students.stream().filter(s -> s.getAge()>15).collect(Collectors.toList());
        //篩選住在浙江省的學生
        return students.stream().filter(s ->"浙江".equals(s.getAddress())).collect(Collectors.toList());
    }
     /**
     * 2、集合轉換
     * @param students
     * @return
     */
    private static void testMap(List<Student> students) {
        //在地址前面加上部分信息,只獲取地址輸出
        List<String> addresses = students.stream().map(s ->"住址:"+s.getAddress()).collect(Collectors.toList());
        addresses.forEach(a ->System.out.println(a));
    }
    /**
     * 3、集合去重(基本類型)
     */
    private static void testDistinct1() {
        //簡單字符串的去重
        List<String> list = Arrays.asList("111","222","333","111","222");
        list.stream().distinct().forEach(System.out::println);
    }
      /**
     * 3、集合去重(引用對象)
     * 兩個重複的“肖戰”同學進行了去重,這不僅因爲使用了distinct()方法,而且因爲Student對象重寫了equals和hashCode()方法,否則去重是無效的
     */
    private static void testDistinct2() {
        //引用對象的去重,引用對象要實現hashCode和equal方法,否則去重無效
        Student s1 = new Student(1L, "肖戰", 15, "浙江");
        Student s2 = new Student(2L, "王一博", 15, "湖北");
        Student s3 = new Student(3L, "楊紫", 17, "北京");
        Student s4 = new Student(4L, "李現", 17, "浙江");
        Student s5 = new Student(1L, "肖戰", 15, "浙江");
        List<Student> students = new ArrayList<>();
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        students.add(s5);
        students.stream().distinct().forEach(System.out::println);
    }
    /**
     * 4、集合排序(默認排序)
     */
    private static void testSort1() {
        List<String> list = Arrays.asList("333","222","111");
        list.stream().sorted().forEach(System.out::println);
    }
/**
     * 4、集合排序(指定排序規則)
     * 先按照學生的id進行降序排序,再按照年齡進行降序排序
     * 
     */
    private static void testSort2() {
        Student s1 = new Student(1L, "肖戰", 15, "浙江");
        Student s2 = new Student(2L, "王一博", 15, "湖北");
        Student s3 = new Student(3L, "楊紫", 17, "北京");
        Student s4 = new Student(4L, "李現", 17, "浙江");
        List<Student> students = new ArrayList<>();
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        students.stream()
                .sorted((stu1,stu2) ->Long.compare(stu2.getId(), stu1.getId()))
                .sorted((stu1,stu2) -> Integer.compare(stu2.getAge(),stu1.getAge()))
                .forEach(System.out::println);
    }
    /**
     * 5、集合limit,返回前幾個元素
     */
    private static void testLimit() {
        List<String> list = Arrays.asList("333","222","111");
        list.stream().limit(2).forEach(System.out::println);
    }
    /**
     * 6、集合skip,刪除前n個元素
     */
    private static void testSkip() {
        List<String> list = Arrays.asList("333","222","111");
        list.stream().skip(2).forEach(System.out::println);
    }
    /**
     * 7、集合reduce,將集合中每個元素聚合成一條數據
     */
    private static void testReduce() {
        List<String> list = Arrays.asList("歡","迎","你");
        String appendStr = list.stream().reduce("北京",(a,b) -> a+b);
        System.out.println(appendStr);
    }
     /**
     * 8、求集合中元素的最小值
     * 求所有學生中年齡最小的一個
     */
    private static void testMin() {
        Student s1 = new Student(1L, "肖戰", 14, "浙江");
        Student s2 = new Student(2L, "王一博", 15, "湖北");
        Student s3 = new Student(3L, "楊紫", 17, "北京");
        Student s4 = new Student(4L, "李現", 17, "浙江");
        List<Student> students = new ArrayList<>();
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        Student minS = students.stream().min((stu1,stu2) ->Integer.compare(stu1.getAge(),stu2.getAge())).get();
        System.out.println(minS.toString());
    }
    /**
     * 9、anyMatch/allMatch/noneMatch(匹配)
     * anyMatch:Stream 中任意一個元素符合傳入的 predicate,返回 true
       allMatch:Stream 中全部元素符合傳入的 predicate,返回 true
       noneMatch:Stream 中沒有一個元素符合傳入的 predicate,返回 true
     */
    private static void testMatch() {
        Student s1 = new Student(1L, "肖戰", 15, "浙江");
        Student s2 = new Student(2L, "王一博", 15, "湖北");
        Student s3 = new Student(3L, "楊紫", 17, "北京");
        Student s4 = new Student(4L, "李現", 17, "浙江");
        List<Student> students = new ArrayList<>();
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        Boolean anyMatch = students.stream().anyMatch(s ->"湖北".equals(s.getAddress()));
        if (anyMatch) {
            System.out.println("有湖北人");
        }
        Boolean allMatch = students.stream().allMatch(s -> s.getAge()>=15);
        if (allMatch) {
            System.out.println("所有學生都滿15週歲");
        }
        Boolean noneMatch = students.stream().noneMatch(s -> "楊洋".equals(s.getName()));
        if (noneMatch) {
            System.out.println("沒有叫楊洋的同學");
        }
    }
}

2.2 Stream 源碼實現

  • Stream相關的類

    BaseStream 和 Stream 爲最頂端的接口類。BaseStream 主要定義了流的基本接口方法,例如,spliterator、isParallel 等;Stream 則定義了一些流的常用操作方法,例如,map、filter 等
    ReferencePipeline 是一個結構類,他通過定義內部類組裝了各種操作流。他定義了 Head、StatelessOp、StatefulOp 三個內部類,實現了 BaseStream 與 Stream 的接口方法
    Sink 接口是定義每個 Stream 操作之間關係的協議,他包含 begin()、end()、cancellationRequested()、accept() 四個方法
    ReferencePipeline 最終會將整個 Stream 流操作組裝成一個調用鏈,而這條調用鏈上的各個 Stream 操作的上下關係就是通過 Sink 接口協議來定義實現的

在這裏插入圖片描述

2.3 Stream 操作疊加

管道結構通常是由 ReferencePipeline 類實現的,前面講解 Stream 包結構時,我提到過 ReferencePipeline 包含了 Head、StatelessOp、StatefulOp 三種內部類。

名詞 作用
Head 主要用來定義數據源操作,在我們初次調用 names.stream() 方法時,會初次加載 Head 對象,此時爲加載數據源操作
StatelessOp 無狀態中間操作
StatefulOp 有狀態中間操作
Stage 在 JDK 中每次的中斷操作會以使用階段(Stage)命名
AbstractPipeline 用於生成一箇中間操作 Stage 鏈表

中間操作之後,此時的 Stage 並沒有執行,而是通過 AbstractPipeline 生成了一箇中間操作 Stage 鏈表;當我們調用終結操作時,會生成一個最終的 Stage,通過這個 Stage 觸發之前的中間操作,從最後一個 Stage 開始,遞歸產生一個 Sink 鏈。

在這裏插入圖片描述
例子2:需求是查找出一個長度最長,並且以“張”爲姓氏的名字

List<String> names = Arrays.asList(" 張三 ", " 李四 ", " 王老五 ", " 李三 ", " 劉老四 ", " 王小二 ", " 張四 ", " 張五六七 ");
 
String maxLenStartWithZ = names.stream()
    	            .filter(name -> name.startsWith(" 張 "))
    	            .mapToInt(String::length)
    	            .max()
    	            .toString();
執行流程 流程 是否正確
流程: 首先遍歷一次集合,得到以“張”開頭的所有名字;然後遍歷一次 filter 得到的集合,將名字轉換成數字長度;最後再從長度集合中找到最長的那個名字並且返回。 錯誤
流程: 1、names.stream() 方法將會調用集合類基礎接口 Collection 的 Stream 方法; 2、Stream 方法就會調用 StreamSupport 類的 Stream 方法,方法中初始化了一個 ReferencePipeline 的 Head 內部類對象; 3、 再調用 filter 和 map 方法,這兩個方法都是無狀態的中間操作,所以執行 filter 和 map 操作時,並沒有進行任何的操作,而是分別創建了一個 Stage 來標識用戶的每一次操作;4、new StatelessOp 將會調用父類 AbstractPipeline 的構造函數,這個構造函數將前後的 Stage 聯繫起來,生成一個 Stage 鏈表:5、當執行 max 方法時,會調用 ReferencePipeline 的 max 方法,此時由於 max 方法是終結操作,所以會創建一個 TerminalOp 操作,同時創建一個 ReducingSink,並且將操作封裝在 Sink 類中。6、Java8 中的 Spliterator 的 forEachRemaining 會迭代集合,每迭代一次,都會執行一次 filter 操作,如果 filter 操作通過,就會觸發 map 操作,然後將結果放入到臨時數組 object 中,再進行下一次的迭代。完成中間操作後,就會觸發終結操作 max。 正確

names.stream() 方法將會調用集合類基礎接口 Collection 的 Stream 方法;

default Stream<E> stream() {
   return StreamSupport.stream(spliterator(), false);
}
 public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
   Objects.requireNonNull(spliterator);
   return new ReferencePipeline.Head<>(spliterator, StreamOpFlag.fromCharacteristics(spliterator), parallel);
}

ReferencePipeline 的 filter 方法和 map 方法:

@Override
public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
    Objects.requireNonNull(predicate);
    return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,StreamOpFlag.NOT_SIZED) {
        @Override
        Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
            return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
                @Override
                public void begin(long size) {
                    downstream.begin(-1);
                }
                @Override
                public void accept(P_OUT u) {
                    if (predicate.test(u))
                        downstream.accept(u);
                }
            };
        }
    };
}
   @Override
    @SuppressWarnings("unchecked")
    public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
        Objects.requireNonNull(mapper);
        return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
            @Override
            Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
                return new Sink.ChainedReference<P_OUT, R>(sink) {
                    @Override
                    public void accept(P_OUT u) {
                        downstream.accept(mapper.apply(u));
                    }
                };
            }
        };
    }

new StatelessOp 將會調用父類 AbstractPipeline 的構造函數,這個構造函數將前後的 Stage 聯繫起來,生成一個 Stage 鏈表:

AbstractPipeline(AbstractPipeline<?, E_IN, ?> previousStage, int opFlags) {
    if (previousStage.linkedOrConsumed)
        throw new IllegalStateException(MSG_STREAM_LINKED);
    previousStage.linkedOrConsumed = true;
    previousStage.nextStage = this;// 將當前的 stage 的 next 指針指向之前的 stage

    this.previousStage = previousStage;// 賦值當前 stage 當全局變量 previousStage 
    this.sourceOrOpFlags = opFlags & StreamOpFlag.OP_MASK;
    this.combinedFlags = StreamOpFlag.combineOpFlags(opFlags, previousStage.combinedFlags);
    this.sourceStage = previousStage.sourceStage;
    if (opIsStateful())
        sourceStage.sourceAnyStateful = true;
    this.depth = previousStage.depth + 1;
}

因爲在創建每一個 Stage 時,都會包含一個 opWrapSink() 方法,該方法會把一個操作的具體實現封裝在 Sink 類中,Sink 採用(處理 -> 轉發)的模式來疊加操作。
當執行 max 方法時,會調用 ReferencePipeline 的 max 方法,此時由於 max 方法是終結操作,所以會創建一個 TerminalOp 操作,同時創建一個 ReducingSink,並且將操作封裝在 Sink 類中。

@Override
public final Optional<P_OUT> max(Comparator<? super P_OUT> comparator) {
     return reduce(BinaryOperator.maxBy(comparator));
}

最後,調用 AbstractPipeline 的 wrapSink 方法,該方法會調用 opWrapSink 生成一個 Sink 鏈表,Sink 鏈表中的每一個 Sink 都封裝了一個操作的具體實現。

@Override
@SuppressWarnings("unchecked")
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
    Objects.requireNonNull(sink);
    for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
        sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
    }
    return (Sink<P_IN>) sink;
}

當 Sink 鏈表生成完成後,Stream 開始執行,通過 spliterator 迭代集合,執行 Sink 鏈表中的具體操作。

@Override
final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
     Objects.requireNonNull(wrappedSink);
     if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
         wrappedSink.begin(spliterator.getExactSizeIfKnown());
         spliterator.forEachRemaining(wrappedSink);
         wrappedSink.end();
     }
     else {
         copyIntoWithCancel(wrappedSink, spliterator);
     }
}

Java8 中的 Spliterator 的 forEachRemaining 會迭代集合,每迭代一次,都會執行一次 filter 操作,如果 filter 操作通過,就會觸發 map 操作,然後將結果放入到臨時數組 object 中,再進行下一次的迭代。完成中間操作後,就會觸發終結操作 max。

2.4.Stream 並行處理

List<String> names = Arrays.asList(" 張三 ", " 李四 ", " 王老五 ", " 李三 ", " 劉老四 ", " 王小二 ", " 張四 ", " 張五六七 ");
 String maxLenStartWithZ = names.stream().parallel().filter(name -> name.startsWith("張"))
    	            .mapToInt(String::length).max().toString();

Stream 的並行處理在執行終結操作之前,跟串行處理的實現是一樣的。而在調用終結方法之後,實現的方式就有點不太一樣,會調用 TerminalOp 的 evaluateParallel 方法進行並行處理。這裏的並行處理指的是,Stream 結合了 ForkJoin 框架,對 Stream 處理進行了分片,Splititerator 中的 estimateSize 方法會估算出分片的數據量

final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
   assert getOutputShape() == terminalOp.inputShape();
    if (linkedOrConsumed)
        throw new IllegalStateException(MSG_STREAM_LINKED);
    linkedOrConsumed = true;

    return isParallel()
           ? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
           : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
}

通過預估的數據量獲取最小處理單元的閥值,如果當前分片大小大於最小處理單元的閥值,就繼續切分集合。每個分片將會生成一個 Sink 鏈表,當所有的分片操作完成後,ForkJoin 框架將會合並分片任何結果集

3、合理使用 Stream

我們將對常規的迭代、Stream 串行迭代以及 Stream 並行迭代進行性能測試對比,迭代循環中,我們將對數據進行過濾、分組等操作。分別進行以下幾組測試:

測試 結論(迭代使用時間)
多核 CPU 服務器配置環境下,對比長度 100 的 int 數組的性能; 常規的迭代 <Stream 並行迭代 <Stream 串行迭代
多核 CPU 服務器配置環境下,對比長度 1.00E+8 的 int 數組的性能; Stream 並行迭代 < 常規的迭代 <Stream 串行迭代
多核 CPU 服務器配置環境下,對比長度 1.00E+8 對象數組過濾分組的性能; Stream 並行迭代 < 常規的迭代 <Stream 串行迭代
單核 CPU 服務器配置環境下,對比長度 1.00E+8 對象數組過濾分組的性能。 常規的迭代 <Stream 串行迭代 <Stream 並行迭代

結論:在循環迭代次數較少的情況下,常規的迭代方式性能反而更好;在單核 CPU 服務器配置環境中,也是常規迭代方式更有優勢;而在大數據循環迭代中,如果服務器是多核 CPU 的情況下,Stream 的並行迭代優勢明顯。所以在平時處理大數據的集合時,應該儘量考慮將應用部署在多核 CPU 環境下,並且使用 Stream 的並行迭代方式進行處理。

4、總結

在串行處理操作中,Stream 在執行每一步中間操作時,並不會做實際的數據操作處理,而是將這些中間操作串聯起來,最終由終結操作觸發,生成一個數據處理鏈表,通過 Java8 中的 Spliterator 迭代器進行數據處理;此時,每執行一次迭代,就對所有的無狀態的中間操作進行數據處理,而對有狀態的中間操作,就需要迭代處理完所有的數據,再進行處理操作;最後就是進行終結操作的數據處理。

在並行處理操作中,Stream 對中間操作基本跟串行處理方式是一樣的,但在終結操作中,Stream 將結合 ForkJoin 框架對集合進行切片處理,ForkJoin 框架將每個切片的處理結果 Join 合併起來。最後就是要注意 Stream 的使用場景。

5、思考題

這裏有一個簡單的並行處理案例,請你找出其中存在的問題。

// 使用一個容器裝載 100 個數字,通過 Stream 並行處理的方式將容器中爲單數的數字轉移到容器 parallelList
List<Integer> integerList= new ArrayList<Integer>(); 
for (int i = 0; i <100; i++) {
      integerList.add(i);
}
 
List<Integer> parallelList = new ArrayList<Integer>() ;
integerList.stream()
           .parallel()
           .filter(i->i%2==1)
           .forEach(i->parallelList.add(i));

答案:ArrayList不是線程安全的,在並行操作時,會出現多線程操作問題,例如出現null值,有可能是在擴容時,複製出現問題。同時也會出現值被覆蓋的情況。可以換成線程安全的ArrayList。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章