Java函数式编程之【流(Stream)性能优化】

Java函数式编程之【流(Stream)性能优化】

一、流(Stream)性能优化的预备知识

我们先来详细探讨一下基于 Java Stream API 的函数式编程时性能调优相关的一些基础知识。

(一)并行与并发的区别

在Java函数式编程中编写并行程序很容易,要优化Java Stream程序的性能首先要搞清楚并行和并发的概念。

并发 (Concurrency):多个任务在同一时间段内执行,但不一定是同时执行。例如:可在单核CPU上并发运行多个线程。其执行方式是通过时间片轮转,进行任务切换实现。

在单核CPU上的并发执行,逻辑上是"同时"执行,实质是串行轮流执行的。

并行 (Parallelism):多个任务同时执行,但需要有多核或多处理器硬件支持。例如:多核CPU同时处理多个不同任务。

(二)Stream操作特性分类

我们先来了解一下Stream操作分类,因为弄清Stream操作分类对大数据的高效迭代处理的性能优化有很大帮助。

中间操作和终止操作是Stream API中的关键概念。中间操作又可以细分为无状态(Stateless)操作和有状态(Stateful)操作;终结操作又可细分为短路(Short-circuiting)操作和非短路(Unshort-circuiting)操作。

  • 中间操作:又称为装饰操作,它的输入是一个Stream,操作后返回一个新的Stream。

中间操作只对操作进行了声明,它会把Stream从一种流(Stream)转换为另一种流,中间操作都是惰性的,它们不会立即执行。例如,filter(), map() 和 sorted() 等方法都属于中间操作,它们会返回一个新的Stream实例。中间操作可以链接在一起,形成一个链式调用,成为Stream处理管道的组成部分。

无状态(Stateless)操作和有状态(Stateful)操作:

中间操作又可以分为无状态(Stateless)与有状态(Stateful)操作,前者是指元素的处理不受之前元素的影响,后者是指该操作只有拿到所有元素之后才能继续下去。

1,常见无状态(Stateless)的中间操作有:filter() 、map()、 flatMap()、 peek()

2,常见有状态(Stateful)的中间操作有:distinct() 、sorted()、 limit() 、skip()

  • 终止操作:又称为终端操作,终止操作将触发了整个流(Stream)的计算过程,得到操作结果。

终止操作则是主动求值操作。终止操作会触发整个Stream处理管道的执行。例如,collect(), forEach(), reduce() 等方法都是终止操作。

当终止操作被调用时,所有的中间操作将按序执行,最终得到操作结果。

短路(Short-circuiting)操作和非短路(Unshort-circuiting)操作:

终结操作又可以分为短路(Short-circuiting)与非短路(Unshort-circuiting)操作,前者是指遇到某些符合条件的元素就可以得到最终结果,后者是指必须处理完所有元素才能得到最终结果。

短路(Short-circuiting): anyMatch()、 allMatch()、 findFirst()、 findAny()、 noneMatch()

非短路(Unshort-circuiting)操作:forEachOrdered()、toArray()、reduce()、collect()、max()、min()、count()

(三)Stream流管道的相关知识

函数式编程的一个流管道(Stream pipeline),包括Stream的创建(由数据源创建Stream)、0个或n个Stream的中间操作和一个Stream的终止操作。

如果不了解 流(Stream) 函数式编程的内部机制,就写不错高性能的程序。只有了解了流管道(Stream pipeline)的执行过程,我们才能更好地优化Stream的性能。

我们先来梳理一下 Stream API 是由哪些主要接口和类组合而成的呢?BaseStream 和 Stream 是最顶端的接口类。

  • BaseStream接口主要定义了流的基本方法,例如,iterator()、spliterator()、isParallel()等;
  • Stream接口则定义了一些流的常用操作方法,例如,filter()、map()、mapToLong()、flatMap()和collect()等。
  • ReferencePipeline是一个抽象类,他通过定义内部类组装了各种流的操作。它定义了Head、StatelessOp、StatefulOp三个内部类,实现了BaseStream与Stream的接口,继承了它们的方法。
  • Sink接口则定义了每个Stream操作之间关系的协议,它包含begin()、end()、cancellationRequested()、accpet()四个方法。ReferencePipeline最终会将整个Stream流操作组装成一个调用链,而这条调用链上的各个Stream操作的上下关系就是通过Sink接口协议来定义实现的。

