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

118 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 当引用第三方包的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` 会累积多个流的状态,导致逻辑错误。