Java集合专题(九):Set接口的实现类之HashSet底层详解

HashSet全面说明

  1. HashSet实现了Set接口

  2. HashSet实际上是HashMap。

public HashSet() {
    map = new Hashmap<>();
}
  1. 可以存放null值,但是只能有一个null。
  2. HashSet不保证元素是有序的,取决于 hash 后,再确定索引的结果。(即,不保证存放元素的顺序和取出顺序一致)
  3. 不能有重复的元素/对象。
案例一
public class HashSet01 {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        System.out.println(set.add("john"));    //T
        System.out.println(set.add("lucy"));    //T
        System.out.println(set.add("john"));    //F
        System.out.println(set.add("jack"));    //T
        System.out.println(set.add("Rose"));    //T
        set.remove("john");
        System.out.println("set=" + set);		//3个
    }
}
===========输出结果===========
set=[Rose, lucy, jack]
//说明
//1. 在执行add方法后,会返回一个boolean值
//2. 如果添加成功,返回的是一个 true, 否则返回一个false
//3. 可以通过 remove 指定删除哪个对象
案例二
public class HashSet01 {
    public static void main(String[] args) {
        HashSet set = new HashSet();

        //4. HashSet 不能存放相同的元素/数据
        set.add("lucy");    //添加成功
        set.add("lucy");    //加入不了,因为这里的 Lucy 都指向的是同一个常量池
        
        set.add(new Dog("tom"));	//添加成功
        set.add(new Dog("tom"));	//添加成功
        System.out.println("set=" + set);
    }
}
class Dog{//定义了一个Dog
    private String name;

    public Dog(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}
===========输出结果===========
set=[Dog{name='tom'}, Dog{name='tom'}, lucy]

HashSet底层机制说明

分析 HashSet 底层是 HashMap,HashMap 底层是(数组 + 链表 + 红黑树)

模拟一个HashSet的底层(HashMap 的底层结构)

public class HashSetStructure {
    public static void main(String[] args) {
        //1. 创建一个数组,数组的类型是 Node[]
        //2. 有些人直接把这个 Node[] 数组称为 表
        Node[] table = new Node[16];
        System.out.println("table=" + table);
        //3. 创建节点
        Node john = new Node("john",null);

        table[2] = john;
        Node jack = new Node("jack",null);
        john.next = jack;  //将Jack 节点挂在到John
        Node rose = new Node("rose", null);
        jack.next = rose;  //将rose 节点挂在到Jack

        Node lucy = new Node("lucy", null);
        table[3] = lucy;
        System.out.println("table=" + table);
    }
}
class Node{//节点,存储数据,可以指向下一个节点,从而形成链表
    Object item;  //存放数据
    Node next; //指向下一个节点