Stream操作的链接调用

一个流管道(Stream pipeline)可由Stream的各种操作组合而成,并最终由终止操作完成数据处理。

在JDK中每次的中间操作会用阶段(Stage)命名。

流管道的链接调用结构通常是由ReferencePipeline类实现的,前文已介绍过ReferencePipeline包含了Head、StatelessOp、StatefulOp三种内部类。

来看一个简单的示例:

cpp 复制代码
package stream;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class StreamDemo {
	public static void main(String[] args) {
		//字符串的"+"操作   //字符串拼接
		List<String> names = Arrays.asList("Bob","John","Mary");
		Optional<String> str = names.stream()
				.filter(e->e.length()>=4)
				.reduce((s1,s2)->s1+s2);
		System.out.println("拼接结果:"+str.orElse(""));
		
	}
}

Head类主要用来定义数据源操作,在初次调用names.stream()方法时,会初次加载Head对象,此时为加载数据源操作;接着加载的是中间操作,分别为无状态中间操作StatelessOp对象和有状态操作StatefulOp对象,此时的Stage并没有执行,而是通过AbstractPipeline生成了一个中间操作Stage链表;当我们调用终止操作时,会生成一个最终的Stage,通过这个Stage触发之前的中间操作,从最后一个Stage开始,递归产生一个Sink链。

二、流(Stream)的性能优化策略

(一)选择合适的Stream(流)类型

1,优先使用基本数据类型流(避免装箱/拆箱开销)

这是最立竿见影的优化策略之一。因为 Stream<Integer>Stream<Long>Stream<Double> 等都属于对象流 Stream<T>IntegerLongDouble 都是基本数据类型的包装类,包装类型的对象流 Stream<T> 会带来巨大的 装箱(Boxing)拆箱(Unboxing) 开销。

将基本数据类型(如 int、long或double)转换为数值的包装类型(如 Integer、Long和Double)称为装箱操作;反之称为拆箱操作。

对于大数据的数值运算,装箱和拆箱的计算开销,以及包装类型占用的额外存储空间,会明显降低程序的运算速度。

优化策略: 在大数据的数值计算和数理统计场景,对于 int, long, 和 double 等基本数据类型,应优先使用基本数据类型流 IntStream, LongStream 和 DoubleStream,以避免装箱和拆箱的开销。

这些流有一个最常用的汇总统计SummaryStatistics()方法,这个方法可得到一组统计数据:元素个数、汇总值、最大值、最小值和平均值。

Java语言为基本数据类型流专门内置了一些实用的方法:例如,都有内置的规约操作,包括average、count、max、min、sum。

这些方法都直接在原始数据类型上操作,避免了包装类的装箱和拆箱开销。

当数据源为大数据的列表 List<Integer> 时的情形:

cpp 复制代码
	// 低效 - 存在装箱开销
	List<Integer> numbers = ...;
	int sum = numbers.stream()
           .reduce(0, Integer::sum);

	// 高效 - 使用 IntStream
	int sum = numbers.stream()
           .mapToInt(Integer::intValue) // 转换为 IntStream
           .sum(); // 直接在 int 上操作

更进一步 : 如果数据源本身是基本数据类型的数组的情形,可直接使用 Arrays.stream() 创建Stream<Integer> 流:

cpp 复制代码
	int[] intArray = ...;
	IntStream intStream = Arrays.stream(intArray);

装箱和不装箱的性能比较测试例程:

