Java 8 Stream API的使用示例


前言

Java Stream API借助於Lambda表達式,為Collection操作提供了一個新的選擇。如果使用得當,可以極大地提高編程效率和代碼可讀性。

本文將介紹Stream API包含的方法,並通過示例詳細展示其用法。


一、Stream特點

Stream不是集合元素,它不是數據結構也不保存數據,而更像一個高級版本的迭代器(Iterator)。Stream操作可以像鏈條一樣排列,形成Stream Pipeline,即鏈式操作。

Stream Pipeline由數據源的零或多個中間(Intermediate)操作和一個終端(Terminal)操作組成。中間操作都以某種方式進行流數據轉換,將一個流轉換為另一個流,轉換后元素類型可能與輸入流相同或不同,例如將元素按函數映射到其他類型或過濾掉不滿足條件的元素。 終端操作對流執行最終計算,例如將其元素存儲到集合中、遍歷打印元素等。

Stream特點:

  • 無存儲。Stream不是一種數據結構,也不保存數據,數據源可以是一個數組,Java容器或I/O Channel等。

  • 為函數式編程而生。對Stream的任何修改都不會修改數據源,例如對Stream過濾操作不會刪除被過濾的元素,而是產生一個不包含被過濾元素的新Stream。

  • 惰性執行。Stream上的中間操作並不會立即執行,只有等到用戶真正需要結果時才會執行。

  • 一次消費。Stream只能被“消費”一次,一旦遍歷過就會失效,就像容器的迭代器那樣,想要再次遍歷必須重新生成。

注意:沒有終端操作的流管道是靜默無操作的,所以不要忘記包含一個終端操作。

二、用法示例

以下將基於《Java 8 Optional類使用的實踐經驗》一文中的Person類,展示Stream API的用法。考慮到代碼簡潔度,示例中盡量使用方法引用。

2.1 Stream創建

2.1.1 通過參數序列創建Stream

對於可變參數序列,通過Stream.of()創建Stream,而不必先創建Array再創建Stream。

IntStream stream = IntStream.of(10, 20, 30, 40, 50); // 不要使用Stream<Integer>
Stream<String> colorStream = Stream.of("Red", "Pink", "Purple");
Stream<Person> personStream = Stream.of(
        new Person("mike", "male", 10),
        new Person("lucy", "female", 4),
        new Person("jason", "male", 5)
);

2.1.2 通過數組創建Stream

不用區分基礎數據類型,但參數只能是數組。

int[] intNumbers = {10, 20, 30, 40, 50};
IntStream stream = IntStream.of(intNumbers);

2.1.3 通過集合(Collection子類)創建Stream

調用parallelStream()或stream().parallel()方法可創建並行Stream。

Stream<Integer> numberStream = Arrays.asList(10, 20, 30, 40, 50).stream();

2.1.4 通過生成器創建Stream

· 通常用於隨機數、元素滿足固定規則的Stream,或用於生成海量測試數據的場景。

· 應配合limit()、filter()使用,以控制Stream大小,否則stream長度無限。

Stream.generate(Math::random).limit(10)
Stream.generate(() -> (int) (System.nanoTime() % 100)).limit(5)

2.1.5 通過iterate創建Stream

· 重復對給定種子值(seed)調用指定的函數來創建Stream,其元素為seed, f(seed), f(f(seed))...無限循環。

· 通常用於隨機數、元素滿足固定規則的Stream,或用於生成海量測試數據的場景。

· 應配合limit()、filter()使用,以控制Stream大小,否則stream長度無限。

// 按行依次輸出:0、5、10、15、20
Stream.iterate(0, n -> n + 5).limit(5).forEach(System.out::println);

2.1.6 通過區間創建整數序列Stream

用於IntStream、LongStream,range()不包含尾元素,rangeClosed()包含尾元素。

LongStream longRange = LongStream.range(-100L, 100L); // 生成[-100, 100)區間的元素序列

2.1.7 通過IO方式創建Stream

· 適用於從文本文件中逐行讀取數據、遍歷文件目錄等場景。

· 通常配合try ... with resources語法使用,以安全而簡潔地關閉資源。

