## 当引用第三方包的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 Predicate distinctByKey(Function keyExtractor) { Map map = new ConcurrentHashMap<>(); return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } ``` 要理解 `distinctByKey` 方法中 `map` 的生命周期及其被捕获的原因,需要结合 Java 的**变量作用域**、**lambda 表达式的闭包特性**以及**流的执行机制**来分析。 ### 一、`map` 的生命周期细节 `map` 是 `distinctByKey` 方法内部创建的局部变量,其生命周期可分为以下阶段: #### 1. **创建阶段:方法调用时初始化** 当调用 `distinctByKey(keyExtractor)` 方法时,方法体首先执行 `new ConcurrentHashMap<>()`,此时 `map` 被创建并初始化(分配内存、初始化哈希表等)。 此时 `map` 的作用域仅限于 `distinctByKey` 方法内部,是一个**局部变量**。 #### 2. **存活阶段:被 lambda 捕获,随流处理存活** 方法返回的 `Predicate` 是一个 lambda 表达式:`t -> map.putIfAbsent(...)`。 这个 lambda 表达式**捕获了方法内部的 `map` 变量**(即持有对 `map` 的引用),因此即使 `distinctByKey` 方法执行结束,`map` 也不会被立即销毁 —— 因为它被外部的 `Predicate` 对象引用着。 当这个 `Predicate` 被传递给流的 `filter` 操作(如 `stream.filter(distinctByKey(...))`)时: - 流的中间操作(如 `filter`)仅会保存 `Predicate` 对象,不会立即执行。 - 直到流的**终端操作**(如 `collect`、`forEach`)被调用时,流才开始处理元素,此时 `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 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` 会累积多个流的状态,导致逻辑错误。