cpp 复制代码
	public static void 装箱与不装箱Test() {
		// 装箱版本测试
		long startTime = System.currentTimeMillis();
	    List<Integer> list = new ArrayList<>();
	    for (int i = 0; i < 10_000_000; i++) {
	        list.add(i);
	    }
	    long sum = 0;
	    for (Integer num : list) {
	        sum += num;
	    }
	    long endTime = System.currentTimeMillis();
		long boxedTime = endTime - startTime;

		// 原始数据类型版本测试
		startTime = System.currentTimeMillis();
	    int[] array = new int[10_000_000];
	    for (int i = 0; i < array.length; i++) {
	        array[i] = i;
	    }
	    sum = 0;
	    for (int num : array) {
	        sum += num;
	    }
	    endTime = System.currentTimeMillis();
		long primitiveTime = endTime - startTime;
		
		System.out.println("装箱版本耗时: " + boxedTime + "ms");
		System.out.println("原始数据类型版本耗时: " + primitiveTime + "ms");
	}

装箱与不装箱的测试结果:

在大数据计算密集型任务中,避免装箱操作通常可以获大幅的性能提升,同时减少内存使用和GC压力。

2,正确选择 顺序流(stream) 或 并行流(parallelStream)

顺序流或并行流 :根据数据集大小和是否对元素顺序敏感选择顺序流stream()或并行流parallelStream()。小规模数据集或顺序敏感的操作适合顺序流;大规模数据集且能接受非确定性结果时适合并行流。

下面来看一个小规模数据集,我们分别使用基本数据类型流、顺序流装箱和并行流装箱进行比较测试:

cpp 复制代码
package stream;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class StreamTest {
	
	public static void primitive() { //基本数据类型流(顺序流)
		long start = System.currentTimeMillis();
		long sum = IntStream.range(1, 10_000)
		        .sum();
		long duration = System.currentTimeMillis() - start;

		System.out.println("基本类型流求和结果: " + sum + ", 耗时: " + duration + "ms");
	}
	
	public static void boxed() { //顺序流装箱
		long start = System.currentTimeMillis();
		List<Integer> numbers = IntStream.range(1, 10_000).boxed().collect(Collectors.toList());
		int sum = numbers.stream()
		        .mapToInt(Integer::intValue)
		        .sum();
		long duration = System.currentTimeMillis() - start;

		System.out.println("顺序流求和结果: " + sum + ", 耗时: " + duration + "ms");
	}

	public static void parallel() { //并行流装箱
		long start = System.currentTimeMillis();
		List<Integer> numbers = IntStream.range(1, 10_000).boxed().collect(Collectors.toList());
		int sum = numbers.parallelStream()
		        .mapToInt(Integer::intValue)
		        .sum();
		long duration = System.currentTimeMillis() - start;

		System.out.println("并行流求和结果: " + sum + ", 耗时: " + duration + "ms");
	}
	
	public static void main(String[] args) {
		primitive();
		boxed();
		parallel();
	}
}

小规模数据集,我们分别使用基本数据类型流、顺序流装箱和并行流装箱进行比较测试。

预期结果是耗时从小到大。但是令人遗憾的是,测试并未得到预期的结果。

如果调整代码顺序会得到完全不同的测试结果。测试顺序严重影响结果。

原因是:Java 的 JIT(即时编译器)需要时间来优化代码。先执行的代码都会因为JIT还没有充分优化而变慢。

  • 先执行的代码:承担 JIT 编译的开销。
  • 后执行的代码:享受已经优化好的代码。

解决方案:正确的性能测试方法是使用专业的基准测试工具,如JMH(Java Microbenchmark Harness)。

下面这个示例,试图演示在数值运算场景中,基本数据类型流LongStream 比对象流Stream有更好的效率。

使用专业的基准测试工具(如 JMH),才能得到准确的结果。你会发现 LongStream 确实比 Stream 快 2-5 倍。

cpp 复制代码
package stream;
import java.util.Random;
import java.util.stream.LongStream;
import java.util.stream.Stream;
public class BOxUnboxingComparison {
    private static final int DATA_SIZE = 10_000;
    private static final Random RANDOM1 = new Random(42);
    private static final Random RANDOM2 = new Random(42);
    
