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