开发者

Java Stream的distinct去重原理分析

目录
  • 一、distinct 的基础用法与核心特性
  • 二、distinct 的底层实现原理
    • 1. 顺序流中的去重实现
    • 2. 并行流中的去重优化
  • 三、去重逻辑的核心依赖:hashCode 与 equals
    • 1. 自定义对象的去重规则
    • 2. 常见误区:仅重写 equals 不重写 hashCode
  • 四、distinct 的性能影响与优化策略
    • 1. 性能损耗的主要原因
    • 2. 大数据集的去重优化
    • 3. 并行流去重的参数调优
  • 五、特殊场景的去重方案
    • 1. 基于部分属性的去重
    • 2. 去重并保留最新元素
    • 3. 模糊去重(非精确匹配)
  • 六、性能对比:distinct 与其他去重方式
    • 总结

      一、distinct 的基础用法与核心特性

      distinct()是 Stream API 中的有状态中间操作,用于移除流中的重复元素,其底层依赖元素的hashCode()equals()方法。用法示例:

      List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4);
      List<Integer> unique = numbers.stream()
          .distinct()
          .collect(Collectors.toList());  // [1, 2, 3, 4]

      核心特性

      • 去重逻辑基于元素的唯一性标识,而非内存地址;
      • 保持元素首次出现的顺序;
      • 属于有状态操作,处理过程中需维护已出现元素的集合。

      二、distinct 的底层实现原理

      1. 顺序流中的去重实现

      顺序流中,distinct()通过HashSet存储已处理元素,流程如下:

      • 遍历流中的每个元素;
      • 对每个元素计算hashCode(),检查HashSet中是否存在相同哈希值的元素;
      • 若存在,进一步通过equals()比较内容,相同则过滤;
      • 若不存在,将元素添加到HashSet并保留在流中。

      源码关键片段(JDK 17):

      // ReferencePipeline.Java
      public final Stream<P_OUT> distinct() {
          return new DistinctOps<P_OUT, P_OUT>(this);
      }
       
      // DistinctOps.java
      @Override
      public void accept(P_OUT t) {
          if (set.add(t)) {  // 调用HashSet的add方法,返回false表示重复
              down.accept(t);
          }
      }

      2. 并行流中的去重优化

      并行流中,distinct()使用ConcurrentHashMap或分段处理提升性能:

      • 将流分割为多个子任务,每个子任务维护独立的HashSet
      • 子任务处理完成后,合并所有HashSet的结果;
      • 合并时使用HashMap去重,避免并发冲突。

      并行处理示意图

      +----------------+     +----------------+     +----------------+
      |  子任务1: HashSet |---->|  子任务2: HashSet |---->|  合并阶段: HashMap |
      |  存储元素A,B,C   |     |  存储元素B,D,E   |     |  最终结果A,B,C,D,E |
      +----------------+     +----------------+     +----------------+

      三、去重逻辑的核心依赖:hashCode 与 equals

      1. 自定义对象的去重规则

      若需对自定义对象去重,必须正确重写hashCode()equals()

      class User {
          private String id;
          private String name;
          
          @Override
          public int hashCode() {
              return Objects.hash(id);  // 仅用id计算哈希值
          }
          
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (o == null || getClass() != o.getClass()) return false;
              User user = (User) o;
              return Objects.equals(id, user.id);  // 仅比较id
          }
          // 其他方法省略
      }
       
      // 使用示例
      List<User> users = Arrays.asList(
          new User("1", "Alice"),
          new User("1", "Bob"),  // id相同,会被去重
          new User("2", "Charlie")
      );
      List<User> uniqueUsers = users.stream()
          .distinct()
          .collect(Collectors.toList());  // 保留两个用户

      2. 常见误区:仅重写 equals 不重写 hashCode

      若只重写equals,会导致去重失效,因为HashSet首先通过hashCode判断元素是否存在:

      class ErrorUser {
          private String id;
          // 错误:未重写hashCode
          @Override
          public boolean equals(Object o) {
              // 正确实现equals...
          }
      }
      // 使用distinct时,两个id相同的ErrorUser可能因hashCode不同被视为不同元素

      四、distinct android的性能影响与优化策略

      1. 性能损耗的主要原因

      • 内存占用:需存储所有已出现元素,大数据集可能导致 OOM;
      • 哈希计算开销:每个元素需计算hashCode并进行哈希表查找;
      • 并行流的合并开销:多线程环境下的集合合并操作耗时。

      2. 大数据集的去重优化

      • 预排序 + 相邻去重:对有序流使用distinct()效率更高,因重复元素相邻时哈希表查找次数减少
      // 优化前:无序流去重
      List<Integer> randomData = getRandomNumbers(1000000);
      randomData.stream().distinct().count();  // 全量哈希表查找
       
      // 优化后:先排序再去重
      randomData.stream()
          .sorted()
          .distinct()
          .count();  // 相邻重复元素只需一次比较
      • 使用 Primitive Stream 减少装箱
      // 低效:对象流装箱
      Stream<Integer> boxedStream = data.stream().distinct();
       
      // 高效:IntStream直接操作
      IntStream primitiveStream = data.stream().mapToInt(Integer::intValue).distinct();
      • 分块处理大集合:避免一次性加载所有元素到内存
      // 分块去重示例
      int chunkSize = 100000;
      List<Integer> result = new ArrayList<>();
      for (int i = 0; i < data.size(); i += chunkSize) {
          int end = Math.min(i + chunkSize, data.size());
          List<Integer> chunk = data.subList(i, end);
          result.addAll(chunk.stream().distinct().collect(Collectors.toList()));
      }
      // 最后再去重一次合并结果
      List<Integer> finalResult = result.stream().distinct().collect(Collectors.toList());

      3. 并行流去重的参数调优

      通过自定义Spliterator控制分块大小,减少合并开销:

      class EfficientSpliterator implements Spliterator<Integer> {
          private final List<Integer> list;
          private int index;
          private static final int CHUNK_SIZE = 10000;  // 分块大小
          
          public EfficientSpliterator(List<Integer> list) {
              this.list = list;
              this.index = 0;
          }
          
          @Override
          public Spliterator<Integer> trySplit() {
              int size = list.size() - index;
              if (size < CHUNK_SIZE) return null;
              int splitPos = index + size / 2;
              Spliterator<Integer> spliterator = 
                  new EfficientSpliterator(list.subList(index, splitPos));
              index = splitPos;
              return spliterator;
          }
          // 其他方法省略...
      }
       
      // 使用示例
      List<Integer> data = ...;
      Stream<Integer> optimizedStream = StreamSupport.stream(
          new EfficientSpliterator(data), true);  // 启用并行

      五、特殊场景的去重方案

      1. 基于部分属性的去重

      若需根据对象的部分属性去重(而非全部属性),可结合mapcollect

      class Product {
          private String id;
        编程客栈  private String name;
          private double price;
          // 构造器、getter省略
      }
       
      // 按id去重
      List<Product> uniqueProducts = products.stream()
          .collect(Collectors.collectingAndThen(
              Collectors.toMap(Product::getId, p -> p, (p1, p2) -> p1),
              map -> new ArrayList<>(map.values())
          ));

      2. 去重并保留最新元素

      在日志等场景中,需按时间戳去重并保留最新记录:

      class LogEntry {
          private String message;
          private long timestamp;
          // 构造器、getter省略
      }
       
      List<LogEntry> latestLogs = jslogs.stream()
          .collect(Collectors.toMap(
              LogEntry::getMessage, 
              entry -> entry, 
              (oldEntry, newEntry) -> newEntry.getTimestamp() > oldEntry.getTimestamp() 
                  ? newEntry : oldEntry
          ))
          .values()
          .stream()
          .collect(Collectors.toList());

      3. 模糊去重(非精确匹配)

      如需基于相似度去重(如字符串编辑距离),需自定义去重逻辑:

      List<String> fuzzyUnique = strings.stream()
          .filter(s -> !strings.stream()
              .anyMatch(t -> s != t && levenshteinDistance(s, t) < 2))
          .collect(Collectors.toList());

      六、性能对比:distinct 与其他去重方式

      去重方式大数据集性能内存占用实现复杂度适用场景
      Stream.distinct()高(存储所有元素)通用去重
      先排序 + 相邻去重有序数据去重
      HashSet 直接去重简单集合去python重
      分块去重超大数据集去重

      总结

      distinct()作为 Stream API 中的基础操作,其核心去重逻辑依赖于hashCode()equals()的正确实现,而性能优化的关键在于:

      • 数据有序性利用:先排序再去重可减少哈希表查找次数;
      • 内存占用控制:对大数据集采用分块处理,避免一次性存储所有元素;
      • 基础类型优化:使用IntStream等避免装箱损耗;
      • 并行处理调优:通过自定义Spliterator控制分块大小,减少合并开销。

      理解distinct()的底层实现原理,不仅能避免自定义对象去重时的常见错误,更能在处理大规模数据时选择合适的优化策略。记住:去重操作的本质是空间与时间的权衡,根据具体业务场景(数据规模、有序性、精确性要求)选择最优方案,才能实现性能与功能的平衡。

      以上就是Java StreazRIiLLBm的distinct去重原理分析的详细内容,更多关于Java Stream distinct去重的资料请关注编程客栈(www.devze.com)其它相关文章!

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