主頁 > 後端開發 > 萬字詳解 | Java 流式編程

萬字詳解 | Java 流式編程

2023-04-07 07:26:18 後端開發

概述

Stream API 是 Java 中引入的一種新的資料處理方法,它提供了一種高效且易于使用的方法來處理資料集合,Stream API 支持函式式編程,可以讓我們以簡潔、優雅的方式進行資料操作,還有使用 Stream 的兩大原因:

  1. 在大多數情況下,將物件存盤在集合中就是為了處理它們,因此你會發現你把編程 的主要焦點從集合轉移到了流上,
  2. 當 Lambda 運算式和方法參考(method references),流(Stream)結合使用的時候會讓人感覺自成一體,行云流水的感覺

先展示一段簡單的流式編程:

import java.util.Random;

public class Randoms {
    
    public static void main(String[] args) {
        // 隨機展示 5 至 20 之間不重復的整數并進行排序
        new Random(47)
                .ints(5, 20)
                .distinct()             // 使流中的整數不重復
                .limit(7)       		// 獲取前 7 個元素
                .sorted()               // 排序
                .forEach(System.out::println);
    }
}

輸出結果:

6
10
13
16
17
18
19

實際上函式式的編程風格是宣告式(Declarative programming)的,它宣告了要做什么, 而不是指明(每一步)如何做,

相同的程式,相比宣告式風格,命令式(Imperative)編程的形式(指明每一步如何做),代碼閱讀起來會更難理解:

import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;

public class ImperativeRandoms {

    public static void main(String[] args) {
        Random rand = new Random(47);
        SortedSet<Integer> rints = new TreeSet<>();
        while (rints.size() < 7) {
            int r = rand.nextInt(20);
            if (r < 5) continue;
            rints.add(r);
        }
        System.out.println(rints);
    }
}

輸出結果:

[7, 8, 9, 11, 13, 15, 18]

所以使用流式編程的幾個理由:

  1. 表達力強,清晰的語意
  2. 內部迭代 internal iteration (看不見迭代程序)更簡單的處理并發
  3. 流式懶加載的,只在絕對必要時才計算

Java 8 通過在添加介面中添加 default 關鍵字,通過默認方法的方式將流式 Stream 方法平滑地嵌入到現有的類中,

流操作的型別有三種:

  1. 創建流:生產流
  2. 修改流元素:中間操作
  3. 消費流元素:終端操作,收集流元素,通常式匯入一個集合

創建流

通過 Stream.of() 很容見的將一組元素轉化為流:

import java.util.stream.Stream;

public class StreamOf {

    public static void main(String[] args) {
        // 創建流
        Stream.of(new Bubble(1), new Bubble(2), new Bubble(3))
                .forEach(System.out::println);

        Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!")
                .forEach(System.out::print);

        System.out.println();
        Stream.of(3.14159, 2.718, 1.618)
                .forEach(System.out::println);
    }
}

輸出結果:

Bubble 1
Bubble 2
Bubble 3
It's a wonderful day for pie!
3.14159
2.718
1.618

通過 stream() 方法很容易將傳統的集合轉化為 Stream:

public class CollectionToStream {

    public static void main(String[] args) {
        List<Bubble> bubbles = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3));

        System.out.println(bubbles.stream()  // 將集合轉換成為流
                .mapToInt(b -> b.i)          // 獲取流中所有元素,對元素進行應用操作,并產生新的物件,這里的 mapToInt 中間操作會轉換成為包含整型數字的 IntStream
                .sum());                     // 合計

        HashSet<String> w = new HashSet<>(Arrays.asList("It's a wonderful day for pie!".split(" ")));
        w.stream()
                .map(x -> x + " ")
                .forEach(System.out::print);        // stream 遍歷并且列印 Set 中的元素
        System.out.println();

        Map<String, Double> m = new HashMap<>();
        m.put("pi", 3.14159);
        m.put("e", 2.718);
        m.put("phi", 1.618);
        m.entrySet().stream()
                .map(e -> e.getKey() + ": " + e.getValue())
                .forEach(System.out::println);      // stream 遍歷并且列印 Map 中的元素
    }
}

輸出結果:

6
a pie! It's for wonderful day 
phi: 1.618
e: 2.718
pi: 3.14159

亂數流

Java 8 的 Random 類也集成流的方法,很方便的創建亂數流:

import java.util.Random;
import java.util.stream.Stream;