    public static void main(String[] args) {
        // 测试1: 基本的统计操作
        System.out.println("=== 基本统计操作性能测试 ===");
        /***对象流Stream(Long),数据计算时需要拆箱***/
        long startTime = System.nanoTime();
        long boxedSum = Stream.generate(() -> Math.abs(RANDOM1.nextLong()) % 100)
                .limit(DATA_SIZE)
                .mapToLong(Long::longValue)  // 必须拆箱才能计算
                .sum();
        long boxedTime = System.nanoTime() - startTime;

        /***基本数据类型流LongStream,数据计算时无须拆箱***/
        startTime = System.nanoTime();
        long primSum = LongStream.generate(() -> Math.abs(RANDOM2.nextLong()) % 100)
                .limit(DATA_SIZE)
                .sum();
        long primTime = System.nanoTime() - startTime;
        
        printResults("求和", primTime, boxedTime, primSum, boxedSum);
        
        // 测试2: 过滤和聚合操作
        System.out.println("\n=== 过滤和聚合操作性能测试 ===");
        
		/***对象流Stream(Long),数据计算时需要拆箱***/
        startTime = System.nanoTime();
        double boxedAvg = Stream.generate(() -> Math.abs(RANDOM1.nextLong()) % 100)
            .limit(DATA_SIZE)
            .filter(x -> x > 50)        // 过滤(自动拆箱比较)
            .mapToLong(Long::longValue)  // 必须拆箱
            .average()
            .orElse(0);
        boxedTime = System.nanoTime() - startTime;
        
        /***基本数据类型流LongStream,数据计算时无须拆箱***/
        startTime = System.nanoTime();
        double primAvg = LongStream.generate(() -> Math.abs(RANDOM2.nextLong()) % 100)
            .limit(DATA_SIZE)
            .filter(x -> x > 50)        // 过滤
            .average()                   // 计算平均值
            .orElse(0);
        primTime = System.nanoTime() - startTime;
        
        printResults("平均值", primTime, boxedTime, primAvg, boxedAvg);
	}
    
    private static void printResults(String operation, long primitiveTime, long boxedTime, 
                                   Object primitiveResult, Object boxedResult) {
        System.out.printf("LongStream    - 耗时: %8d ns, 结果: %s%n", primitiveTime, primitiveResult);
        System.out.printf("Stream<Long>  - 耗时: %8d ns, 结果: %s%n", boxedTime, boxedResult);
        System.out.printf("性能差距: %.2f 倍%n", (double) boxedTime / primitiveTime);
    }
}

(二)中间操作的优化

每个中间操作都可能涉及到对元素的复制和创建新的Stream对象。合理配置中间操作可优化Stream的性能。

  • 使用 distinct() 删除重复元素

如果 stream 中可能包含重复元素,可使用 distinct() 操作将其删除,防止无效处理以提高性能。

cpp 复制代码
		List<Integer> list = Arrays.asList(1, 2, 3, 3, 4, 5, 5);
		list.stream().distinct().forEach(System.out::println);
  • 合理配置 limit() 防止无效处理

示例:

cpp 复制代码
	// 生成斐波那契数列但只取前10个
	Stream.iterate(new int[]{0, 1}, fib -> new int[]{fib[1], fib[0] + fib[1]})
    	.limit(10) // 限制器
    	.map(fib -> fib[0])
   		.forEach(System.out::println);
  • 谨慎使用 sorted()

sorted()操作可能很昂贵,尤其是对于大数据流(large streams)。请谨慎使用,只有在必要时才使用。如果知道输入数据已经排序,可以跳过此操作。

  • 选择正确的中间操作顺序。需要数据过滤的场景,尽早使用filter()

中间操作的顺序对性能有巨大影响,尤其是 filter() 和 map() 的顺序。
优化策略

合理配置map()和filter()的组合。先过滤filter(),后映射map(): 尽可能早地使用 filter() 筛选掉不需要处理的元素。这会使后续所有操作(map, sorted, collect 等)的数据量变小。

示例:

cpp 复制代码
	var list = Arrays.asList(1, 2, 3, 4, 5,8,16,18);
	var filteredList = list.stream()
                       .filter(i -> i % 2 == 0)
                       .map(i -> i * 2)
                       .collect(Collectors.toList());

上面这个示例"在 map() 之前使用 filter()"可过滤掉一大半的元素,大大减少了map()操作的工作量。

避免在中间操作中执行昂贵操作: 如果 map 中包含一个昂贵的方法调用,确保它只在必要的元素上执行(即先被 filter 处理过的)。

cpp 复制代码
	// 低效的中间操作顺序
	List<String> names = list.stream()
        .map(e -> expensiveOperation(e)) // 对所有元素执行昂贵操作
        .filter(s -> s.length() > 3)
        .collect(Collectors.toList());

	// 高效的中间操作顺序 - 优先过滤
	List<String> names = list.stream()
        .filter(e -> e.length() > 3) // 先减少数量
        .map(e -> expensiveOperation(e)) // 只对过滤后的元素执行
        .collect(Collectors.toList());

再来看另一个典型示例:

cpp 复制代码
// 目标:获取所有字符串长度的平方,且只保留大于10的结果

// 低效:先map所有,再filter
List<Integer> lengthsSquared = words.stream()
        .map(String::length) // 所有元素都被映射
        .map(len -> len * len) // 所有元素再次被映射
        .filter(sq -> sq > 10) // 最后过滤掉大部分
        .collect(Collectors.toList());

// 更高效:合并map和filter,但逻辑稍复杂
// (有时无法避免,但这里可以优化)

// 最优:合并map操作,并尽早filter
List<Integer> lengthsSquared = words.stream()
        .map(String::length) // 先map一次
        .filter(len -> len > 3) // 尽早过滤:因为如果length<=3,平方肯定<=9,不会大于10
        .map(len -> len * len) // 只为剩余的元素进行平方计算
        .collect(Collectors.toList());
  • 适当合并中间操作减少Stream内部迭代次数

1,多个可合并的映射(map)操作 合并为一个操作

将多个可合并的映射(map)操作合并为一个,从而减少Stream管道中需要执行的迭代次数和中间对象的创建。

经典示例:处理字符串列表,假设我们有一个字符串列表,我们需要:

(1)将每个字符串转换为大写。

(2)反转每个字符串。

(3)获取每个字符串的前3个字符。

低效做法 :使用多个连续的 map 操作。

在这个算法中,Stream管道内部发生了3次完整的迭代,并创建了2个中间的Stream元素集合。

cpp 复制代码
//优化前的低效算法:
List<String> words = Arrays.asList("hello", "world", "java", "stream");

List<String> result = words.stream()
        .map(String::toUpperCase)      // 第一次迭代:生成新流 ["HELLO", "WORLD", "JAVA", "STREAM"]
        .map(s -> new StringBuilder(s).reverse().toString()) // 第二次迭代:生成新流 ["OLLEH", "DLROW", "AVAJ", "MAERTS"]
        .map(s -> s.substring(0, Math.min(3, s.length())))   // 第三次迭代:生成新流 ["OLL", "DLR", "AVA", "MAE"]
        .collect(Collectors.toList());

System.out.println(result); // 输出: [OLL, DLR, AVA, MAE]

高效做法 :将多个连续的 map 操作合并为一个。

"合并中间操作"最经典的体现就是将多个连续的 map 操作合并为一个,这是提升Stream性能最简单有效的方法之一。

cpp 复制代码
//优化后的高效算法:
List<String> words = Arrays.asList("hello", "world", "java", "stream");

List<String> result = words.stream()
        .map(s -> { // 只进行一次映射操作,将所有转换逻辑合并
            String upper = s.toUpperCase();
            String reversed = new StringBuilder(upper).reverse().toString();
            return reversed.substring(0, Math.min(3, reversed.length()));
        })
        .collect(Collectors.toList());

System.out.println(result); // 输出: [OLL, DLR, AVA, MAE]

2,多个连续的 filter() 操作合并。

经典示例:筛选用户列表

合并多个filter操作是一个非常重要且常见的优化策略。它的核心思想是:通过逻辑运算符将多个过滤条件合并为一个filter,减少Stream管道中的阶段数量和 predicate(断言)的执行次数。

假设我们有一个User对象列表,我们需要筛选出同时满足以下条件的用户:

(1), 年龄大于等于18岁(成年人)

(2), 年龄小于65岁(未退休)

(3), 账户状态是激活的(ACTIVE)

(4), 登录次数大于0(活跃用户)

低效算法: 使用多个连续的 filter() 操作

在这个算法中,数据仿佛要"过四道筛子":

cpp 复制代码
// 优化前的低效算法
List<User> users = // ... 获取用户列表

List<User> filteredUsers = users.stream()
        .filter(user -> user.getAge() >= 18)        // 第一轮过滤:检查条件1
        .filter(user -> user.getAge() < 65)         // 第二轮过滤:检查条件2
        .filter(user -> user.getStatus() == User.Status.ACTIVE) // 第三轮过滤:检查条件3
        .filter(user -> user.getLoginCount() > 0)   // 第四轮过滤:检查条件4
        .collect(Collectors.toList());

高效做法:合并 filter() 操作

cpp 复制代码
// 第一次优化后的算法
List<User> users = // ... 获取用户列表

List<User> filteredUsers = users.stream()
        .filter(user -> user.getAge() >= 18 
                     && user.getAge() < 65
                     && user.getStatus() == User.Status.ACTIVE
                     && user.getLoginCount() > 0)
        .collect(Collectors.toList());

其实,在本例中还可以继续进行优化。

利用逻辑运算的短路(Short-Circuit)评估: 这也是一个关键的优化点。Java中的逻辑运算符 && (AND) 和 || (OR) 是短路的。

1)、对于 逻辑运算符 && (AND) :如果第一个条件为 false ,整个表达式的结果立即确定为 false ,后续条件将不再被计算。

在本例中,如果一个用户年龄是17岁(user.getAge() >= 18 为 false),JVM会立即跳过对其退休状态、账户状态和登录次数的检查。这节省了3次方法调用和比较操作。

将最可能失败或计算成本最低的条件放在&&的最左边,可以最大化短路优化带来的收益。

合并后,条件的顺序变得很重要,因为它直接影响短路评估的效果。

进一步优化策略:

    • 将最可能使条件失败的条件放在前面。 如果90%的用户都是非活跃的,那么先检查loginCount > 0就能最快地淘汰大部分元素。
    • 将计算成本最低的条件放在前面。 比较年龄(基本类型比较)的成本远低于从数据库或网络获取状态(假设getStatus()很昂贵)。先进行廉价的检查,可以避免对即将被淘汰的元素执行昂贵的操作。

优化合并filter()操作的最终版本:

cpp 复制代码
//优化合并filter()操作的最终版本
List<User> users = // ... 获取用户列表

List<User> filteredUsers = users.stream()
	.filter(user -> user.getLoginCount() > 0        // 成本低,可能淘汰很多用户
	    && user.getAge() >= 18                      // 成本低
    	&& user.getAge() < 65                       // 成本低
    	&& user.getStatus() == User.Status.ACTIVE)  // 可能成本最高(例如涉及数据库查询),放在最后

注意事项: 如果某个昂贵条件是独立的,无法通过短路避免,有时将其单独放在一个filter里并在前面使用更廉价的条件过滤可能更优,但这需要具体分析。通常,合并filter并利用短路特性来调整过滤条件顺序是最佳实践。

2)、对于 逻辑运算符 || (OR) :如果第一个条件为 true ,整个表达式立即为 true ,后续条件不再计算。

请看一个 逻辑运算符 **|| (OR)**逻辑连接的filter() 操作合并为一个操作的示例:

cpp 复制代码
// 多个filter() 合并 和 优化 示例

List<User> users = // ... 获取用户列表
// 未合并为一个filter (OR 逻辑)的原始状态
List<User> filteredUsers = users.stream()
	.filter(user -> user.getTotalSpending() > 1000)
	.filter(user -> user.getYearsAsMember() > 5)
	...

// 简单合并为一个filter (OR 逻辑)
List<User> filteredUsers = users.stream()
	.filter(user -> user.getTotalSpending() > 1000 || user.getYearsAsMember() > 5)
	...

// 优化后: 优化策略:将更可能为true或成本更低的条件放在||前面
List<User> filteredUsers = users.stream()
	.filter(user -> user.getYearsAsMember() > 5 || user.getTotalSpending() > 1000)
	...

结论: 可将多个连续的、用 && (AND)|| (OR) 逻辑连接的filter() 操作合并为一个,并利用短路评估来排序条件,是减少不必要的计算、提升Stream性能的经典且高效的手段。在编写Stream代码时,应有意识地检查是否存在可合并的filter()操作。

额外的好处: Stream API需要为每个中间操作维护一个调用栈。减少中间操作的数量使得整个流水线更简单,JIT编译器也可能更容易对其进行优化。

(三)终端操作的优化

  • 选择高效的终端操作

不同的终端操作有不同的性能特征。优化策略:

(1)、anyMatch/allMatch/noneMatch 是短路(short-circuiting)操作,一旦找到答案就会停止处理,性能通常很好。

(2)、findFirst 在并行流中需要协调,可能会有一定开销。如果顺序不重要,findAny 在并行流中限制更少,性能可能更高。

(3)、count 对于 SIZED 流(如从 List 创建的流)来说非常高效。否则,它需要遍历所有元素。

(4)、collect 的性能取决于你使用的收集器。Collectors.toList() 通常很高效。Collectors.groupingBy 和 Collectors.toMap 的成本较高,需要注意。

流管道(Stream pipeline)中的操作应该都是无副作用的。

例如:在中间操作中修改外部状态会导致并发问题(尤其在并行流中)和不可预测的行为。

终端操作也应该是无副作用的。应优先使用收集器(Collectors) 而不是在 forEach 中修改外部集合。forEach 应仅用于消费最终结果,而不是处理过程。请看示例:

cpp 复制代码
// 错误 - 在 forEach 中修改外部集合(非线程安全)
List<String> result = new ArrayList<>();
stream.filter(...)
      .forEach(e -> result.add(e)); // 在并行流中会崩溃

// 正确 - 使用收集器
List<String> result = stream.filter(...)
                            .collect(Collectors.toList()); // 线程安全且高效
  • 利用短路操作提高效率
    诸如 findFirst(),findAny() 和 anyMatch() 等短路操作,在找到符合条件或不符合条件时就会立即返回,不处理整个流。利用这些操作可以提高性能。
cpp 复制代码
/***权限检查短路***/
List<Permission> userPermissions = getUserPermissions(userId);
// 检查用户是否有管理员权限
boolean isAdmin = userPermissions.stream()
    .anyMatch(p -> p.getType() == PermissionType.ADMIN); // 找到第一个管理员权限就返回
// 比收集到列表再判断更高效

/***验证输入数据***/
List<String> inputs = getInputs();
// 验证所有输入是否有效(遇到第一个无效就停止)
boolean allValid = inputs.stream()
    .peek(input -> System.out.println("Validating: " + input))
    .allMatch(this::isValidInput);

(四)正确使用并行流(parallelStream)

Java的Stream API 支持并行流(parallelStream)操作,可以利用多个处理器阵列或多核处理器并行处理数据。

并行流操作通过将数据分割成多个子集,然后并行处理,最终合并结果。

并行流(parallelStream)可以在处理大量数据时提供更好的性能,但也会带来额外开销和竞争条件。谨慎使用parallelStream,并需要考虑数据规模、操作复杂程度和可用处理器数量等因素。

并行操作引入了线程管理的开销和线程间协调开销。如果使用不当,也可能引起线程管理的开销增加。例如:在处理 IO 密集型任务或数据量小的场景,并行流可能因线程管理和线程间协调的开销反而更慢。

