自学内容网 自学内容网

Java 入门指南:Java 8 新特性 —— Stream 流

Java Stream

Java Stream 是 Java 8 引入的一个新的API,它提供了一种函数式编程的方式来处理集合数据。Stream 可以看作是一系列支持高效的、函数式操作的元素序列。

通过使用Stream,可以对集合进行 过滤映射排序查找 等操作,而不需要显示地使用循环或者迭代器。Stream API 使用一种类似于 SQL 查询的方式来操作数据,这样可以使代码更加简洁、可读性更强。

Java 8 新增的 Stream 是为了解放程序员操作集合(Collection)时的生产力,之所以能解放,很大一部分原因可以归功于同时出现的 Java Lambda 表达式——极大的提高了编程效率和程序可读性。

操作类型

Stream 可以分为中间操作和结束操作。

  • 中间操作包括过滤、映射、排序等操作,它们会返回一个 Stream 对象,可以进行多次连续操作(链式操作)

  • 结束操作会执行实际的计算,例如聚合、收集结果等。

使用 Stream 的一般流程包括从数据源(如集合、数组)获取一个 Stream 对象,通过链式操作对 Stream 进行中间操作,最后执行一个结束操作将结果返回。

中间操作不会立即执行,只有等到终端操作的时候,stream 才开始真正地遍历,用于映射、过滤等。通俗点说,就是一次遍历执行多个操作,性能就大大提高了。

操作过程

创建流

数组:使用 Arrays.stream() 或者 Stream.of() 创建流

查看 Stream 源码的话,会发现 of() 方法内部其实调用了 Arrays.stream() 方法。

public static<T> Stream<T> of(T... values) {
    return Arrays.stream(values);
}

集合:直接使用 stream() 方法创建流,该方法已经被添加到 Collection 接口中。

调用 parallelStream() 方法创建并发流,默认使用的是 ForkJoinPool.commonPool() 线程池。

List<Long> aList = new ArrayList<>();
Stream<Long> parallelStream = aList.parallelStream();
操作流
遍历 forEach

Java 8 引入了函数式编程的概念,并添加了 forEach 方法,以更简洁的方式进行元素遍历和操作。

forEach 方法接受一个函数式接口作为参数,它定义了对每个元素执行的操作。该函数式接口通常使用 Lambda 表达式或者方法引用来实现。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.forEach(n -> System.out.println(n));

forEach 方法是一个终端操作,它没有返回值。它可以用于执行特定的操作,例如打印元素、更新元素或者发送元素到其他系统等。

与传统的循环相比,使用 forEach 可以使代码更加简洁、可读性更强,并且可以与 Stream API 结合使用,更方便地进行函数式编程。

需要注意的是,使用 forEach 方法遍历集合时,元素的迭代顺序是根据集合的具体实现而定,对于有序集合(如 List)它会保持元素的顺序,对于无序集合(如 Set)它不保证元素的顺序。

过滤 filter

通过 filter() 方法可以从流中筛选出需要的元素

filter 方法接收一个 Predicate(谓词,Java 8 新增的一个函数式接口,接受一个输入参数返回一个布尔值结果)作为参数,Predicate 是一个函数式接口,它的方法是 test(T),作用是测试指定条件是否符合某个规则。

将一个 Lambda 表达式或方法引用作为 Predicate 参数,来定义筛选条件,过滤出符合条件的元素。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);  
          
        // 将List转换为Stream,然后应用filter方法  
        Stream<Integer> stream = numbers.stream().filter(n -> n % 2 == 0);  
          
        // 使用forEach和方法引用来打印每个偶数  
        stream.forEach(System.out::println); // 输入 2 4
映射 map

使用 map() 方法,可以将集合中的每个元素通过指定的映射函数进行转换,并返回一个新的集合。通过某种操作把一个流中的元素转化成新的流中的元素。

map 不会对原集合中的元素进行更改,而是返回一个新的集合

map 方法接收一个 Function(函数)作为参数,Function 是一个函数式接口,它的方法是 apply(T),作用是对指定的输入值执行转换操作,并返回转换后的结果。

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

T 是流中元素的类型,R 是转换后元素的类型,mapper 是一个函数,它接受一个 T 类型的参数并返回一个 R 类型的结果。

可以将一个 Lambda 表达式或方法引用作为 Function 参数,来定义元素的转换规则。

