1.LinkedHashSet类基本介绍
LinstendHash是hashset的子类,底层维护的是LinstendHashmap->hashmap 的子类
LinstendHash是有序的,是一个双向链表形式 底层是数组+双向链表
2.LinstendHashSet底层添加元素原理机制
第一次添加元素会将数组扩容到16,存放的节点类型是存放节点类型是 LinkedHashMap$Entry
LinstendHash是hashset的子类,hashset底层是hashmap,底层添加元素还是调用的hashmap的add方法
添加一个元素时,先得到hash值,会转成索引值
找到数据存储表table,看这个索引位置是否存放的有元素,
如果没有,直接加入
如果有,调用equals比较,如果相同,放弃添加,如果不同,则添加到最后
3.LinstendHashSet添加对象源码解读
public static void main(String[] args) {
HashSet<Object> objects = new HashSet<>();
System.out.println(objects.add(new String("123"));
System.out.println(objects.add(new String("123"));
}
想必大家都知道这是一道很经典的面试题,这两个对象只能成功添加一个,至于为什么只能添加一个,我们通过这道题目对LinstendHashSet添加元素的机制进行源码解读,
第一步 执行hashmap的add方法
public boolean add(E e) { e = "java"
return map.put(e, PRESENT)==null;
}
第二步 执行put方法,该方法会执行hash(key)方法 ,会得到一个hash值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
第三步 执行hash方法
从这里不难看出,null的hash值为0,对象的hash值就是调用对象的hashcode方法然后再进行处理得到hash值
所以说通过这个方法,我们不难看出上述两个字符串都为123的两个str对象,他们的hash值是一样的,因为String类重写了hashcode方法,感兴趣的小伙伴可以看下String的hashcode方法的原码,这里我不做过多解释
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我们接着往下走
第四步 执行putVal方法 这一步才是添加元素最核心的方法,具体分析看下面代码注释
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果table为空或者大小为0,第一次添加元素table肯定为空,所以调用resize()方法第一次扩容到
//长度为16的Node数组,resize()方法我不再追加
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 这里就是根据刚才的到的hash值来计算元素在tab存放的索引,
// 由于上述两个str对象的hash值是一样的,所以上述两个对象的下标i是一样的,所以我们在第二次添加new //String("123")对象的时候,就不会进入到这个if里,因为tab[i]不为空,所以直接走下面的else代码,
if ((p = tab[i = (n - 1) & hash]) == null) //i=hash%n 判断tab[i] 是否为空 如果为证明此索引没有被添加过对象
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 当前索引的hash值和准备添加的hash是否相等 && 当前索引的key值和准备添加的key值是
//否相等 || (准备加入的key不等于null && 准备加入的key.equals(当前索引的key值))
// 由于两个str的hash值一样,并且两个对象的值一样, 直接将e指向tab[i],这个tab[i]就是之
//前第一次添加的str对象,最后将e返回,读到这里小伙伴们应该也明白了第二次没有添加对象了吧
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
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) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3.根据实际距离解读LinstendHashSet添加对象的过程
1.我们这里有一个car类,维护了name和price属性,有参构造器也为name和price,我们这里认为 如果name和price一样就是同一个元素则不能添加成功,添加三个对象
Car("宝马",110) ;Car("宝马1",110); Car("奥迪",110)
分析 如果name和price一样就是同一个元素那我们就要重新写car的hashcode方法和equals方法,
根据name和price来控制hashcode和两个对象值是否一样
class Car {
private String name;
private double price;
public Car(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return Double.compare(car.price, price) == 0 &&
Objects.equals(name, car.name);
}
@Override
public int hashCode() {
return Objects.hash(name); // 这里hashcode我只用name属性来控制,事项给大家演示下hash值一样,对象的值不一样的这种场景
}
}
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add(new Car("宝马",100));
System.out.println(linkedHashSet.add(new Car("宝马",110)));
System.out.println(linkedHashSet.add(new Car("宝马1",110)));
System.out.println(linkedHashSet.add(new Car("奥迪",110)));
}
}
然后我们通过图解分析LinstendHashSet添加对象的过程
以上就是通过LinkedHashSet底层原理和一道比较经典的面试题向大家简单介绍了LinkedHashSet添加元素的底层原理,