try (Stream<String> lines = Files.lines(Paths.get("./file.txt"), StandardCharsets.UTF_8)) {
            // 跳過第一行,輸出第2~4共計三行
            lines.skip(1).limit(3).forEach(System.out::println);
        } catch (IOException e){
            System.out.println("Oops!");
        }

2.2 Stream操作

常見的操作可以歸類如下:

Intermediate:Stream經過此類操作后,結果仍為Stream

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

Terminal:Stream里包含的內容按照某種算法匯聚為一個值

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

基本的Stream用法格式為Stream.Intermediate.Terminal(SIT)Java8特性詳解 lambda表達式 Stream以圖示形式直觀描述了這種格式及若干Intermediate操作。

本節主要介紹常用操作及代碼示例。為便於演示,首先定義如下集合對象:

List<Person> persons = Arrays.asList(
        new Person("mike", "male", 10).setLocation("China", "Nanjing"),
        new Person("lucy", "female", 4),
        new Person("jason", "male", 5).setLocation("China", "Xian")
);

2.2.1 map + sum + filter + reduce

只有IntStream、LongStream和DoubleStream支持sum()方法。

// 計算年齡總和:totalAge = 19
int totalAge = persons.stream().mapToInt(Person::getAge).sum();
// 並行計算年齡總和,此處不建議使用reduce(針對復雜的規約操作)
persons.stream().parallel().mapToInt(Person::getAge).reduce(0, Integer::sum);
// 計算男生年齡總和:totalAge = 15
persons.stream().filter(person -> "male".equals(person.getGender())).mapToInt(Person::getAge).sum();

2.2.2 map + average + max

average()返回OptionalDouble,max()/min()返回OptionalInt或Optional

// 計算年齡均值,輸出6.333333333333333
persons.stream().mapToInt(Person::getAge).average().ifPresent(System.out::println);
// 計算字典序最大的人名,輸出mike
persons.stream().map(Person::getName).max(String::compareToIgnoreCase).ifPresent(System.out::println);

2.2.3 map + forEach

// 輸出每個學生姓名的大寫形式,按行輸出:MIKE、LUCY、JASON
persons.stream()
        .map(Person::getName) // 將Person對象映射為String(姓名)
        .map(String::toUpperCase) // 將姓名轉換大寫
        .forEach(System.out::println); // 按行輸出List元素

2.2.4 collect

· collect操作可將Stream元素轉換為不同的數據類型,如字符串、List、Set和Map等。

· Java 8通過Collectors類支持各種內置收集器,以簡化collect操作。

// 得到字符串:Colors: Red&Pink&Purple!
colorStream.collect(Collectors.joining("&", "Colors: ", "!"));
// 得到ArrayList,元素為:Red, Pink, Purple
// 注意,Stream轉換為數組的格式形如stream.toArray(String[]::new)
colorStream.collect(Collectors.toList());
// 得到HashSet,元素為:Red, Pink, Purple
colorStream.collect(Collectors.toSet());
// 得到LinkedList,toCollection()用於指定集合類型
colorStream.collect(Collectors.toCollection(LinkedList::new));
// 得到HashMap,{mike=Person{name='mike'}, jason=Person{name='jason'}, lucy=Person{name='lucy'}}
personStream.collect(Collectors.toMap(Person::getName, Function.identity()));

collect收集器還提供summingInt()、averagingInt()和summarizingInt()等計算方法。

// 返回流中整數屬性求和,即19
persons.stream().collect(Collectors.summingInt(Person::getAge))
// 計算流中Integer屬性的平均值,即6.333333333333333
persons.stream().collect(Collectors.averagingInt(Person::getAge))
// 收集流中Integer屬性的統計值,即IntSummaryStatistics{count=3, sum=19, min=4, average=6.333333, max=10}
persons.stream().collect(Collectors.summarizingInt(Person::getAge))

2.2.5 sorted + collect

// 按照年齡升序排序:sortedpersons = [Person{name='lucy'}, Person{name='jason'}, Person{name='mike'}]
List<Person> sortedPersons = persons.stream()
        .sorted(Comparator.comparingInt(Person::getAge)) // 按照年齡排序
        .collect(Collectors.toList()); // 匯聚為一個List對象
// 按照姓名長度升序排序,按行輸出:mike: 4、lucy: 4、jason: 5
persons.stream()
        .sorted(Comparator.comparingInt(p -> p.getName().length()))
        .map(Person::getName)
        .map(name -> name + ": " + name.length())
        .forEach(System.out::println);