public class MapExample {  
    public static void main(String[] args) {  
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");  
  
        // 将每个名字转换为大写形式  
        List<String> upperCaseNames = names.stream()  
                .map(String::toUpperCase) // 使用方法引用作为mapper函数  
                .collect(Collectors.toList()); // 收集结果到列表中  
  
        // 输出结果  
        System.out.println(upperCaseNames); // 输出:[ALICE, BOB, CHARLIE]  
    }  
}

在这个示例中,我们有一个包含名字的字符串列表 names。我们使用 stream() 方法将列表转换为流,然后使用 map 方法将流中的每个名字转换为大写形式。这里,我们使用了方法引用 String::toUpperCase 作为 mapper 函数,它简化了 Lambda 表达式的编写。最后,我们使用 collect(Collectors.toList()) 方法将转换后的流收集到一个新的列表中,并打印出来。

匹配 match

匹配(match)操作主要用于检查流中的元素是否满足特定条件

Stream 类提供了三个方法可供进行元素匹配,它们分别是:

  • anyMatch(),只要有一个元素匹配传入的条件,就返回 true,否则返回 false。一旦找到一个不满足条件的元素,就会立即停止遍历。

  • allMatch(),只要有一个元素不匹配传入的条件,就返回 false;如果全部匹配,则返回 true。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);  
boolean anyMatchResult = numbers.stream().anyMatch(n -> n > 3);  
System.out.println("At least one number is greater than 3: " + anyMatchResult); // 输出:true
  • noneMatch(),只要有一个元素匹配传入的条件,就返回 false;如果全部不匹配,则返回 true。一旦找到一个满足条件的元素,就会立即停止遍历。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); 
boolean noneMatchResult = numbers.stream().noneMatch(n -> n > 5); 
System.out.println("No number is greater than 5: " + noneMatchResult); // 输出:true

这些方法都是终端操作,因为它们会遍历流中的元素并返回一个布尔值。

归约 reduce

reduce() 方法的主要作用是把 Stream 中的元素将集合中的元素通过指定的操作进行归约(合并)并返回一个结果

reduce 方法接收一个 BinaryOperator(二元操作符)作为参数,BinaryOperator 是一个函数式接口,它的方法是 apply(T, T),作用是对两个输入值执行某个操作,并返回操作结果。

可以将一个 Lambda 表达式或方法引用作为 BinaryOperator 参数,来定义归约操作的规则。

reduce 方法接收两个参数,第一个参数是归约操作的初始值(也称为 “identity”),第二个参数是一个函数,用于定义如何对两个值进行归约操作。

它有两种用法:

  1. Optional<T> reduce(BinaryOperator<T> accumulator)

没有起始值,只有一个参数,就是运算规则,此时返回 [[Optinal]]

这种情况下,reduce 方法会将集合中的元素进行两两组合,直到只剩下一个元素。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> result = numbers.stream() 
                              .reduce((a, b) -> a + b);
  1. T reduce(T identity, BinaryOperator<T> accumulator)
    有起始值,有运算规则,两个参数,此时返回的类型和起始值类型一致。

reduce 方法在函数式编程中非常有用,它可以用于将集合中的元素归约为一个值,例如求和、求最大值、求最小值、连接字符串等。

排序 sorted

sorted() 方法用于对流中的元素进行排序,默认情况下按照元素的自然顺序进行排序。如果元素实现了 Comparable 接口,则按照该接口定义的顺序进行排序。此外,还可以通过传入 Comparator 对象来自定义排序规则。

sorted 方法有两种主要形式:

  1. 自然排序:当流中的元素实现了Comparable接口时,可以直接调用sorted()方法进行自然排序(即按照元素的自然顺序,如数字的升序或字符串的字典序)。
javaStream<T> sorted();

这里的 T 是流中元素的类型,且 T 必须实现了 Comparable<T> 接口。

假设有一个包含整数的列表,我们需要对其进行升序排序:

List<Integer> numbers = Arrays.asList(5, 3, 8, 1, 9, 2); List<Integer> sortedNumbers = numbers.stream() .sorted() .collect(Collectors.toList()); System.out.println(sortedNumbers); // 输出:[1, 2, 3, 5, 8, 9]
  1. 自定义排序:如果需要按照特定的规则进行排序,可以调用sorted(Comparator<? super T> comparator)方法,并传入一个Comparator对象作为参数。
javaStream<T> sorted(Comparator<? super T> comparator);

这里的 Comparator 是一个函数式接口,用于定义元素之间的比较规则。

假设有一个 Person 类,包含 nameage 属性,我们需要根据年龄对 Person 对象的列表进行排序:

List<Person> people = new ArrayList<>(); // 假设people已经被添加了一些Person对象 
List<Person> sortedPeople = people.stream() .sorted(Comparator.comparingInt(Person::getAge)) .collect(Collectors.toList()); // 此时sortedPeople按照年龄升序排列

如果需要按照年龄降序排列,可以传入 Comparator.comparingInt(Person::getAge).reversed() 作为参数,或者使用 Comparator.reverseOrder() 结合 Comparator.comparingInt(Person::getAge)

去重 distinct

distinct 方法用于从流中去除重复元素。它比较的是元素的 equals 方法。如果两个元素的 equals 方法返回 true,那么这两个元素就被认为是相同的。

如果在处理自定义类型的对象列表,并希望使用 distinct() 方法去重,那么需要确保自定义类正确地实现了 hashCode()equals() 方法。这两个方法用于比较对象是否相等,从而影响 distinct() 方法的行为。

public class DistinctExample {  
    public static void main(String[] args) {  
        List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);  
  
        // 使用distinct()去重  
        List<Integer> distinctNumbers = numbers.stream()  
                .distinct() // 去除重复元素  
                .collect(Collectors.toList()); // 收集结果到列表中  
  
        // 输出结果  
        System.out.println(distinctNumbers); // 输出:[1, 2, 3, 4, 5]  
    }  
}

在这个示例中,我们有一个包含重复整数的列表 numbers。我们通过调用 stream() 方法将其转换为流,然后使用 distinct() 方法去除重复的元素。最后,我们使用 collect(Collectors.toList()) 方法将结果收集到一个新的列表中,并打印出来。

限制 limit

limit 方法用于限制流中元素的数量,返回一个新的流,该流最多包含指定数量的元素。这对于处理大数据集时限制结果的数量非常有用。

limit 方法接受一个 long 类型的参数 n,表示最多从流中获取 n 个元素。调用 limit 方法后,会返回一个新的流,这个流包含了原流中前 n 个元素(如果原流中的元素少于 n,则返回原流中的所有元素)。

limit 方法通常用于以下几种场景:

  1. 分页处理:在处理大量数据时,可以将数据分页显示。通过 limit 方法,可以限制每页显示的元素数量。

  2. 限制处理的数据量:在某些情况下,为了提高程序的性能或避免内存溢出,需要限制处理的数据量。使用 limit 方法可以有效地限制流中的元素数量。

  3. 截断操作:在某些数据处理场景中,可能只需要流中的前几个元素,此时可以使用 limit 方法进行截断操作。

public class LimitExample {  
    public static void main(String[] args) {  
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);  
  
        // 使用limit方法限制流中的元素数量  
        List<Integer> limitedNumbers = numbers.stream()  
                .limit(3) // 限制为前3个元素  
                .collect(Collectors.toList()); //收集结果到列表中  
  
        // 输出结果  
        System.out.println(limitedNumbers); // 输出:[1, 2, 3]  
    }  
}

在上面的示例中,我们创建了一个包含5个整数的列表 numbers。然后,我们通过调用 stream() 方法将列表转换为流,并使用 limit(3) 方法限制流中的元素数量为3。最后,我们使用 collect(Collectors.toList()) 方法将结果收集到一个新的列表中,并打印出来。

虽然 limit 方法可以帮助我们限制处理的数据量,但在某些情况下,如果流中的元素数量非常大,而我们只需要其中的一小部分,那么最好在使用 limit 方法之前先使用其他流操作(如 filter)来减少流中的元素数量,以提高性能。

跳过 skip

skip 方法用于跳过流中的前 N 个元素,然后返回剩下的元素。这与 limit 方法一起使用时,常用于实现分页查询。

skip 方法接受一个 long 类型的参数,表示要跳过的元素数量。调用 skip 方法后,会返回一个新的流,这个流包含了除了被跳过的前n个元素之外的所有元素。如果流中的元素数量少于n,那么返回的流将是一个空流。

