为什么HashSet存储字符串的时候,当字符串内容相同的时候它只存储一个值呢?
public class HashSetDemo {
public static void main(String[] args) {
HashSet<String> hs = new HashSet<String>();
hs.add("I");
hs.add("here");
hs.add("am");
hs.add("here");
for (String s : hs) {
System.out.println(s);
}
}
}
输出:
am
here
I
我们添加了两次”here”,但是输出的结果只有一个,为什么呢?
为了解决这个问题,我们去查看add()方法的源码:
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null; //可以发现,我们添加的元素是作为键存储的
}
transient int hashSeed = 0;
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode(); //调用对象的hashCode()方法。
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
public V put(K key, V value) {
if (table == EMPTY_TABLE) { //看哈希表是否为空,如果为空就开辟空间
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key); //这里调用了hash方法,和对象的哈希值相关
int i = indexFor(hash, table.length); //在哈希表中查找hash值
for (Entry<K,V> e = table[i]; e != null; e = e.next) { //和以前的元素的哈希值进行比较
Object k;
//当哈希值并且地址值都一样 或者 equals比较结果一样的时候 他就不会进行添加,而是走if里面的代码
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
我们可以发现HashSet的底层调用的是Map的put方法。而在put方法里面,我们可以发现添加的元素作为键key来存储的。
一般来说,字符串不同的时候,它们的哈希值是不同的。哈希值相同并不代表两个元素相同。比如两个字符串他们的地址值不一样,但是他们的哈希值哟可能是一样的。哈希值是逻辑值,地址值是实际的物理值。
因此,我们可以知道add方法的底层主要取决于hashCode()和equals()方法。
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
他首先比较哈希值,如果哈希值相同,继续比较地址值 否则比较equals(),如果相同则不添加到集合,如果不同,就直接添加到集合中。
如果类没有重写这两个方法,默认使用的Object()对象的方法,一般来说不会相同。但是String它重写了hashCode()和equals()方法,所以它可以把内容相同的字符串去掉,只留下一个。
那么HashSet存储自定义对象的时候是如何做到保证元素唯一性的呢?要求如果两个对象的成员变量的值都相同,则为同一个元素
Student类:
public class Student {
private int age;
private String name;
public Student(int age, String name) {
super();
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
测试类:
public class HashSetDemo2 {
public static void main(String[] args) {
HashSet<Student> hs = new HashSet<Student>();
Student s1 = new Student(23, "lili");
Student s2 = new Student(13, "xiong");
Student s3 = new Student(43, "xixi");
Student s4 = new Student(13, "xiong");
Student s5 = new Student(18, "xixi");
hs.add(s1);
hs.add(s2);
hs.add(s3);
hs.add(s4);
hs.add(s5);
for (Student s : hs) {
System.out.println(s.getName() + "---" + s.getAge());
}
}
}
输出:
lili---23
xixi---18
xixi---43
xiong---13
通过上面的源码我们可以知道,HashSet底层依赖的是hashCode()和equals()方法。我们的Student类中如果不重写这两个方法,就默认使用的是Object类中的这两个方法,因此即使这两个对象的成员变量的值都相同,他也认为是两个对象,因为是通过new创建的。并没有达到效果。
因此为了达到我们的目的,我们就需要在Student了中重写这两个方法才能达到效果。通过右键自动生成这两个方法即可。
哈希表:是一个元素为链表的数组。综合了数组和链表的优点。
如何重写HashCode():
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
因为成员变量响应哈希值,所以我们可以把成员变量值相加即可。上面的代码为什么要乘以prime呢?是为了防止出现20+40和10+50的情况,乘以一个prime就不会出现这种问题了。