2.2.6 map + anyMatch

// 判斷是否存在名為jason的人:existed = true
boolean existed = persons.stream()
        .map(Person::getName)
        .anyMatch("jason"::equals); // 任意匹配項是否存在

2.2.7 groupingBy + map + reduce

// 將所有人按照性別分組並計數,輸出:{female=1, male=2}
Map<String, Long> groupBySex = persons.stream().collect(groupingBy(Person::getGender, counting()));
System.out.println(groupBySex);
// 將所有人按照性別分組並計算各組最大年齡,輸出:Person{name='mike'}
Map<String, Optional<Person>> groupBySexAge = persons.stream().collect(
        groupingBy(Person::getGender, maxBy(Comparator.comparingInt(Person::getAge))));
System.out.println(groupBySexAge.get("male").get());
// 將所有人按照性別分組,按行輸出:female: lucy、male: mike,jason
persons.stream().collect(groupingBy(Person::getGender))
        .forEach((k, v) ->System.out.println(k + ": "
                + v.stream().map(Person::getName)
                .reduce((x, y) -> x + "," + y).get()));

注意,本例采用import static java.util.stream.Collectors.*;這種靜態導入的方式簡化Collectors.groupingBy()的調用,代碼更簡潔易讀。此外,不推薦示例中forEach()的用法。

2.2.8 maps + collect

// 計算身高比例分布:agePercentages = [52.63%, 21.05%, 26.32%]
List<String> agePercentages = persons.stream()
        .mapToInt(Person::getAge) // 將Person對象映射為年齡整型值
        .mapToDouble(age -> age / (double)totalAge * 100) // 計算年齡比例
        .mapToObj(new DecimalFormat("##.00")::format) // DoubleStream -> Stream<String>
        .map(percentage -> percentage + "%") // 添加百分比后綴

        .collect(Collectors.toList());
// 若元素數目較多,可先定義formator = new DecimalFormat("##.00"),再調用mapToObj(formator::format)

2.2.9 flatMap

flatMap()將Stream中的集合實例內的元素全部拍平鋪開,形成一個新的Stream,從而到達合並的效果。

// 傳統寫法(注意兩層循環)
private static int countPrefix(List<List<String>> nested, String prefix) {
    int count = 0;
    for (List<String> element : nested) {
        if (element != null) {
            for (String str : element) {
                if (str.startsWith(prefix)) {
                    count++;
                }
            }
        }
    }
    return count;
}
// Stream寫法
private static int countPrefixWithStream(List<List<String>> nested, String prefix) {
    return (int) nested.stream()
            .filter(Objects::nonNull)
            .flatMap(Collection::stream)
            .filter(str -> str.startsWith(prefix))
            .count();
}

List<List<String>> lists = Arrays.asList(
        Arrays.asList("Jame"),
        Arrays.asList("Mike", "Jason"),
        Arrays.asList("Jean", "Lucy", "Beth")
);
System.out.println("以J開頭的人名數:" + countPrefixWithStream(lists, "J"));

三、規則總結