// 生成亂數流
public class RandomGenerators {

    public static <T> void show(Stream<T> stream) {
        stream.limit(4).forEach(System.out::println);
        System.out.println("++++++++++");
    }

    public static void main(String[] args) {
        Random rand = new Random(47);
        show(rand.ints().boxed());
        show(rand.longs().boxed());
        show(rand.doubles().boxed());
        // 控制上限和下限
        show(rand.ints(10, 20).boxed());
        show(rand.longs(50, 100).boxed());
        show(rand.doubles(20, 30).boxed());
        // 控制流大小
        show(rand.ints(2).boxed());
        show(rand.longs(2).boxed());
        show(rand.doubles(2).boxed());
        // 控制流大小和上限和下限
        show(rand.ints(3, 3, 9).boxed());
        show(rand.longs(3, 12, 22).boxed());
        show(rand.doubles(3, 11.5, 12.3).boxed());
    }
}

輸出結果:

-1172028779
1717241110
-2014573909
229403722
++++++++++
2955289354441303771
3476817843704654257
-8917117694134521474
4941259272818818752
++++++++++
# ……………………

int 整型范圍

Stream API 對基本資料型別生成流提供便捷的方法,例如對一段整型序列求和,展示新舊代碼對比

import java.util.stream.IntStream;

public class Ranges {

    public static void main(String[] args) {
        // 傳統方法
        int result = 0;
        for (int i = 0; i < 20; i++) {
            result += i;
        }
        System.out.println(result);

        // 使用流
        System.out.println(IntStream.range(0, 20).sum());
    }
}

輸出結果:

190
190

generate()

Stream API 還可以結合 Supplier 函式介面來創建流,例如,創建一個亂數序列:

import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamGenerateExample {

    public static void main(String[] args) {
        Random random = new Random();
        Stream<Integer> randomNumbers = Stream.generate(random::nextInt);

        // 生成 10 個亂數放入集合中
        List<Integer> integers = randomNumbers
                .limit(10)
                .collect(Collectors.toList());

        integers.forEach(System.out::println);
    }
}

輸出結果:

514000574
1771591868
600289224
-1474939200
-276604430
-876159270
509964750
-497958443
811408347
703285366

iterate()

Stream.iterate 是 Java 8 引入的 Stream API 的一部分,它接受一個種子值(seed)和一個一元函式(unary operator),然后生成一個無限的、順序的流,流中的每個元素都是通過對前一個元素應用一元函式生成的,與 Stream.generate 類似, Stream.iterate 常常用于生成一個斐波那契數列,代碼示例:

import java.util.stream.Stream;

public class Fibonacci {
    int x = 1;

    Stream<Integer> numbers() {
        return Stream.iterate(0, i -> {
            int result = x + i;
            x = i;
            return result;
        });
    }

    public static void main(String[] args) {
        Fibonacci fbi = new Fibonacci();
        fbi.numbers()
                .skip(20)           // 丟棄前 20 個
                .limit(10)     // 取 10 個
                .forEach(System.out::println);
    }
}

輸出結果:

6765
10946
17711
28657
46368
75025
121393
196418
317811
514229

Stream.iterateStream.generate 都是 Java 8 引入的 Stream API 的一部分,它們用于生成無限的順序流,稍不留神就容易把它們搞混,可以通過以下的方式來區分它們:

iterate

  • 它接受一個種子值(seed)和一個一元函式(unary operator),
  • 它生成的流中的每個元素都是通過對前一個元素應用一元函式生成的,
  • 適用于需要生成基于前一個值的序列的場景,例如生成遞增/遞減序列、斐波那契數列等,

generate

  • 它接受一個 Supplier<T> 型別的引數,
  • 它生成的流中的每個元素都是由提供的 Supplier 生成的,
  • 適用于生成獨立于前一個值的序列的場景,例如生成亂數序列、常量序列等,

Arrays

Arrays 類中的 stream() 方法用于將陣列轉換為 Stream,以下是使用 Arrays.stream() 方法的一些示例:

import java.util.Arrays;

public class ArraysStreamExample {

