Files
article/java/包装模式之java对象.md
2025-12-03 13:18:45 +08:00

5.6 KiB
Raw Blame History

当引用第三方包的java对象时重写equals

使用包装模式


 private static class VideoInfoWrapper {

        private final VideoInfo videoInfo;

        public VideoInfoWrapper(VideoInfo videoInfo) {
            this.videoInfo = videoInfo;
        }

        public VideoInfo unwrap() {
            return videoInfo;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof VideoInfo)) {
                return false;
            }
            VideoInfo vi = (VideoInfo) obj;
            return videoInfo.id.equals(vi.id)
                    && videoInfo.width == vi.width
                    && videoInfo.height == vi.height;
        }

        @Override
        public int hashCode() {
            int n = 31;
            n = n * 31 + videoInfo.id.hashCode();
            n = n * 31 + videoInfo.height;
            n = n * 31 + videoInfo.width;
            return n;
        }
    }

自定义过滤函数实现去重

private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
        Map<Object, Boolean> map = new ConcurrentHashMap<>();
        return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }

要理解 distinctByKey 方法中 map 的生命周期及其被捕获的原因,需要结合 Java 的变量作用域lambda 表达式的闭包特性以及流的执行机制来分析。

一、map 的生命周期细节

mapdistinctByKey 方法内部创建的局部变量,其生命周期可分为以下阶段:

1. 创建阶段:方法调用时初始化

当调用 distinctByKey(keyExtractor) 方法时,方法体首先执行 new ConcurrentHashMap<>(),此时 map 被创建并初始化(分配内存、初始化哈希表等)。

此时 map 的作用域仅限于 distinctByKey 方法内部,是一个局部变量

2. 存活阶段:被 lambda 捕获,随流处理存活

方法返回的 Predicate<T> 是一个 lambda 表达式:t -> map.putIfAbsent(...)

这个 lambda 表达式捕获了方法内部的 map 变量(即持有对 map 的引用),因此即使 distinctByKey 方法执行结束,map 也不会被立即销毁 —— 因为它被外部的 Predicate 对象引用着。

当这个 Predicate 被传递给流的 filter 操作(如 stream.filter(distinctByKey(...)))时:

  • 流的中间操作(如 filter)仅会保存 Predicate 对象,不会立即执行。
  • 直到流的终端操作(如 collectforEach)被调用时,流才开始处理元素,此时 Predicate.test(t) 会被反复调用,每次调用都会通过捕获的引用操作同一个 map(记录已出现的键,实现去重)。

因此,map 在整个流处理期间(从终端操作开始到结束)一直存活,用于存储去重的状态。

3. 销毁阶段:引用链断裂后被回收

当流的终端操作执行完毕,流处理结束后:

  • 如果 Predicate 对象没有被其他地方引用(通常是一次性使用,用完即弃),则 Predicate 会成为垃圾回收GC的候选对象。
  • 由于 map 仅被这个 Predicate 引用,当 Predicate 被 GC 回收时,map 的引用计数变为 0也会被 GC 回收。

至此,map 的生命周期结束。

二、为什么 map 会被 lambda 捕获?

lambda 表达式(如 Predicate 的实现)本质是闭包Closure—— 它可以访问外部作用域(即 distinctByKey 方法)中的变量,这种访问通过 “捕获变量” 实现。具体原因如下:

1. 功能需求:需要共享状态

distinctByKey 的核心逻辑是 “通过记录已出现的键实现去重”,因此需要一个共享的容器(map)来存储这些键。

如果 map 不被 lambda 捕获,那么每次调用 Predicate.test(t) 时都会创建新的 map,无法记录历史状态,去重功能就会失效。

2. Java 语法对闭包的支持

Java 的 lambda 表达式可以捕获外部作用域的变量,但有严格限制:只能捕获 “有效最终变量effectively final(即变量声明后从未被修改,或被 final 修饰)。

distinctByKey 中:

  • map 被声明为 Map<Object, Boolean> map = new ConcurrentHashMap<>(),且后续从未被重新赋值(始终指向同一个 ConcurrentHashMap 实例),因此它是 “有效最终变量”。
  • 因此lambda 表达式可以合法地捕获 map 的引用(注意:捕获的是引用,不是变量本身),并在后续操作中通过该引用修改 map 内部的内容(如 putIfAbsent)。

3. 捕获的本质:延长变量生命周期

从内存角度看,map 原本是 distinctByKey 方法的局部变量,方法执行结束后本应被销毁。但由于 lambda 表达式(Predicate)持有对 map 的引用,map 的生命周期被延长,与 Predicate 的生命周期绑定 —— 只要 Predicate 还存在,map 就不会被回收。

三、关键结论

  • map 的生命周期:从 distinctByKey 方法调用时创建,到流处理结束且 Predicate 被回收时销毁,贯穿整个流的去重过程。
  • 被捕获的原因lambda 表达式需要通过共享 map 记录去重状态,而 Java 允许 lambda 捕获 “有效最终变量”,从而实现闭包对外部状态的访问。

这种设计使得 distinctByKey 方法能简洁地实现 “按键去重”,但需注意:同一个 Predicate 实例不能重复用于多个流,否则 map 会累积多个流的状态,导致逻辑错误。