如果传递给 skip 方法的参数是负数,那么会抛出 IllegalArgumentException 异常。

public class SkipExample {  
    public static void main(String[] args) {  
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);  
  
        // 跳过前2个元素  
        List<Integer> skippedNumbers = numbers.stream()  
                .skip(2) // 跳过前两个元素  
                .collect(Collectors.toList()); // 收集结果到列表中  
  
        // 输出结果  
        System.out.println(skippedNumbers); // 输出:[3, 4, 5]  
    }  
}

在这个示例中,我们创建了一个包含5个整数的列表 numbers。然后,我们通过调用 stream() 方法将列表转换为流,并使用 skip(2) 方法跳过了前两个元素。最后,我们使用 collect(Collectors.toList()) 方法将结果收集到一个新的列表中,并打印出来。

转换流

collect() 方法能够将 stream 转换为 数组或集合,它可以在流计算的最后阶段将结果进行汇总和收集。

collect 方法接收一个 Collector 参数,Collector 是一个用于定义如何收集元素的接口。可以使用 Collectors 类中提供的静态方法来创建和获取常见的 Collector 实例,例如 toList()toSet()toMap() 等。

当我们需要把一个集合按照某种规则转成另外一个集合的时候,就可以配套使用 map() 方法和 collect() 方法。

List<Integer> list1 = list.stream().map(String::length).collect(Collectors.toList());

通过 stream() 方法创建集合的流后,再通过 map(String:length) 将其映射为字符串长度的一个新流,最后通过 collect() 方法将其转换成新的集合。

Collectors 是一个收集器的工具类,内置了一系列收集器实现:

  • toList() 方法将元素收集到一个新的 java.util.List

  • toCollection() 方法将元素收集到一个新的 java.util.ArrayList

  • joining() 方法将元素收集到一个可以用分隔符指定的字符串中。

流操作的特性

流操作在Java Stream API中扮演着核心角色,它们具有一系列独特的特性,这些特性使得流操作在处理集合数据时既高效又灵活。以下是流操作的主要特性:

  1. 链式调用:流操作支持链式调用,即可以将多个操作连接在一起形成一个操作链。这种链式调用的方式使得代码更加简洁易读,同时也提高了开发效率。例如,你可以通过链式调用对流进行过滤、映射、排序等操作。

  2. 不可变性流操作不会修改原始数据源。每次对流进行操作时,都会返回一个新的流对象,而原始数据源保持不变。这种不可变性的特性有助于避免并发编程中的数据竞争问题,同时也使得代码更加安全可靠。

  3. 惰性求值:流操作是惰性的,即中间操作(如 filtermap 等)不会立即执行,而是会延迟到终端操作(如 forEachcollect 等)执行时才进行。这种惰性求值的特性可以显著提高性能,因为它允许流操作在必要时才处理数据,从而避免了不必要的计算。

  4. 并行处理能力:Java Stream API支持并行流,可以利用多核处理器进行并行处理,从而提高处理效率。通过调用集合的 parallelStream() 方法,可以轻松地创建并行流。并行流中的操作会自动分配到多个线程上执行,从而加速数据处理过程。

  5. 丰富的操作:Stream API提供了丰富的操作,包括中间操作和终端操作。中间操作如 filtermapsorted 等用于对流中的元素进行处理,而终端操作如 forEachcollectreduce 等则用于触发流的执行并产生结果。这些操作覆盖了数据处理中的大部分需求,使得开发者可以更加灵活地处理集合数据。

  6. 短路操作:短路操作是 Stream 中一种特殊的操作,它们可以在不需要处理整个流的情况下提前终止操作。

    例如,findFirst() 操作会返回流中的第一个元素,而无需遍历整个流;anyMatch() 操作会在流中找到第一个匹配条件的元素时立即返回true,而无需继续检查剩余元素。这些短路操作可以显著提高处理效率,特别是在处理大数据集时。

  7. 自定义操作:除了使用 Stream API 提供的标准操作外,开发者还可以创建自定义操作来处理流中的元素。例如,可以通过实现 Collector 接口来创建自定义收集器,从而以自定义的方式收集流中的元素。这种自定义操作的特性使得 Stream API 更加灵活和强大。


原文地址:https://blog.csdn.net/Zachyy/article/details/142425898

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!