    public static void main(String[] args) {
        // IntStream
        int[] integer = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        Arrays.stream(integer)
                .filter(n -> n % 2 == 0)
                .forEach(System.out::println);

        // Stream<String>
        String[] words = {"hello", "world", "java", "stream"};
        Arrays.stream(words)
                .map(String::toUpperCase)
                .forEach(System.out::println);

        // DoubleStream
        double[] doubles = {1.0, 3.5, 7.2, 8.8, 12.0, 15.5};
        double average = Arrays.stream(doubles)
                .average()
                .orElse(0.0);
        System.out.println("Average: " + average);
    }
}

輸出結果:

2
4
6
8
10
HELLO
WORLD
JAVA
STREAM
Average: 8.0

中間操作

中間操作(intermediate operations)是那些在 Stream 上執行的操作,但不會觸發流的處理,它們通常回傳一個新的 Stream,該 Stream 包含應用了某種操作后的元素,

以下是一些常見的中間操作:

  1. filter(Predicate<T> predicate):根據給定的謂詞篩選 Stream 中的元素,
  2. map(Function<T, R> mapper):將 Stream 中的每個元素轉換為另一種型別,根據給定的映射函式,
  3. flatMap(Function<T, Stream<R>> mapper):將每個元素轉換為另一個 Stream,然后將所有這些流連接成一個 Stream,
  4. distinct():回傳一個去重后的 Stream,其中每個元素只出現一次,
  5. sorted():回傳一個按自然順序排序的 Stream,
  6. sorted(Comparator<T> comparator):根據給定的比較器回傳一個排序后的 Stream,
  7. peek(Consumer<T> action):對 Stream 中的每個元素執行給定的操作,但不會改變 Stream 中的元素,通常用于除錯目的,

注意:中間操作是惰性的,也就是說,它們只在終端操作被呼叫時才會實際執行,例如 forEachcollectreduce

跟蹤和除錯

Stream.peek() 是一個中間操作,它接受一個 Consumer,并允許您在流的每個元素上執行某個操作,同時保持流的元素不變,通常用于除錯目的,因為它允許您查看流處理程序中的中間結果,示例:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Peeking {

    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Alice", "Bob", "Cindy", "David");

        List<String> result = names.stream()
                .filter(name -> name.length() > 3)
                .peek(name -> System.out.println("Filtered name: " + name))
                .map(String::toUpperCase)
                .peek(name -> System.out.println("Mapped name: " + name))
                .collect(Collectors.toList());

        System.out.println("Result: " + result);
    }
}

以上代碼邏輯是:

  1. 首先通過 filter 操作篩選出長度大于3的名字
  2. 然后,我們使用 peek() 來列印篩選后的名字
  3. 接下來,我們使用 map 操作將篩選后的名字轉換為大寫形式
  4. 然后我們再次使用 peek() 來列印轉換后的名字
  5. 最后,我們通過 collect 操作將流中的元素收集到一個新的 List 中,并列印結果

輸出結果:

Filtered name: John
Mapped name: JOHN
Filtered name: Alice
Mapped name: ALICE
Filtered name: Cindy
Mapped name: CINDY
Filtered name: David
Mapped name: DAVID
Result: [JOHN, ALICE, CINDY, DAVID]

排序

Stream.sorted() 可以通過內置的比較器,很容易的對集合進行排序

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class Peeking {

    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Alice", "Bob", "Cindy", "David");

        List<String> result = names.stream()
                .sorted(Comparator.reverseOrder())
                .map(String::toLowerCase)
                .collect(Collectors.toList());

        System.out.println("Result: " + result);
    }
}

輸出結果:

Result: [john, david, cindy, bob, alice]

過濾元素

常見的場景,Stream API 提供以下函式進行過濾:

  • distinct():消除流中重復的元素,相比創建 Set 成本低的多
  • filter(Predicate):根據 Predicate 邏輯進行過濾,剩下的元素傳遞給后面的流

簡單看一個示例:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class DistinctAndFilterExample {

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 1, 4, 5, 4, 6, 7, 7, 8);
        List<Integer> result = numbers.stream()
                .distinct()     // 1: 消除重復元素
                .filter(e -> e % 2 == 0)        //2: 篩選出偶數
                .collect(Collectors.toList());      //3: 將結果放入 List 中

        System.out.println(result);
    }
}

以上代碼邏輯很簡單:

  1. 使用 distinct() 方法洗掉重復元素
  2. 使用 filter() 方法篩選出偶數
  3. 我們通過 collect() 方法將處理后的 Stream 轉換回 List

輸出結果:

[2, 4, 6, 8]

操作元素