使用Stream時,需注意以下規則:

  1. 避免重用Stream。

    Java 8 Stream一旦被Terminal操作消費,將不能夠再使用,必須為待執行的每個Terminal操作創建新的Stream鏈。在實際開發時,將共用的Stream實例定義為成員變量時,尤其容易犯錯。

    重用Stream將報告stream has already been operated upon or closed的異常。

    若需要多次調用,可利用Stream Supplier實例來創建已構建所有中間操作的新Stream。例如:

    Supplier<Stream<String>> streamSupplier =
            () -> Stream.of("d2", "a2", "b1", "b3", "c")
                    .filter(s -> s.startsWith("a"));
    streamSupplier.get().anyMatch(s -> true);   // 每次調用get()構造一個新stream
    streamSupplier.get().noneMatch(s -> true);
    

    注意,anyMatch()方法接受Predicate引元,通常無需使用filter,此處僅為示例方便。

  2. 避免創建無限流。

    通過iterate或生成器創建Stream時,應配合limit()使用,以控制Stream大小。

    distinct()limit()共用時,應特別注意去重后元素數目是否滿足limit限制。例如:

    IntStream.iterate(0, i -> (i + 1) % 2) // 生成0和1的整數序列   
        .distinct() // 去重后為0和1兩個元素   
        .limit(10) // limit(10)限制得不到滿足,從而變成無限流   
        .forEach(System.out::println);
    
  3. 注意Stream操作順序,盡可能提前通過filter()等操作降低數據規模

    以下面一段簡單的代碼為例:

    Stream.of("a1", "b2", "c3", "d4", "e5").map(s -> {   
        System.out.println("map: " + s);
        return s.toUpperCase();
    }).filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("A");
    }).forEach(s -> System.out.println("forEach: " + s));
    

    運行輸出如下:

    map: a1
    filter: A1
    forEach: A1
    map: b2
    filter: B2
    map: c3
    filter: C3
    map: d4
    filter: D4
    map: e5
    filter: E5
    

    可見,流中的每個字符串都被調用5次map()filter(),而forEach()只調用一次。

    再改變操作順序,將filter()移到Stream操作鏈的頭部:

    Stream.of("a1", "b2", "c3", "d4", "e5").filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a");
    }).map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    }).forEach(s -> System.out.println("forEach: " + s));
    

    運行輸出如下:

    filter: a1
    map: a1
    forEach: A1
    filter: b2
    filter: c3
    filter: d4
    filter: e5
    

    可見,map()只被調用一次。雖然Stream惰性計算的特性使得操作順序並不影響最終結果,但合理地安排順序可以減少實際執行次數。數據規模較大時,性能會有較明顯的提升。

  4. 注意Stream操作的副作用。

    大多數Stream操作必須是無干擾、無狀態的。

    “無干擾”是指在流操作的過程中,不去修改流的底層數據源。例如,遍歷流時不能通過添加或刪除集合中的元素來修改集合。

    “無狀態”是指Lambda表達式的結果不能依賴於流管道執行過程中,可能發生變化的外部作用域的任何可變變量或狀態。

    以下代碼試圖在操作流時添加和移出元素,運行時均會拋出java.util.ConcurrentModificationException異常:

    List<String> strings = new ArrayList<>(Arrays.asList("one", "two"));
    String concatenatedString = strings.stream()
            // 不要這樣做,干擾發生在這里
            .peek(s -> strings.add("three"))
            .reduce((a, b) -> a + " " + b)
            .get();
    List<Integer> list = IntStream.range(0, 10)
            .boxed() // 流元素裝箱為Integer類型
            .collect(Collectors.toCollection(ArrayList::new));
    list.stream()
            .peek(list::remove) // 不要這樣做,干擾發生在這里
            .forEach(System.out::println);
    

    以下代碼對並行Stream使用了有狀態的Lambda表達式:

    Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8};
    List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(intArray));
    List<Integer> parallelStorage = new ArrayList<>();
    //List<Integer> parallelStorage = Collections.synchronizedList(new ArrayList<>());
    listOfIntegers.parallelStream()
            // 不要這樣做,此處使用了有狀態的Lambda表達式
            .map(e -> { parallelStorage.add(e); return e; })
            .forEachOrdered(e -> System.out.print(e + " "));
    System.out.println(": 1st");
    parallelStorage.stream().forEachOrdered(e -> System.out.print(e + " "));
    System.out.println(": 2nd");
    

    運行結果可能出現以下幾種:

    // 並行執行流時,map()添加元素的順序和隨后的forEachOrdered()元素打印順序不同
    1 2 3 4 5 6 7 8 : 1st
    1 6 3 2 7 8 5 4 : 2nd
    // 多線程可能同時讀取到相同的下標n進行賦值,導致元素數量少於預期(采用synchronizedList可解決該問題)
    1 2 3 4 5 6 7 8 : 1st
    1 5 8 3 6 : 2nd
    

    《Effective Java 第三版》中指出,不要嘗試並行化流管道,除非有充分的理由相信它將保持計算的正確性並提高其速度。 不恰當地並行化流的代價可能是程序失敗或性能災難。

  5. 避免過度使用Stream,否則可能使代碼難以閱讀和維護。

    常見的問題是Lambda表達式過長,可通過抽取方法等手段,盡量將Lambda表達式限制在幾行之內。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM