MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java Stream distinct 方法的性能优化

2021-01-193.4k 阅读

Java Stream distinct 方法的性能优化

Java Stream 简介

Java 8 引入了 Stream API,这是一个处理集合数据的强大工具。Stream API 提供了一种声明式的方式来处理集合,使得代码更加简洁、易读,并且支持并行处理以提高性能。Stream 代表一系列支持顺序和并行聚合操作的元素。例如,我们可以使用 Stream 来过滤、映射、排序和聚合集合中的元素。

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

public class StreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
        List<Integer> distinctNumbers = numbers.stream()
               .distinct()
               .collect(Collectors.toList());
        System.out.println(distinctNumbers);
    }
}

在上述代码中,我们首先创建了一个包含重复元素的 List,然后通过 stream() 方法将其转换为 Stream,接着使用 distinct() 方法去除重复元素,最后通过 collect(Collectors.toList()) 方法将 Stream 转换回 List

distinct 方法的工作原理

distinct() 方法是 Stream 接口的中间操作,它返回由该流的不同元素(根据 Object.equals(Object) 方法)组成的流。在底层实现中,distinct() 方法依赖于一个 Set 来跟踪已经遇到的元素。当流中的每个元素被处理时,它会被添加到 Set 中,如果该元素已经存在于 Set 中,则会被过滤掉。

对于顺序流,distinct() 方法会按照元素在流中的顺序依次处理。而对于并行流,情况会稍微复杂一些。并行流会将数据分成多个部分,每个部分独立地进行去重操作,然后再合并这些结果。在合并过程中,还需要再次检查并去除可能由于并行处理而产生的重复元素。

性能分析

  1. 顺序流:在顺序流中,distinct() 方法的时间复杂度主要取决于底层 Set 的操作。通常情况下,Set 的添加操作时间复杂度为 O(1)(假设使用 HashSet),但是在最坏情况下(大量哈希冲突),时间复杂度可能会退化到 O(n)。因此,对于顺序流,distinct() 方法的平均时间复杂度为 O(n),其中 n 是流中元素的数量。

  2. 并行流:并行流的性能受到多个因素的影响。一方面,并行处理可以利用多核处理器的优势,加快处理速度。但是,在并行流中使用 distinct() 方法时,除了每个子流的去重操作外,还需要进行合并操作,这可能会引入额外的开销。此外,如果数据分布不均匀,导致各个子流处理的数据量差异较大,会出现负载不均衡的情况,从而降低并行处理的效率。

性能优化策略

1. 减少流中元素的数量

在使用 distinct() 方法之前,尽量减少流中元素的数量。例如,可以先对数据进行过滤,去除不需要的元素,然后再进行去重操作。

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

public class FilterBeforeDistinct {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5, 6, 7, 8, 8, 9);
        List<Integer> filteredAndDistinct = numbers.stream()
               .filter(n -> n > 3)
               .distinct()
               .collect(Collectors.toList());
        System.out.println(filteredAndDistinct);
    }
}

在上述代码中,我们先通过 filter(n -> n > 3) 方法过滤掉小于等于 3 的元素,然后再进行去重操作。这样可以减少 distinct() 方法需要处理的元素数量,从而提高性能。

2. 优化对象的 equals 方法

distinct() 方法依赖于 Object.equals(Object) 方法来判断元素是否重复。如果自定义类作为流中的元素,确保 equals 方法的实现高效。避免在 equals 方法中进行复杂的计算。

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && name.equals(person.name);
    }

    @Override
    public int hashCode() {
        return 31 * name.hashCode() + age;
    }
}

在上述 Person 类中,equals 方法首先进行快速的引用比较(this == o),然后再进行详细的属性比较。同时,合理实现 hashCode 方法,以确保在使用 HashSet 作为底层数据结构时,能高效地判断元素是否重复。

3. 避免不必要的装箱和拆箱

在处理基本数据类型时,尽量使用专门的流(如 IntStreamLongStreamDoubleStream),而不是使用 Stream<Integer>Stream<Long> 等包装类型的流。因为包装类型的流会涉及装箱和拆箱操作,这会带来额外的性能开销。

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

public class PrimitiveStreamVsObjectStream {
    public static void main(String[] args) {
        // 使用 IntStream
        IntStream intStream = IntStream.of(1, 2, 2, 3, 4, 4, 5);
        intStream.distinct().forEach(System.out::println);

        // 使用 Stream<Integer>
        Stream<Integer> objectStream = Stream.of(1, 2, 2, 3, 4, 4, 5);
        objectStream.distinct().forEach(System.out::println);
    }
}

在上述代码中,IntStream 直接处理 int 基本类型,避免了装箱和拆箱操作,相比 Stream<Integer> 性能更高。

4. 谨慎使用并行流

虽然并行流在理论上可以提高性能,但在实际应用中,需要根据数据量和计算复杂度来判断是否适合使用并行流。对于数据量较小或者计算复杂度较低的任务,并行流可能会引入过多的开销,反而降低性能。

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

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

        long sequentialStartTime = System.currentTimeMillis();
        numbers.stream()
               .distinct()
               .collect(Collectors.toList());
        long sequentialEndTime = System.currentTimeMillis();

        long parallelStartTime = System.currentTimeMillis();
        numbers.parallelStream()
               .distinct()
               .collect(Collectors.toList());
        long parallelEndTime = System.currentTimeMillis();

        System.out.println("Sequential stream time: " + (sequentialEndTime - sequentialStartTime) + " ms");
        System.out.println("Parallel stream time: " + (parallelEndTime - parallelStartTime) + " ms");
    }
}

在上述代码中,我们分别使用顺序流和并行流对相同的数据进行去重操作,并记录它们的执行时间。通过多次运行这个程序,可以观察到在数据量较小时,顺序流可能会比并行流更快。

5. 自定义去重策略

在某些情况下,默认的基于 equals 方法的去重策略可能无法满足需求,或者性能不够理想。此时,可以考虑自定义去重策略。例如,我们可以根据对象的某个特定属性进行去重。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

class Product {
    private String name;
    private double price;

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

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }
}

public class CustomDistinct {
    public static void main(String[] args) {
        List<Product> products = Arrays.asList(
                new Product("Product1", 10.0),
                new Product("Product2", 20.0),
                new Product("Product1", 15.0),
                new Product("Product3", 30.0)
        );

        List<Product> distinctProducts = products.stream()
               .collect(Collectors.collectingAndThen(
                        Collectors.toMap(Product::getName, p -> p, (p1, p2) -> p1.price < p2.price? p1 : p2),
                        map -> new ArrayList<>(map.values())));

        distinctProducts.forEach(p -> System.out.println("Name: " + p.getName() + ", Price: " + p.getPrice()));
    }
}

在上述代码中,我们通过 Collectors.toMap 方法根据产品的名称进行去重,并选择价格较低的产品。这种自定义的去重策略可以满足特定的业务需求,并且在某些情况下可能比默认的 distinct() 方法性能更好。

6. 利用缓存

如果流中的数据是重复生成的,或者生成数据的过程比较耗时,可以考虑利用缓存来存储已经处理过的数据,从而减少 distinct() 方法的处理次数。

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

public class CacheForDistinct {
    private static final Map<Integer, Boolean> cache = new HashMap<>();

    public static void main(String[] args) {
        Stream<Integer> stream = Stream.generate(() -> {
            int num = (int) (Math.random() * 100);
            return num;
        }).limit(1000);

        stream.filter(num -> {
            if (cache.containsKey(num)) {
                return false;
            } else {
                cache.put(num, true);
                return true;
            }
        }).forEach(System.out::println);
    }
}

在上述代码中,我们使用一个 HashMap 作为缓存,在处理流中的元素之前,先检查该元素是否已经在缓存中。如果已经存在,则过滤掉该元素;否则,将其添加到缓存中并保留。这样可以减少 distinct() 方法需要处理的元素数量,提高性能。

7. 优化数据结构

如果可能,在将数据转换为流之前,选择合适的数据结构来减少重复元素的产生。例如,如果数据来自于数据库查询,可以在 SQL 查询中使用 DISTINCT 关键字来直接获取不重复的数据,而不是在 Java 代码中通过 distinct() 方法进行去重。

// 假设这里有一个简单的数据库查询示例
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

public class DatabaseQuery {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String user = "root";
        String password = "password";
        List<Integer> numbers = new ArrayList<>();

        try (Connection conn = DriverManager.getConnection(url, user, password);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT DISTINCT number FROM numbers_table")) {
            while (rs.next()) {
                numbers.add(rs.getInt("number"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println(numbers);
    }
}

在上述代码中,通过在 SQL 查询中使用 DISTINCT 关键字,直接从数据库中获取不重复的数据,避免了在 Java 中对大量数据进行去重操作,从而提高了整体性能。

总结性能优化要点

  1. 预处理数据:在使用 distinct() 方法之前,通过过滤等操作减少流中元素的数量。
  2. 优化 equals 方法:对于自定义类,确保 equals 方法和 hashCode 方法的高效实现。
  3. 避免装箱拆箱:优先使用基本数据类型的流。
  4. 谨慎并行处理:根据数据量和计算复杂度决定是否使用并行流。
  5. 自定义策略:在必要时,实现自定义的去重策略。
  6. 利用缓存:如果数据重复生成,利用缓存减少处理次数。
  7. 优化数据结构:在数据获取阶段选择合适的数据结构或查询方式,减少重复数据。

通过以上性能优化策略,可以有效地提升 Java Stream distinct 方法的性能,使得在处理集合数据去重时更加高效。无论是处理小规模数据还是大规模数据,都能够根据实际情况选择最合适的优化方法,从而提高程序的整体性能。