    public Node(Object item, Node next) {
        this.item = item;
        this.next = next;
    }
}

在代码中打一个断点

在这里插入图片描述

执行后的结果,我们可以看到在索引为2和3的位置都挂上了节点。

在这里插入图片描述

添加元素源码分析

原理

在这里插入图片描述

步骤
  1. HashSet 底层是 HashMap。
  2. 添加一个元素时,先得到 hash 值 -> 会转成 -> 索引值。
  3. 找到存储数据表 table,看这个索引位置是否已经存放的有元素。
  4. 如果没有,直接加入。
  5. 如果有,调用 equals 方法比较,如果相同,就放弃添加,如果不相同,则添加到最后。
  6. 在 Java8 中,如果一条链表的元素个数达到 TREEIFY_THRESHOLD(默认是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)。
public class HashSetSource {
    public static void main(String[] args) {

        HashSet hashSet = new HashSet();//断点
        hashSet.add("java");
        hashSet.add("php");
        hashSet.add("java");
        System.out.println("set=" + hashSet);
    }
}
===========输出结果===========
set=[java, php]
源码解读
  1. 执行 HashSet 构造方法,获得一个 HashMap 的实例。
public HashSet() {
    map = new HashMap<>();
}
  1. 执行 add() 方法,该方法返回了一个以 map 对象调用的方法 put(K key, V value) ,在这个方法中有两个参数:
  • K Key:添加到集合中的数据(例如:hashSet.add(“java”))
  • V value:等于 PRESENT: private static final Object PRESENT = new Object();

public boolean add(E e) {//e = "java"
    return map.put(e, PRESENT)==null;
}
  1. 执行 put() 方法,该方法会执行 hash(key) 得到 key 对应的一个 hash 值 。计算 hash 值使用到的算法是:

(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);目的是为了让每一个 key 得到不同的 hash 值。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  1. 执行 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 就是 HashMap 中的一个属性,类型是 Mode[]
    //if语句表示如果当前 table 是 null,或者大小 = 0,就进行第一次扩容,到 16 个空间大小。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    	//(1)根据 key 得到的 hash 值去计算该 key 应该存放到 table 表的哪一个索引位置,并且
        //把这个位置的对象,赋值给辅助变量 p。
        //(2)判断 p 是否为 null
        //(2.1)如果 p 为 null,表示还没有存放过元素,就创建一个 Node
        //		newNode(hash, key, value, null) key, value两个值都是这一次操作传入的值。
    if ((p = tab[i = (n - 1) & hash]) == null)//判断 tap[i] 的位置上有没有元素。
        //(2.2)如果 p 不为 null,就将数据放在该位置
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //如果当前索引位置对应的链表的第一个元素和准备添加的 key 的 hash 值一样,并且满足下面的两个         //条件之一         
        //(1)准备加入的 key 和 p 指向的 Node 节点的 key 是同一个对象
        //(2)p 指向的 Node 节点的 key 的 equals 方法和准备加入的 key 比较后相同
        //就不能加入该元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //再判断 p 是不是一棵红黑树
        //如果是,就调用 putTreeVal() 方法,来进行添加
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //如果 table 对应索引的位置,已经是一个链表,就使用 for 循环比较
            //(1)依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后
            //	注意:在把元素添加到链表后,立即判断该链表是否已经达到8个节点
            //		 就调用 treeifyBin() 对当前链表进行树化,转成红黑树
            //		注意:在转成红黑树时还进行一个判断
            //		if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
            //			resize();
            //		如果上面的条件成立,就先对 table 表扩容,
            //		只有上面条件不成立时,才进行转换,变成红黑树
            //(2)依次和该链表的每一个元素比较过程中,如果有相同的情况,就直接 breaker。
            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();
    //HashMap 的一个空方法,方法体中什么都没有干,存在的目的是为了让 HashMap 的实现子类去实现。
    afterNodeInsertion(evict);
    return null;
}
/*
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
*/
  1. 在 putVal() 方法中又执行了 resize() 方法,这个方法是 HashMap 中的核心,主要就是实现对集合的扩容。
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;//将 table(第一次添加数据table为 null) 赋值给新数组 oldTab。
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;//临界值(12)
    int newCap, newThr = 0;
    if (oldCap > 0) {//第一次添加数据时 oldCap 为 0,所以不会进入到这个if循环
        //扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            //原数组长度大于最大容量(1073741824) 则将 threshold 设为 Integer.MAX_VALUE = 2147483647
            //接近 MAXIMUM_CAPACITY 的两倍
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 新数组长度 是原来的2倍,
			// 临界值也扩大为原来2倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 如果原来的 thredshold 大于 0 则将容量设为原来的 thredshold
        // 在第一次带参数初始化时候会有这种情况
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 在默认无参数初始化会有这种情况
        newCap = DEFAULT_INITIAL_CAPACITY;16//当集合的容量使用达到了 newThr 时会进行扩容
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);12}
    if (newThr == 0) {
        // 如果新的容量 == 0
        // loadFactor 哈希加载因子 默认 0.75,可在初始化时传入,16*0.75=12 可以放12个键值对
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;// 将临界值设置为新临界值
    @SuppressWarnings({"rawtypes","unchecked"})
    // 扩容
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {// 如果原来的table有数据,则将数据复制到新的table中
        for (int j = 0; j < oldCap; ++j) {// 根据容量进行循环整个数组,将非空元素进行复制
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {// 获取数组的第j个元素
                oldTab[j] = null;
                if (e.next == null)// 如果链表只有一个,则进行直接赋值
                    // e.hash & (newCap - 1) 确定元素存放位置
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月光旅人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值