對元素的操作主要通過 map(Function) 來完成,在上面的示例代碼中也有看到過,常見于以下場景:

  1. 型別轉換
  2. 資料轉換
  3. 物件屬性提取

類似函式 mapToInt,mapToLong,mapToDouble 操作都一樣,只是結果分別為:IntStream,LongStream,DoubleStream

以下是一個簡單的使用示例:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MapExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // Using map(Function) to square each number
        List<Integer> squaredNumbers = numbers.stream()
                                              .map(number -> number * number)
                                              .collect(Collectors.toList());

        System.out.println("Original list: " + numbers);
        System.out.println("Squared numbers: " + squaredNumbers);
    }
}

輸出結果:

Original list: [1, 2, 3, 4, 5]
Squared numbers: [1, 4, 9, 16, 25]

合并流

某些情況,我們輸入源的流可能是一個復雜的多層嵌套的資料結構,我們想在處理流資料的同時,順便也更更改它的結構,例如把它展開為一個展平為單層資料結構,那么 flatMap() 中間函式就會派上用場:

  • flatMap():與 map() 所作的事情相同,但它將這些生成的 Stream 合并為一個單一的 Stream
有啥用 ?

flatMap() 在函式式編程和流式處理中非常有用,因為它可以解決一些常見的資料處理問題,

  1. 展平嵌套資料結構:在處理復雜的資料結構時,我們經常會遇到嵌套的集合,例如串列的串列、集合的集合等,flatMap() 可以將這些嵌套的資料結構展平為一個單一的流,從而簡化后續的資料處理和操作,
  2. 合并多個流:在某些情況下,我們需要將多個流合并成一個流,以便對所有流中的資料執行相同的操作,flatMap() 可以幫助我們將這些流合并為一個流,從而提高代碼的可讀性和可維護性,
  3. 動態生成流:flatMap() 使我們能夠根據流中的每個元素動態生成新的流,并將這些新生成的流合并為一個流,這對于根據流中的資料動態創建資料處理管道非常有用,
  4. 更高效的操作鏈:在某些情況下,使用 flatMap() 可以減少對流中資料的遍歷次數,從而提高操作鏈的效率,例如,如果我們需要先對流中的每個元素執行映射操作,然后再執行篩選操作,我們可以使用 flatMap() 將這兩個操作組合在一起,從而減少對流的遍歷次數,

展平嵌套資料結構:例如,將一個串列的串列轉換為一個包含所有元素的平面串列:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FlatMapExample {

    public static void main(String[] args) {
        List<List<Integer>> nestedList = Arrays.asList(
                Arrays.asList(1, 2, 3),
                Arrays.asList(4, 5, 6),
                Arrays.asList(7, 8, 9)
        );

        List<Integer> flatList = nestedList.stream()
                .flatMap(list -> list.stream())
                .collect(Collectors.toList());

        System.out.println("Nested list: " + nestedList);
        System.out.println("Flat list: " + flatList);
    }
}

輸出結果:

Nested list: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Flat list: [1, 2, 3, 4, 5, 6, 7, 8, 9]

合并多個流:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class FlatMapExample {

    public static void main(String[] args) {
        List<String> list1 = Arrays.asList("a", "b", "c");
        List<String> list2 = Arrays.asList("d", "e", "f");
        List<String> list3 = Arrays.asList("g", "h", "i");

        // 創建包含多個串列的流
        Stream<List<String>> listsStream = Stream.of(list1, list2, list3);

        // 使用 flatMap() 合并多個流
        List<String> mergedList = listsStream.flatMap(list -> list.stream())
                .collect(Collectors.toList());

        System.out.println("Merged list: " + mergedList);
    }
}

