90、Java Stream API 全面解析

Java Stream API 全面解析

1. 过滤操作

在 Java 的 Stream API 中,过滤操作是非常常见且实用的。由于 filter() 或其他中间操作会返回一个新的流,所以可以对已过滤的流再次进行过滤。例如,要得到一个仅包含大于 5 的奇数的流,可以这样做:

oddVals = myList.stream().filter((n) -> (n % 2) == 1)
                         .filter((n) -> n > 5);

这里,两个过滤器都使用了 lambda 表达式。

2. 归约操作

2.1 特殊归约方法

min() max() 这样的方法属于终端操作,它们会根据流中的元素返回一个结果。在 Stream API 中,它们被称为特殊归约操作,因为它们将流归约为单个值,分别是最小值和最大值。除了这两个方法,还有 count() 方法用于统计流中元素的数量。

2.2 reduce() 方法

Stream API 提供了 reduce() 方法来实现更通用的归约操作。 reduce() 有三个版本,我们先介绍前两个:

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identityVal, BinaryOperator<T> accumulator)

第一个版本返回一个 Optional 对象,包含归约结果;第二个版本直接返回流元素类型的对象。在这两个版本中, accumulator 是一个对两个值进行操作并产生结果的函数。在第二个版本中, identityVal 是一个标识值,对于加法操作,标识值是 0;对于乘法操作,标识值是 1。

BinaryOperator java.util.function 中声明的一个函数式接口,它扩展了 BiFunction 接口。 BiFunction 定义了抽象方法 R apply(T val, U val2) ,当 BinaryOperator 扩展 BiFunction 时,所有类型参数指定为相同类型,即 T apply(T val, T val2) 。在 reduce() 方法中, val 包含上一次的结果, val2 包含下一个元素。

2.3 累加器操作的约束

累加器操作必须满足以下三个约束:
- 无状态 :操作不依赖任何状态信息,每个元素独立处理。
- 不干扰 :操作不会修改数据源。
- 可结合 :操作满足结合律,例如 (10 * 2) * 7 10 * (2 * 7) 结果相同。

2.4 reduce() 方法示例

// Demonstrate the reduce() method.
import java.util.*;
import java.util.stream.*;

class StreamDemo2 {
  public static void main(String[] args) {
    // Create a list of Integer values.
    ArrayList<Integer> myList = new ArrayList<>( );
    myList.add(7);
    myList.add(18);
    myList.add(10);
    myList.add(24);
    myList.add(17);
    myList.add(5);

    // Two ways to obtain the integer product of the elements
    // in myList by use of reduce().
    Optional<Integer> productObj = myList.stream().reduce((a,b) -> a*b);
    if(productObj.isPresent())
      System.out.println("Product as Optional: " + productObj.get());

    int product = myList.stream().reduce(1, (a,b) -> a*b);
    System.out.println("Product as int: " + product);
  }
}

输出结果:

Product as Optional: 2570400
Product as int: 2570400

在这个程序中,第一个 reduce() 版本使用 lambda 表达式计算元素的乘积,结果存储在 Optional 对象中;第二个版本明确指定了标识值 1,直接返回 Integer 类型的结果。

2.5 更复杂的归约操作

还可以使用 reduce() 方法计算偶数元素的乘积:

int evenProduct = myList.stream().reduce(1, (a,b) -> {
                    if(b%2 == 0) return a*b; else return a;
                  });

3. 使用并行流

3.1 并行流的优势

并行执行代码可以显著提高性能,但并行编程复杂且容易出错。Stream API 提供了轻松可靠地并行处理某些操作的能力。

3.2 获取并行流的方法

  • 使用 Collection 接口的 parallelStream() 方法。
  • 对顺序流调用 BaseStream 接口的 parallel() 方法:
S parallel()

如果对已经是并行的流调用 parallel() 方法,将返回原流。

3.3 并行流的操作

一旦获得并行流,在环境支持的情况下,流上的操作可以并行执行。例如,将前面程序中的第一个 reduce() 操作并行化:

Optional<Integer> productObj = myList.parallelStream().reduce((a,b) -> a*b);

结果与顺序流相同,但乘法操作可以在不同线程中进行。

3.4 并行流的 reduce() 方法

并行流使用的 reduce() 方法版本:

<U> U reduce(U identityVal, BiFunction<U, ? super T, U> accumulator,
                     BinaryOperator<U> combiner)

这里, combiner 定义了如何合并累加器函数产生的两个部分结果。例如:

int parallelProduct = myList.parallelStream().reduce(1, (a,b) -> a*b,
                                                       (a,b) -> a*b);

在某些情况下,累加器和合并器的操作可能不同。例如,计算列表中元素平方根的乘积:

// Demonstrate the use of a combiner with reduce()
import java.util.*;
import java.util.stream.*;

class StreamDemo3 {
  public static void main(String[] args) {
    ArrayList<Double> myList = new ArrayList<>( );
    myList.add(7.0);
    myList.add(18.0);
    myList.add(10.0);
    myList.add(24.0);
    myList.add(17.0);
    myList.add(5.0);

    double productOfSqrRoots = myList.parallelStream().reduce(
                                     1.0,
                                     (a,b) -> a * Math.sqrt(b),
                                     (a,b) -> a * b
                                );

    System.out.println("Product of square roots: " + productOfSqrRoots);
  }
}

如果使用错误的方式计算平方根的乘积:

// This won't work.
double productOfSqrRoots2 = myList.parallelStream().reduce(
                                   1.0,
                                   (a,b) -> a * Math.sqrt(b));

会导致错误,因为在合并部分结果时,应该相乘的是部分结果本身,而不是它们的平方根。

3.5 并行流和顺序流的切换

可以使用 BaseStream 接口的 sequential() 方法将并行流切换为顺序流:

S sequential()

流可以根据需要在并行和顺序之间切换。

3.6 流元素的顺序

流可以是有序的或无序的。当使用并行流时,允许流无序有时可以提高性能。可以使用 BaseStream 接口的 unordered() 方法指定流为无序:

S unordered()

另外, forEach() 方法可能不会保留并行流的顺序,如果需要保留顺序,可以使用 forEachOrdered() 方法。

4. 映射操作

4.1 map() 方法

map() 方法用于将一个流的元素映射到另一个流。其通用版本如下:

<R> Stream<R> map(Function<? super T, ? extends R> mapFunc)

这里, R 是新流的元素类型, T 是调用流的元素类型, mapFunc 是实现映射的 Function 实例。 map() 方法是一个中间方法,返回一个新的流。

Function java.util.function 中声明的函数式接口,定义了抽象方法 R apply(T val) ,用于对对象进行映射并返回结果。

4.2 map() 方法示例

// Map one stream to another.
import java.util.*;
import java.util.stream.*;

class StreamDemo4 {
  public static void main(String[] args) {
    ArrayList<Double> myList = new ArrayList<>( );
    myList.add(7.0);
    myList.add(18.0);
    myList.add(10.0);
    myList.add(24.0);
    myList.add(17.0);
    myList.add(5.0);

    // Map the square root of the elements in myList to a new stream.
    Stream<Double> sqrtRootStrm = myList.stream().map((a) -> Math.sqrt(a));

    // Find the product of the square roots.
    double productOfSqrRoots = sqrtRootStrm.reduce(1.0, (a,b) -> a*b);

    System.out.println("Product of square roots is " + productOfSqrRoots);
  }
}

这个程序先将列表中元素的平方根映射到一个新流,然后使用 reduce() 方法计算平方根的乘积。

4.3 选择字段映射

还可以使用 map() 方法创建一个只包含原流中部分字段的新流。例如:

// Use map() to create a new stream that contains only
// selected aspects of the original stream.
import java.util.*;
import java.util.stream.*;

class NamePhoneEmail {
  String name;
  String phonenum;
  String email;

  NamePhoneEmail(String n, String p, String e) {
    name = n;
    phonenum = p;
    email = e;
  }
}

class NamePhone {
  String name;
  String phonenum;

  NamePhone(String n, String p) {
    name = n;
    phonenum = p;
  }
}

class StreamDemo5 {
  public static void main(String[] args) {
    ArrayList<NamePhoneEmail> myList = new ArrayList<>( );
    myList.add(new NamePhoneEmail("Larry", "555-5555",
                                  "Larry@HerbSchildt.com"));
    myList.add(new NamePhoneEmail("James", "555-4444",
                                  "James@HerbSchildt.com"));
    myList.add(new NamePhoneEmail("Mary", "555-3333",
                                  "Mary@HerbSchildt.com"));

    System.out.println("Original values in myList: ");
    myList.stream().forEach( (a) -> {
      System.out.println(a.name + " " + a.phonenum + " " + a.email);
    });
    System.out.println();

    // Map just the names and phone numbers to a new stream.
    Stream<NamePhone> nameAndPhone = myList.stream().map(
                                    (a) -> new NamePhone(a.name,a.phonenum)
                                   );

    System.out.println("List of names and phone numbers: ");
    nameAndPhone.forEach( (a) -> {
      System.out.println(a.name + " " + a.phonenum);
    });
  }
}

这个程序将 NamePhoneEmail 对象的姓名和电话号码映射到一个新的 NamePhone 对象流中。

4.4 管道操作

可以将多个中间操作组合在一起创建强大的操作。例如,过滤出姓名为 “James” 的元素,并映射其姓名和电话号码:

Stream<NamePhone> nameAndPhone = myList.stream().
                              filter((a) -> a.name.equals("James")).
                              map((a) -> new NamePhone(a.name,a.phonenum));

4.5 其他 map() 版本

除了通用的 map() 方法,还有三个返回基本类型流的版本:

IntStream mapToInt(ToIntFunction<? super T> mapFunc)
LongStream mapToLong(ToLongFunction<? super T> mapFunc)
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapFunc)

例如,将 ArrayList 中元素的天花板值映射到一个 IntStream 中:

// Map a Stream to an IntStream.
import java.util.*;
import java.util.stream.*;

class StreamDemo6 {
  public static void main(String[] args) {
    ArrayList<Double> myList = new ArrayList<>( );
    myList.add(1.1);
    myList.add(3.6);
    myList.add(9.2);
    myList.add(4.7);
    myList.add(12.1);
    myList.add(5.0);

    System.out.print("Original values in myList: ");
    myList.stream().forEach( (a) -> {
      System.out.print(a + " ");
    });
    System.out.println();

    // Map the ceiling of the elements in myList to an IntStream.
    IntStream cStrm = myList.stream().mapToInt((a) -> (int) Math.ceil(a));

    System.out.print("The ceilings of the values in myList: ");
    cStrm.forEach( (a) -> {
      System.out.print(a + " ");
    });
  }
}

4.6 扁平映射方法

Stream API 还提供了支持扁平映射的方法,如 flatMap() flatMapToInt() flatMapToLong() flatMapToDouble() ,用于处理原流中每个元素映射到多个元素的情况。

5. 收集操作

5.1 collect() 方法的第一种形式

可以使用 collect() 方法将流转换为集合。第一种形式如下:

<R, A> R collect(Collector<? super T, A, R> collectorFunc)

这里, R 是结果的类型, T 是调用流的元素类型, A 是内部累积类型, collectorFunc 指定收集过程。 collect() 是一个终端操作。

5.2 预定义收集器

Collectors 类提供了一些预定义的收集器,如 toList() toSet()

static <T> Collector<T, ?, List<T>> toList()
static <T> Collector<T, ?, Set<T>> toSet()

例如,将姓名和电话号码收集到一个 List 和一个 Set 中:

// Use collect() to create a List and a Set from a stream.
import java.util.*;
import java.util.stream.*;

class NamePhoneEmail {
  String name;
  String phonenum;
  String email;

  NamePhoneEmail(String n, String p, String e) {
    name = n;
    phonenum = p;
    email = e;
  }
}

class NamePhone {
  String name;
  String phonenum;

  NamePhone(String n, String p) {
    name = n;
    phonenum = p;
  }
}

class StreamDemo7 {
  public static void main(String[] args) {
    ArrayList<NamePhoneEmail> myList = new ArrayList<>( );
    myList.add(new NamePhoneEmail("Larry", "555-5555",
                                  "Larry@HerbSchildt.com"));
    myList.add(new NamePhoneEmail("James", "555-4444",
                                  "James@HerbSchildt.com"));
    myList.add(new NamePhoneEmail("Mary", "555-3333",
                                  "Mary@HerbSchildt.com"));

    // Map just the names and phone numbers to a new stream.
    Stream<NamePhone> nameAndPhone = myList.stream().map(
                                     (a) -> new NamePhone(a.name,a.phonenum)
                                   );

    // Use collect to create a List of the names and phone numbers.
    List<NamePhone> npList = nameAndPhone.collect(Collectors.toList());

    System.out.println("Names and phone numbers in a List:");
    for(NamePhone e : npList)
      System.out.println(e.name + ": " + e.phonenum);

    // Obtain another mapping of the names and phone numbers.
    nameAndPhone = myList.stream().map(
                                     (a) -> new NamePhone(a.name,a.phonenum)
                                    );

    // Now, create a Set by use of collect().
    Set<NamePhone> npSet = nameAndPhone.collect(Collectors.toSet());

    System.out.println("\nNames and phone numbers in a Set:");
    for(NamePhone e : npSet)
      System.out.println(e.name + ": " + e.phonenum);
  }
}

5.3 collect() 方法的第二种形式

第二种形式的 collect() 方法提供了更多对收集过程的控制:

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

这里, target 指定结果对象的创建方式, accumulator 函数将元素添加到结果中, combiner 合并两个部分结果。例如,使用 LinkedList 作为结果集合:

LinkedList<NamePhone> npList = nameAndPhone.collect(
                                () -> new LinkedList<>(),
                                (list, element) -> list.add(element),
                                (listA,listB ) -> listA.addAll(listB));

也可以使用方法引用和构造函数引用:

HashSet<NamePhone> npSet = nameAndPhone.collect(HashSet::new,
                                                HashSet::add,
                                                HashSet::addAll);

在 Stream API 中, collect() 方法执行的是可变归约操作,因为归约结果是一个可变的存储对象,如集合。

总结

通过本文的介绍,我们了解了 Java Stream API 的过滤、归约、并行流、映射和收集等操作。这些操作提供了强大而灵活的方式来处理集合数据,提高了代码的可读性和性能。在实际开发中,可以根据具体需求选择合适的操作来处理数据。

下面是一个简单的流程图,展示了从集合到流,再到集合的转换过程:

graph LR
    A[集合] --> B[流]
    B --> C{操作}
    C -->|过滤| D(过滤后的流)
    C -->|映射| E(映射后的流)
    C -->|归约| F(归约结果)
    C -->|收集| G[集合]
    D --> C
    E --> C

同时,我们还可以用表格总结一下 Stream API 中的主要操作:
| 操作类型 | 方法 | 描述 |
| ---- | ---- | ---- |
| 过滤 | filter() | 根据条件过滤流中的元素 |
| 归约 | reduce() | 将流归约为单个值 |
| 并行流操作 | parallelStream() parallel() | 实现流的并行处理 |
| 映射 | map() mapToInt() 等 | 将流的元素映射到另一个流 |
| 收集 | collect() | 将流转换为集合 |

通过这些操作和方法,我们可以更加高效地处理和操作数据。

6. 操作总结与对比

6.1 中间操作与终端操作对比

在 Java Stream API 中,操作可分为中间操作和终端操作,下面用表格进行对比:
| 操作类型 | 特点 | 示例方法 |
| ---- | ---- | ---- |
| 中间操作 | 返回一个新的流,可进行链式调用,不会触发流的执行 | filter() map() sorted() |
| 终端操作 | 触发流的执行,产生一个结果或副作用,流执行后不能再使用 | reduce() collect() forEach() |

6.2 不同归约操作对比

不同的归约操作有各自的特点和适用场景,如下表所示:
| 归约方法 | 特点 | 适用场景 |
| ---- | ---- | ---- |
| min() max() count() | 特殊归约,功能固定,直接返回结果 | 简单的统计,如求最大值、最小值、元素数量 |
| reduce(BinaryOperator<T> accumulator) | 返回 Optional 对象,适用于无初始值的归约 | 不确定流中是否有元素时的归约 |
| reduce(T identityVal, BinaryOperator<T> accumulator) | 有初始值,直接返回结果类型 | 明确需要初始值的归约 |
| <U> U reduce(U identityVal, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner) | 适用于并行流,可指定合并部分结果的方式 | 并行流的归约,需要自定义合并逻辑 |

6.3 不同映射操作对比

映射操作也有多种形式,满足不同的需求,对比表格如下:
| 映射方法 | 返回类型 | 适用场景 |
| ---- | ---- | ---- |
| <R> Stream<R> map(Function<? super T, ? extends R> mapFunc) | 通用流 | 一般的元素映射,可改变元素类型 |
| IntStream mapToInt(ToIntFunction<? super T> mapFunc) | 整型流 | 映射为整型元素的场景 |
| LongStream mapToLong(ToLongFunction<? super T> mapFunc) | 长整型流 | 映射为长整型元素的场景 |
| DoubleStream mapToDouble(ToDoubleFunction<? super T> mapFunc) | 双精度浮点型流 | 映射为双精度浮点型元素的场景 |

7. 实际应用案例

7.1 数据筛选与统计

假设我们有一个学生信息列表,包含学生的姓名、年龄和成绩,我们要筛选出年龄大于 18 岁且成绩大于 80 分的学生,并统计他们的数量。

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

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

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

public class StudentStatistics {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("Alice", 20, 85));
        students.add(new Student("Bob", 17, 70));
        students.add(new Student("Charlie", 19, 90));

        long count = students.stream()
               .filter(student -> student.age > 18 && student.score > 80)
               .count();

        System.out.println("年龄大于 18 岁且成绩大于 80 分的学生数量: " + count);
    }
}

