1. 前言
今天,我们来探讨一个实际中不常用但却比较有意思的问题。它能帮助你理解 “HashSet中的键值是唯一的,不可重复的” 这句话的真正含义,也考验你对问题的思考深度。
注:实际应用中,我们一般是用 ArrayList 集合来存储相同的字符串的,不会用 HashSet 来存。
我们平时都看到或听说 HashSet 是不能用来存放重复的字符串的,是真的存放不了吗?如果面试问你这个问题,你能给出解决方案吗?
2. 参考解答
先给出参考解答,然后我们再来分析为什么。
解答: 虽然我们不能用 HashSet 来存放 String 类型重复的字符串,但我们可以用 HashSet 来存储 StringBuilder 类型重复的字符串呀。
public class HashSetTest {
public static void main(String[] args){
// 用 HashSet 来存放 String 类型的重复的字符串会发生什么?
HashSet<String> hs1 = new HashSet<>();
String s1 = new String("aaa");
String s2 = new String("aaa");
String s3 = new String("aaa");
hs1.add(s1);
hs1.add(s2);
hs1.add(s3);
System.out.println("hs1:"+hs1); // 重复的字符串是存不进去的
// 用 HashSet 来存放 StringBuilder 类型的重复的字符串又会发生什么?
HashSet<StringBuilder> hs2 = new HashSet<>();
StringBuilder sb1 = new StringBuilder("aaa");
StringBuilder sb2 = new StringBuilder("aaa");
StringBuilder sb3 = new StringBuilder("aaa");
hs2.add(sb1);
hs2.add(sb2);
hs2.add(sb3);
System.out.println("hs2:"+hs2); // 咦,结果发现重复的字符串也能存进去了
// 那为什么呢?我们来打印一个各个对象的hashCode看一下
System.out.println("s1的hashCode:"+s1.hashCode());
System.out.println("s2的hashCode:"+s2.hashCode());
System.out.println("s3的hashCode:"+s3.hashCode());
System.out.println("sb1的hashCode:"+sb1.hashCode());
System.out.println("sb2的hashCode:"+sb2.hashCode());
System.out.println("sb3的hashCode:"+sb3.hashCode());
}
}
复制代码
输出结果:
hs1:[aaa]
hs2:[aaa, aaa, aaa]
s1的hashCode:96321
s2的hashCode:96321
s3的hashCode:96321
sb1的hashCode:356573597
sb2的hashCode:1735600054
sb3的hashCode:21685669
复制代码
从打印结果来看,我们是不能用 HashSet 来存放 String 类型的重复字符串的(如hs1),但我们是可以用HashSet来存放 StringBuilder 类型的重复字符串。
3. 为什么?
从打印的 hashCode 来看,String 类型,相同字符串的不同 String 对象哈希值是一样的。而对于 StringBuilder 类型,相同字符串的不同对象哈希值是不同的。
要知道这个问题的答案,我们首先得了解 JDK 是如何判断两个对象是否相同的。
那 JDK 是如何判断两个对象是否相同的呢?
参考解答: JDK 会先判断两个对象的 hashCode 是否相同,如果 hashCode 不同,则说明肯定是两个不同的对象了;如果 hashCode 相同再通过 equals() 方法进行进一步比较,如果 equals 方法返回 true,则说明两个对象是相同的,如果equals方法返回 false 说明两个对象不同。
具体验证思路如果你感兴趣,请查看: JDK 是如何判断两个对象是否相同的?判断的流程是什么?
那为什么相同字符串的不同 String 对象哈希值是一样的,而且还被 JDK 判断为相同的对象了呢?
因为 String 类复写了 Object 类的 hashCode() 和 equals() 方法,并实现了自己的 hashCode 值生成算法和 equals 的比较规则,具有相同字符串内容的不同 String 对象在初始化时生成的 hashCode 值是一样的,并且 String 类 equals() 方法比较的是两个字符串的内容,而不是内存地址值,这两个条件同时成立, 这就使 JDK 把具有相同内容的不同 String 对象判断为相同的对象了,就不会存入 HashSet 集合中。
而 StringBuilder 为什么就可以呢?它相同内容的不同对象的哈希值值为什么是不同的?
查看 StringBuilder 类的源码你会发现,因为 StringBuilder 并没有复写 Object 类的 hashCode() 方法和 equals() 方法,StringBuilder 用的是父类 Object 类的 hashCode 生成算法,也就是用 native 层的 hashCode 生成算法,很大概率产生的哈希值是不一样的,即使产生了一样的哈希值,Object 类的 equals() 方法比较的是两个对象的内存地址,而不是两个对象的内容,这就使 JDK 把具有相同内容的 StringBuilder 对象判断为不同的对象,就可以存入 HashSet 集合中了。