說明以下上面的示例的代碼:

  1. 首先創建了一個包含三個串列的流(listsStream
  2. 使用 flatMap() 方法將這些串列轉換為單獨的流,并將這些流合并為一個流
  3. 最后,我們使用 collect() 方法將合并后的流轉換為一個串列(mergedList

通過使用 flatMap(),我們可以輕松地將多個流合并為一個流,從而簡化資料處理和操作,輸出結果:

Merged list: [a, b, c, d, e, f, g, h, i]
和 map() 區別
  • map() 主要用于轉換流中的元素,但保持流的結構不變,
  • flatMap()flatMap(Function) 主要用于將嵌套或多層資料結構展平為單層資料結構,
如何選擇 ?
  • 如果你只需要對流中的元素執行某種操作或計算,而不需要改變流的結構,那么 map() 是一個很好的選擇
  • 如果你需要將多個 Stream 合并為一個 Stream,或者將嵌套資料結構展平為單層資料結構,那么 flatMap() 是一個更合適的選擇

Optional 類

Optional 主要用于在流中處理一些空元素,但是它還可以應用在代碼的其他地方,它帶來以下一些好處,例如:

  • 避免 NullPointerException:Optional 類幫助您更優雅地處理可能為 null 的值
  • 提高代碼可讀性:使用 Optional 類可以讓您的代碼更具表現力,更容易理解
  • 更好的 API 設計:使用 Optional 類可以讓您的 API 更清晰地表達預期行為,例如,回傳值可能為空的情況,

流里面的 Optional

在使用 Stream 時,很多操作都會回傳 Optional 物件,例如:

  • findFirst(): 回傳 Stream 中的第一個物件,使用 Optional 包裝
  • findAny(): 回傳 Stream 中任意一個物件,使用 Optional 包裝
  • max(): 回傳 Stream 中最大的物件,使用 Optional 包裝
  • min(): 回傳 Stream 中最小的物件,使用 Optional 包裝
  • average(): 回傳 Stream 中所有物件的平均值,使用 OptionalDouble 包裝

示例代碼:

import java.util.*;

public class OptionalExample {

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);

        Optional<Integer> firstNumber = numbers.stream().filter(n -> n % 2 == 0).findFirst();
        System.out.println("findFirst() example: " + firstNumber.orElse(-1));   // 輸出:2

        Optional<Integer> anyNumber = numbers.stream().filter(n -> n > 2).findAny();
        System.out.println("findAny() example: " + anyNumber.orElse(-1));   // 輸出:3

        Optional<Integer> maxNumber = numbers.stream().max(Comparator.naturalOrder());
        System.out.println("max() example: " + maxNumber.orElse(-1)); // 輸出:9

        Optional<Integer> minNumber = numbers.stream().min(Comparator.naturalOrder());
        System.out.println("min() example: " + minNumber.orElse(-1)); // 輸出:1

        OptionalDouble average = numbers.stream().mapToInt(Integer::intValue).average();
        System.out.println("average() example: " + (average.isPresent() ? average.getAsDouble() : -1)); // 輸出:5.0
    }
}

輸出結果:

findFirst() example: 2
findAny() example: 3
max() example: 9
min() example: 1
average() example: 5.0

示例代碼還展示一些解包 Optional 的操作:

  • ifPresent(Consumer):當值存在時呼叫 Consumer,否則什么也不做
  • orElse(otherObject):如果值存在則直接回傳,否則生成 otherObject

Optional 還提供更靈活的 Supplier 函式式介面的呼叫:

  • orElseGet(Supplier):如果值存在則直接回傳,否則使用 Supplier 函式生成一 個可替代物件
  • orElseThrow(Supplier):如果值存在直接回傳,否則使用 Supplier 函式生成一 個例外

可以通過以下示例代碼來理解:

public class OptionalExample {

    public static void main(String[] args) {
        // 生成一個空 Optional
        Optional<String> optionalValue = https://www.cnblogs.com/xiao2shiqi/p/Optional.empty();

        // orElseGet() 示例
        String value1 = optionalValue.orElseGet(() ->"Default value");
        System.out.println("Value 1: " + value1); // 輸出:Value 1: Default value

        // orElseThrow() 示例
        try {
            String value2 = optionalValue.orElseThrow(
                () -> new IllegalStateException("Value is not present")
            );
            System.out.println("Value 2: " + value2);
        } catch (RuntimeException e) {
            // 輸出:Exception caught: Value is not present
            System.out.println("Exception caught: " + e.getMessage()); 
        }
    }
}

輸出結果:

Value 1: Default value
Exception caught: Value is not present

創建 Optional

我們也可以在自己的代碼里面創建 Optional 物件,有以下幾個靜態方法可以使用:

  • Optional.empty():創建一個空的 Optional 物件,這個物件不包含任何值
  • Optional.of():使用一個非空值創建一個 Optional 物件,如果傳入的值為 null,將拋出一個空指標例外
  • Optional.ofNullable():創建一個可能為 null 的 Optional 物件

示例代碼:

import java.util.*;

public class OptionalExample {
    
