文章目录
HashSet
一、HashSet特点
1.无序
使用HashSet不能保持元素插入顺序
HashSet<String> hashSet = new HashSet<>();
hashSet.add("zhangsan");
hashSet.add("lisi");
hashSet.add("wangwu");
for (String s : hashSet) {
System.out.println(s);
}
输出结果如下:
2.唯一
HashSet中的元素不能重复,可以存储null
HashSet<String> hashSet = new HashSet<>();
hashSet.add("zhangsan");
hashSet.add("lisi");
hashSet.add("wangwu");
hashSet.add("zhangsan");
hashSet.add(null);
hashSet.add(null);
System.out.println("hashSet.size-->" + hashSet.size());
for (String s : hashSet) {
System.out.println(s);
}
输出结果如下:
二、hashcode和equal问题
1.学生类
这里使用学生类作为测试时数据,成员属性如下:
private int id; //学号
private String name; //姓名
private int age; //年龄
2.问题
创建五个学生类实例,存入HashSet中
Student stu1 = new Student(1, "zhangsan", 20);
Student stu2 = new Student(2, "lisi", 21);
Student stu3 = new Student(3, "wangwu", 19);
Student stu4 = new Student(4, "zhaoliu", 20);
Student stu5 = new Student(1, "zhangsan", 20);
hashSet.add(stu1);
hashSet.add(stu2);
hashSet.add(stu3);
hashSet.add(stu4);
hashSet.add(stu5);
打印输出hashSet的size及存储数据,结果如下:
从上图打印结果发现,hashSet的size=5,stu1和stu2的属性都是相同的,为什么size不是4呢?接下来从源码进行分析。
3.源码分析
private transient HashMap<E,Object> map;
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
进入HashSet源码中发现,HashSet底层是通过HashMap实现的,HashMap的数据存储在HashMap的key中,接着进入HashMap查看put方法是如何对数据进行去重。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true); //通过hash方法计算key的hash值
}
hash(key):
static final int hash(Object key) {
int h;
//key.hashCode()使用的是Object的hashCode(),通常是通过将对象的内部地址转换为整数
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
通过debug功能,查看stu1和stu5的hash值,如下所示:
stu1:
stu5:
接着再看到 putVal(hash(key), key, value, false, true) 方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//获取table数组,table数组为Node<K, V>数组,用来存储键值对
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
/*
(n - 1) & hash:数组下标最大值与hash值做与按位与运算,获取一个下标x,
若tab[x] == null,即未发生hash冲突,tab数组中没有与新插入的值相同的
元素,插入成功。
*/
tab[i] = newNode(hash, key, value, null);
else { //发生hash冲突
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//hash值和key均相同,则判断存在该建
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // 已存在该键,如果是HashMap,则用新值替换旧值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
通过该方法,可以看出去重时先比较hash值(效率高),如果hash值不同,则认为没有重复,如果hash值相同,则通过equals方法进行比较。
4.解决方法
stu1和stu5的属性值都是相同的,我们认为stu1和stu5是同一个Student,所以stu1和stu5的hash值应该是相同的,其次,我们还需要重写 equals 方法(默认比较的是对象地址),如下:
public class Student {
private int id;
private String name;
private int age;
//Constructor
//Getter and Setter
//toString()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return id == student.id && age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name, age);
}
}
再次测试,结果如下:
三、HashSet扩容机制
因为HashSet的底层就是HashMap,所以HashSet扩容机制与HashMap扩容一致------HashMap扩容机制
总结
一、HashSet特点
- 1.HashSet底层是一个HashMap
- HashSet不确保插入顺序
- HashSet不能存在重复数据
(注:HashSet是线程非安全的)
二、equals和hashCode问题
- 使用HashSet存储自定义类时,需要重写equals方法和hashCode方法,具体实现根据实际需求
- equals 和 hashCode方法,重写了一种,另外一种也需要重写,保证当hash值相等时,obj.equals(other) == true;