为何Java中Protobuf对象与普通类对象在Set中的行为存在差异?
咱们先把问题拆解清楚:为什么用Protobuf生成的类创建值相同的对象,放进HashSet里会自动去重,而普通Java类做不到?这背后其实是Java集合的去重逻辑,加上Protobuf生成类的特殊实现导致的。
先看普通Java类的问题
你写的NodeJava类默认继承自Object,而Object类的equals()和hashCode()都是基于对象的内存地址工作的:
equals():只有两个引用指向同一个对象时才返回truehashCode():返回的是和对象内存地址相关的数值
所以每次你new NodeJava("localhost", 12346),都会在堆里创建一个新对象,内存地址完全不一样。HashSet判断元素重复的逻辑是:先对比hashCode()是否相等,若相等再调用equals()确认。这两个新对象的hashCode()不同,equals()也返回false,HashSet就会把它们当成不同元素,最终输出的size是3。
要是想让普通类也能去重,你得手动重写这两个方法,让它们基于字段值来判断:
import java.util.Objects; public class NodeJava { private String host; private Integer port; public NodeJava(String host, Integer port) { this.host = host; this.port = port; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; NodeJava nodeJava = (NodeJava) o; return Objects.equals(host, nodeJava.host) && Objects.equals(port, nodeJava.port); } @Override public int hashCode() { return Objects.hash(host, port); } }
改完之后再运行相同的代码,HashSet就能识别出值相同的元素,size会变成2。
再看Protobuf类的特殊之处
Protobuf生成的Java类,天生就解决了这个问题,核心有两点:
1. 自动生成基于字段的equals()和hashCode()
Protobuf编译器在生成Java类时,会根据你定义的消息字段(比如host和port)自动生成equals()和hashCode()方法。举个例子,NodeProto的equals()会逐字段比较host和port的实际值,hashCode()会把这两个字段的值组合起来计算哈希码。
这意味着,哪怕是两个不同的实例,只要字段值完全一样,equals()就返回true,hashCode()也相同,HashSet会认为它们是同一个元素,自然会去重。
2. 可选的实例池化优化
你提到的“创建值相同的新对象时返回同一个对象”,是Protobuf的一个性能优化——实例池化。当你调用Builder的build()方法时,Protobuf会先检查内部缓存里有没有字段值完全相同的实例,如果有,就直接返回那个实例,而不是创建新对象。
比如你两次调用create("localhost", 12346),第二次build()会发现已经有一个一模一样的实例,直接复用它,所以两个引用指向同一个对象。不过要注意:就算没有这个池化机制,只要equals()和hashCode()正确,HashSet依然会去重——池化只是减少了对象创建的开销,不是去重的必要条件。
核心差异总结
| 对比项 | 普通Java类(未重写方法) | Protobuf生成类 |
|---|---|---|
equals()判断逻辑 | 比较对象内存地址 | 比较所有字段的实际值 |
hashCode()计算逻辑 | 基于内存地址生成 | 基于所有字段的值计算哈希码 |
| 对象创建策略 | 每次new生成新实例 | 值相同时可能复用已有实例(池化) |
| HashSet去重效果 | 无法去重 | 自动去重 |
内容的提问来源于stack exchange,提问作者Sudheesh Singanamalla