7.2 数据转换与汇总

假设有一个商品列表,包含商品的名称和价格,我们要将商品价格转换为折扣后的价格,并计算所有商品的总价格。

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

class Product {
    String name;
    double price;

    Product(String name, double price) {
        this.name = name;
        this.price = price;
    }
}

public class ProductDiscount {
    public static void main(String[] args) {
        List<Product> products = new ArrayList<>();
        products.add(new Product("Apple", 10.0));
        products.add(new Product("Banana", 5.0));
        products.add(new Product("Orange", 8.0));

        double totalPrice = products.stream()
               .map(product -> product.price * 0.8) // 8 折优惠
               .reduce(0.0, Double::sum);

        System.out.println("所有商品折扣后的总价格: " + totalPrice);
    }
}

7.3 并行处理大数据集

当处理大数据集时,并行流可以显著提高性能。例如,计算一个包含大量整数的列表中所有元素的乘积。

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

public class ParallelProduct {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        for (int i = 1; i <= 1000; i++) {
            numbers.add(i);
        }

        Optional<Integer> product = numbers.parallelStream()
               .reduce((a, b) -> a * b);

        if (product.isPresent()) {
            System.out.println("所有元素的乘积: " + product.get());
        }
    }
}

8. 注意事项与最佳实践

8.1 并行流使用注意事项

  • 数据独立性 :并行流中的操作必须是无状态、不干扰且可结合的,确保每个元素的处理相互独立,避免数据竞争和不一致的结果。
  • 性能评估 :并非所有场景下并行流都能提高性能,在数据量较小或操作本身简单时,并行流的开销可能会超过性能提升,因此需要进行性能测试和评估。
  • 顺序问题 :并行流可能不保证元素的顺序,如果需要保留顺序,应使用 forEachOrdered() 方法或考虑使用顺序流。

8.2 流操作的最佳实践

  • 链式调用 :充分利用流的链式调用特性,将多个中间操作和终端操作组合在一起,提高代码的可读性和简洁性。
  • 及时关闭流 :虽然 Java 的垃圾回收机制会处理流的关闭,但在某些情况下,如使用外部资源(如文件流)时,应及时关闭流以避免资源泄漏。
  • 合理选择操作 :根据具体需求选择合适的流操作,如过滤、映射、归约等,避免不必要的操作和性能损耗。

8.3 代码可读性与维护性

  • 使用有意义的变量名和 lambda 表达式 :在编写流操作代码时,使用清晰、有意义的变量名和 lambda 表达式,提高代码的可读性和可维护性。
  • 添加注释 :对于复杂的流操作,添加必要的注释来解释操作的目的和逻辑,方便其他开发者理解和维护代码。

9. 未来发展趋势

9.1 更强大的流操作支持

随着 Java 的不断发展,未来可能会在 Stream API 中添加更多强大的操作方法,以满足更复杂的数据处理需求。例如,支持更高级的聚合操作、复杂的过滤条件和更灵活的映射规则。

9.2 与其他技术的集成

Stream API 可能会与其他 Java 技术(如 JavaFX、Spring 框架等)更紧密地集成,为开发者提供更便捷的开发体验。同时,也可能会与大数据处理框架(如 Apache Hadoop、Spark 等)进行集成,实现更高效的大数据处理。

9.3 性能优化

未来的 Java 版本可能会对 Stream API 的性能进行进一步优化,减少并行流的开销,提高流操作的执行效率,特别是在处理大规模数据时。

总结

Java Stream API 为我们提供了一种强大而灵活的方式来处理集合数据。通过过滤、归约、并行流、映射和收集等操作,我们可以高效地处理和转换数据,提高代码的可读性和性能。在使用 Stream API 时,需要注意并行流的使用条件、流操作的最佳实践以及代码的可读性和维护性。随着 Java 技术的不断发展,Stream API 也将不断完善和扩展,为开发者带来更多的便利和可能性。

下面是一个流程图,展示了在实际应用中使用 Stream API 的一般流程:

graph LR
    A[获取集合数据] --> B[创建流]
    B --> C{选择操作类型}
    C -->|过滤| D(过滤操作)
    C -->|映射| E(映射操作)
    C -->|归约| F(归约操作)
    C -->|收集| G(收集操作)
    D --> H(中间结果)
    E --> H
    F --> I(最终结果)
    G --> J[转换为集合]
    H --> C

通过这个流程图,我们可以清晰地看到在实际应用中如何根据需求选择合适的流操作,以及操作之间的顺序和关系。希望本文能帮助你更好地理解和使用 Java Stream API。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值