    public static void main(String[] args) {
        // 生成一個空的 Optional
        Optional<String> emptyOptional = Optional.empty();
        // 生成一個不為空的 Optional
        Optional<String> optionalWithValue = https://www.cnblogs.com/xiao2shiqi/p/Optional.of("hello world");
        // 可能為空的 Optional
        Optional<String> optionalWithValue1 = Optional.ofNullable("hello world"); // 非空值
        Optional<String> optionalWithValue2 = Optional.ofNullable(null); // 空值
    }
}

操作 Optional

創建 Optional 物件后,可以通過內置的函式對 Optional 進行更多的操作,常見的有:

  • filter(Predicate): 物件的值滿足給定的 Predicate,則回傳該 Optional 物件
  • map(Function): 使用給定的 Function 對該值進行轉換,并回傳一個包含轉換后值的新 Optional 物件
  • flatMap(Function): 使用給定的 Function 對該值進行轉換,并回傳一個包含轉換后值的新 Optional 物件,

示例代碼:

import java.util.*;

public class OptionalExample {

    public static void main(String[] args) {
        Optional<Integer> optionalValue1 = Optional.of(10);
        Optional<Integer> optionalValue2 = Optional.empty();

        // 使用 filter() 方法
        Optional<Integer> filteredValue1 = optionalValue1.filter(value -> value > 5);
        System.out.println("Filtered value 1: " + filteredValue1.orElse(-1));
        
        // 使用 map() 方法
        Optional<String> mappedValue1 = optionalValue1.map(value -> "Value is: " + value);
        System.out.println("Mapped value 1: " + mappedValue1.orElse("Not present"));
        
        // 使用 flatMap() 方法
        Optional<String> flatMappedValue1 = optionalValue2.flatMap(value -> Optional.of("Value is: " + value));
        System.out.println("FlatMapped value 1: " + flatMappedValue1.orElse("Not present"));
    }
}

輸出結果:

Filtered value 1: 10
Mapped value 1: Value is: 10
FlatMapped value 1: Not present

注意:這里的 flatMap()map() 方法不同,flatMap() 會改變 Optional 結構本身,map() 則不會

終端操作

終端操作(Terminal Operations)是我們在流管道中所做的最后一件事,通過該操作獲得流中的結果

陣列

通過以下方法,可以輕易的收集一個流,并且將流轉為陣列:

  • toArray():轉換為物件陣列
  • toArray(generator):轉換為特定型別的陣列

示例代碼:

import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class StreamToArrayExample {

    public static void main(String[] args) {
        // 轉換為物件陣列
        Stream<String> stringStream1 = Stream.of("apple", "banana", "cherry");
        System.out.println("Stream to array 1: " + Arrays.toString(stringStream1.toArray()));

        // 轉換為特定型別的陣列
        Stream<String> stringStream2 = Stream.of("apple", "banana", "cherry");
        String[] stringArray = stringStream2.toArray(String[]::new);
        System.out.println("Stream to array 2: " + Arrays.toString(stringArray));

        // 對于基本型別的陣列,可以使用特定的流類
        int[] array = IntStream.range(1, 6).toArray();
        System.out.println("Stream to array 3: " + Arrays.toString(array));
    }
}

輸出結果:

Stream to array 1: [apple, banana, cherry]
Stream to array 2: [apple, banana, cherry]
Stream to array 3: [1, 2, 3, 4, 5]

回圈

Stream 中提供 2 個回圈遍歷方法,用于消費流,分別如下:

  • forEach():遍歷流,在并行流中不保證流的順序
  • forEachOrdered():遍歷流,在并行流中保證按照流中元素的順序執行操作

我們通過以下示例代碼來證明:

import java.util.Arrays;
import java.util.List;

public class ForEachOrderedParallelExample {

    public static void main(String[] args) {
        List<String> stringList = Arrays.asList("apple", "banana", "cherry", "date", "fig", "grape");

        // 順序流
        System.out.println("Sequential stream:");
        stringList.stream().forEachOrdered(System.out::println);

        // 并行流 (亂序)
        System.out.println("\nParallel stream with forEach:");
        stringList.parallelStream().forEach(System.out::println);

        // 并行流 (順序)
        System.out.println("\nParallel stream with forEachOrdered:");
        stringList.parallelStream().forEachOrdered(System.out::println);
    }
}

輸出結果:

Sequential stream:
apple
banana
cherry
date
fig
grape