当调用 stream().parallel() 或 parallelStream() 时进行并行处理时,Java 会将数据分割成多个子集,由 ForkJoinPool 分配线程并行处理。这种机制在处理"大数据集、CPU 密集型操作"时优势明显,比如统计分析、数据汇总,以及排序、过滤、聚合等操作。

并行流需要有硬件支持:多个处理器或多核处理器的系统

并行流 (parallelStream) 可以将工作负载分配到多个 CPU 核心上,但并非万能药。错误使用会导致更差的性能(线程上下文切换开销)甚至错误结果。

优化策略:

使用工具测量,不要凭猜测: 始终使用类似 JMH 的工具进行基准测试,比较并行和串行流的性能。并行化的开销(拆分、协调、合并)只有在数据量足够大、计算足够密集时才能被抵消。

并行流(parallelStream)适用场景:

  • 大数据,大规模数据源(例如,数十万以上元素)。
  • 每个元素的处理是计算密集型(CPU-intensive)的,而不是 I/O 密集型(如网络请求、文件读写)。
  • 数据源易于拆分:ArrayList、数组、IntStream.range() 等支持随机访问的数据源拆分效率极高。而 LinkedList、Stream.iterate() 则难以有效拆分。
  • 操作是无状态且无关联的(如 map, filter),合并操作成本低(如 reduce 的合并器)。

不适合的场景:

  • 数据量小。
  • 操作是 I/O 密集型(线程会大部分时间在等待,浪费资源,应使用专门的异步 API如 CompletableFuture)。
  • 操作有状态或有副作用(如 sorted, distinct, limit 在并行流中代价更高,因为需要协调)。
  • 使用了有状态的 Lambda 表达式或阻塞操作。

示例:

cpp 复制代码
	List<String> hugeList = ...;

	// 好的并行用例:大数据集,计算密集型操作
	long count = hugeList.parallelStream()
                    .filter(s -> s.length() > 10) // 无状态
                    .count(); // 简单合并

	// 坏的并行用例:小数据集
	long count = smallList.parallelStream() // 开销大于收益
                     .count();

三、总结:流(Stream)性能优化的工作流程

  • 首先,写出正确、清晰的程序代码: 不要过早优化。先使用 Stream API 实现正确的逻辑。

  • 基准测试: 使用 JMH (Java Microbenchmark Harness) 等工具来识别真正的性能瓶颈。不要靠猜测。

  • 应用调优策略进行性能优化:

第一步: 检查是否能优先使用基本数据类型流 (IntStream 等)。

第二步: 中间操作的优化,检查中间操作顺序,例如确保尽早过滤 (filter)。

第三步: 评估是否适合并行化 (parallelStream),并进行测试。

第四步(高级): 检查终端操作和收集器,必要时考虑自定义。

再次测量: 每次优化后都进行基准测试,确认优化确实有效。

通过遵循这些策略,才能最大限度地发挥 Java Stream API 函数式编程的威力,同时保持代码的高性能。

相关推荐
CYRUS_STUDIO2 小时前
一步步带你移植 FART 到 Android 10,实现自动化脱壳
android·java·逆向
练习时长一年2 小时前
Spring代理的特点
java·前端·spring
CYRUS_STUDIO2 小时前
FART 主动调用组件深度解析:破解 ART 下函数抽取壳的终极武器
android·java·逆向
MisterZhang6663 小时前
Java使用apache.commons.math3的DBSCAN实现自动聚类
java·人工智能·机器学习·自然语言处理·nlp·聚类
Swift社区3 小时前
Java 常见异常系列:ClassNotFoundException 类找不到
java·开发语言
一只叫煤球的猫4 小时前
怎么这么多StringUtils——Apache、Spring、Hutool全面对比
java·后端·性能优化
维基框架5 小时前
维基框架 (Wiki FW) v1.1.1 | 企业级微服务开发框架
java·架构
某空_6 小时前
【Android】BottomSheet
java
10km6 小时前
jsqlparser(六):TablesNamesFinder 深度解析与 SQL 格式化实现
java·数据库·sql·jsqlparser
是2的10次方啊6 小时前
Java多线程基础:进程、线程与线程安全实战
java