118 lines
5.6 KiB
Markdown
118 lines
5.6 KiB
Markdown
## 当引用第三方包的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` 的生命周期细节
|
||
|
||
`map` 是 `distinctByKey` 方法内部创建的局部变量,其生命周期可分为以下阶段:
|
||
|
||
#### 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` 对象,不会立即执行。
|
||
- 直到流的**终端操作**(如 `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<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` 会累积多个流的状态,导致逻辑错误。
|