Parallel stream with forEach:
date
grape
fig
cherry
banana
apple

Parallel stream with forEachOrdered:
apple
banana
cherry
date
fig
grape

集合

主要用于將流中的元素收集到不同型別的結果容器,如集合、字串或其他資料結構,它的主要方法有:

  • collect(Collector):使用 Collector 收集流元素到結果集合中

我們看看如何把 Stream 收集為常見的 ListSetMap,還有 String ,示例代碼:

public class CollectExample {

    public static void main(String[] args) {
        // 收集到 List
        List<String> collectedList = Stream.of("apple", "banana", "orange", "grape")
                .map(String::toUpperCase)
                .collect(Collectors.toList());
        System.out.println("Collected List: " + collectedList);

        // 收集到 Set
        Set<String> collectedSet = Stream.of("apple", "banana", "orange", "grape")
                .map(String::toLowerCase)
                .collect(Collectors.toSet());
        System.out.println("Collected Set: " + collectedSet);

        // 收集到 Map
        Map<String, Integer> map = Stream.of("apple", "banana", "orange", "grape")
                .map(String::toUpperCase)
                .collect(Collectors.toMap(s -> s, String::length));
        System.out.println("Collected Map: " + map);

        // 收集到 String,使用逗號分隔
        String joinedString  = Stream.of("apple", "banana", "orange", "grape")
                .map(String::toUpperCase)
                .collect(Collectors.joining(","));
        System.out.println("Joined String: " + joinedString);
    }
}

輸出結果:

Collected List: [APPLE, BANANA, ORANGE, GRAPE]
Collected Set: [banana, orange, apple, grape]
Collected Map: {APPLE=5, GRAPE=5, BANANA=6, ORANGE=6}
Joined String: APPLE,BANANA,ORANGE,GRAPE

說明:在這里我們只是簡單介紹了幾個 Collectors 的運用示例,

實際上,它還有一些非常復雜的操作實作,可通過查看 java.util.stream.Collectors 的 API 檔案了解

組合

用于對流中的元素執行累積操作,將它們減少為一個值,reduce 的使用場景包括對流中的元素執行聚合操作,例如求和、求積、求最大值、求最小值等,Stream 里面的 reduce 方法有以下幾種形式:

  • reduce(BinaryOperator):使用給定的累積器函式,對流中的元素進行累積操作,回傳一個 Optional<T>
  • reduce(identity, BinaryOperator): 使用給定的初始值(identity)和累積器函式,對流中的元素進行累積操作,因此如果流為空,identity 就是結果

先看看 reduce 的示例代碼:

import java.util.OptionalInt;
import java.util.stream.IntStream;

public class ReduceExample {

    public static void main(String[] args) {
        // 求和
        OptionalInt sum = IntStream.range(0, 100).reduce(Integer::sum);
        sum.ifPresent(System.out::println);

        // 求積
        OptionalInt numbers = IntStream.range(0, 100).reduce((a, b) -> a * b);
        numbers.ifPresent(System.out::println);

        // 求最大值
        OptionalInt max = IntStream.range(0, 100).reduce(Integer::max);
        max.ifPresent(System.out::println);

        // 求最小值
        OptionalInt min = IntStream.range(0, 100).reduce(Integer::min);
        min.ifPresent(System.out::println);
        
        // 使用給定的初始值(identity)和累積器函式,對流中的元素進行累積操作,這里回傳 int 值
        int reduced = IntStream.range(0, 100).reduce(10, Integer::sum);
        System.out.println(reduced);
    }
}

輸出結果:

4950
0
99
0
4960

還有一種 reduce(identity, BiFunction, BinaryOperator):更復雜的使用形式暫不介紹,我建議可以顯式地組合 map() 和 reduce() 來更簡單的表達它,

匹配

在 Stream 中的終端操作中,提供 allMatch, anyMatch, 和 noneMatch 它們用于檢查流中的元素是否滿足某個條件:

  • allMatch(Predicate) :流的每個元素提供給 Predicate 都回傳 true ,結果回傳為 true
  • anyMatch(Predicate):流中任意一個元素提供的 Predicate 回傳 true,結果回傳 true
  • noneMatch(Predicate):流的每個元素提供的 Predicate 回傳 false,結果回傳 true

PS:以上計算都是短路操作,在匹配第一個結果時,則停止執行計算

下面是它們的使用場景和示例:

import java.util.stream.Stream;

public class MatchExample {

    public static void main(String[] args) {
        // allMatch:檢查流中的所有元素是否都滿足某個條件
        boolean allEven = Stream.of(1, 2, 3, 4, 5).allMatch(num -> num % 2 == 0);
        System.out.println(allEven); // 輸出:false

        // anyMatch:檢查流中是否存在滿足某個條件的元素
        boolean anyEven = Stream.of(1, 2, 3, 4, 5).anyMatch(num -> num % 2 == 0);
        System.out.println(anyEven); // 輸出:true

        // noneMatch:檢查流中是否不存在滿足某個條件的元素
        boolean noneMatch = Stream.of(1, 2, 3, 4, 5).noneMatch(num -> num > 10);
        System.out.println(noneMatch);  // 輸出:true
    }
}

輸出結果:

false
true
true

查找

在 Stream 中的終端操作中,可以根據 Predicate 獲取指定的元素(在 Optional 章節介紹過),查找函式如下:

  • findFirst():查找第一個滿足某個條件的元素,這在有序流中非常有用
  • findAny():在流中查找任意一個滿足某個條件的元素,這在并行流中非常有用

代碼示例:

import java.util.Optional;
import java.util.stream.Stream;

public class FindExample {

    public static void main(String[] args) {
        // findFirst 示例
        Optional<Integer> first = Stream.of(1, 2, 3, 4, 5).filter(num -> num % 2 == 0).findFirst();
        first.ifPresent(System.out::println); // 輸出:2

        // findAny 示例
        Optional<Integer> any = Stream.of(1, 2, 3, 4, 5, 6).filter(num -> num % 2 == 0).findAny();
        any.ifPresent(System.out::println); // 輸出:2 或者 4 或者 6
    }
}

統計

最后就是一些常見對流進行統計的函式了:

  • count():統計流中的元素數量
  • max(Comparator):根據給定的比較器查找流中的最大值
  • min(Comparator):根據給定的比較器查找流中的最小值

示例代碼:

// count: 流中的元素個數
System.out.println(Stream.of(1, 2, 3, 4, 5).count()); // 輸出:5

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> max = numbers.stream().max(Comparator.naturalOrder());
Optional<Integer> min = numbers.stream().min(Comparator.naturalOrder());

// max, min: 根據給定的比較器查找流中的最大值或最小值
max.ifPresent(System.out::println); // 輸出:5
min.ifPresent(System.out::println); // 輸出:1

輸出結果:

5
5
1

以下方式是適用于基本資料型別的特殊流,它們提供了對流中數字的基本統計資訊:

  • average() :求取流元素平均值
  • max() 和 min():數值流操作無需 Comparator
  • sum():對所有流元素進行求和

代碼示例:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// average: 求取流元素平均值
numbers.stream().mapToInt(Integer::intValue).average().ifPresent(System.out::println);  // 輸出:3.0
// max: 數值流求最大值
numbers.stream().mapToInt(Integer::intValue).max().orElse(0);  // 輸出:5
// min: 數值流最小值
numbers.stream().mapToInt(Integer::intValue).min().orElse(0);  // 輸出:1
// sum: 數值流求和
numbers.stream().mapToInt(Integer::intValue).sum();  // 輸出:15

上例操作對于 LongStream 和 DoubleStream 同樣適用

總結

函式式編程 對 Java 語言的編程范式產生了深遠的影響,在 Stream API 出現之前,處理集合資料通常需要使用 for 回圈、條件判斷和輔助變數等,這樣的代碼往往冗長、復雜,不易閱讀和維護,Stream API 的引入,讓我們能以函式式編程的方式處理資料,提高了代碼的簡潔性、可讀性和可維護性,隨著函式式編程在軟體開發領域的普及,Java 可能會引入更多的函式式編程特性,讓我們能夠更方便地使用函式式編程范式撰寫代碼,隨著函式式編程在軟體開發領域的普及,Java 可能會引入更多的函式式編程特性,讓我們能夠更方便地使用函式式編程范式撰寫代碼, 總之,Java 其未來依然充滿潛力,隨著技術的發展和需求的變化,Java 將不斷演進,為開發者提供更好的編程體驗

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549267.html

標籤:Java

上一篇:Java筆記(13) 簡單的Lambda運算式

下一篇:生產事故-記一次特殊的OOM排查

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more