HashSet基本介绍和源码剖析

HashSet的全面说明

继承关系图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SaUshkxh-1647512622526)(E:\Typora笔记\java笔记\img\image-20220317181818080.png)]

构造器

Constructor and Description
HashSet() 构造一个新的空集合; 背景HashMap实例具有默认初始容量(16)和负载因子(0.75)。
HashSet(Collection<? extends E> c) 构造一个包含指定集合中的元素的新集合。
HashSet(int initialCapacity) 构造一个新的空集合; 背景HashMap实例具有指定的初始容量和默认负载因子(0.75)。
HashSet(int initialCapacity, float loadFactor) 构造一个新的空集合; 背景HashMap实例具有指定的初始容量和指定的负载因子。

1.HashSet实现了Set接口

2.HashSet实际上是HashMap【由于HashSet的构造器中创建了HashMap对象】

public HashSet(){
   map = new HashMap<>();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wz9b5az2-1647512622527)(E:\Typora笔记\java笔记\img\image-20220219213525460.png)]

3.可以存放null值,但是只能有一个null

4.HashSet不保证元素是有序的,取决于hash后,再确定索引的结果【即不保证存放元素的顺序和取出顺序一致】

5.不能有重复元素/对象。在Set接口中已经体会

package collection_.collectionP.set_;

import java.util.HashSet;

/**
 * @author: 海康
 * @version: 1.0
 */
public class HashSet01 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        // 1.add方法返回的是一个布尔值:成功返回true 否则返回false
        // 2.可以加入null值,但是只能添加一个
        System.out.println(hashSet.add("湛江"));//true
        System.out.println(hashSet.add("湛江"));//false
        System.out.println(hashSet.add(null));//true
        System.out.println(hashSet.add(null));//false
        System.out.println(hashSet.add("海康"));//true

        // 3.指定对象删除
        boolean remove = hashSet.remove("海康");
//        System.out.println(remove);

        // 4.HashSet 不能添加相同的元素或对象
        hashSet.add("lucy");// 可以添加
        hashSet.add("lucy");// 不能添加因为以上语句指向常量池相同对象
        hashSet.add(new Dog("tom"));// 可以不回成功
        hashSet.add(new Dog("tom"));// 可以添加成功
//        System.out.println("set"+hashSet);

        // 在加深一下,非常经典的面试题
        // 看源码分析
        hashSet.add(new String("haikang"));// 可以添加成功
        hashSet.add(new String("haikang"));// 不可以添加成功
        System.out.println("hashSet"+hashSet);
    }
}

class Dog {
    private String name;

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

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}
HashSet底层机制说明【最重要】

分析HashSet底层是HashMapHashMap底层是【数组+链表+红黑树】

下面的结论非常重要必须记住【背下】

  1. 先获取元素的哈希值【hashCode方法】
  2. 对象哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号
  3. 如果该位置上没有其他元素,则直接存放,如果该位置上已经有其他元素,则需要进行equals判断,如果相等,则不再添加。如果不相等,则以链表的方式添加

模拟数组+链表结构

package collection_.collectionP.set_;

/**
 * @author: 海康
 * @version: 1.0
 */
public class HashSetStructure01 {
    public static void main(String[] args) {
        // 模拟一个 HashSet的底层结构【其实就量模拟HashMap的底层结构】
        // 1. 创建一个数组,数组的类型是 Node[]
        // 2. 有些人直接把 Node[] 数组称为 表
        Node[] table = new Node[16];
        // 在索引为2的位置存放数据
        Node haikaing = new Node("海康", null);
        table[2] = haikaing;
        Node jack = new Node("jack", null);
        haikaing.next = jack;
        Node rose = new Node("Rose", null);
        jack.next = rose;
        System.out.println(table[2]);

        Node lucy = new Node("lucy", null);
        table[3] = lucy;
    }
}

class Node { // 结点 存储数据,可以指向下一个结点 ,从而形成链表
    public Object item; // 存放数据
    public Node next; // 指向下一个结点

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

    @Override
    public String toString() {
        return "Node{" +
                "item=" + item +
                ", next=" + next +
                '}';
    }
}

HashSet底层剖析结论【非常重要面试题】

  1. HashSet底层是HasMap
  2. 添加一个元素时,先得到hash值 —> 转成 —> 索引值
  3. 找到存储数据表table,看看这个 索引位置是否已经存放有元素
  4. 如果没有直接加入
  5. 如果有调用equals比较,如果相同就放弃添加,如果不相同,则添加到最后
  6. java8中,如果一条链表的元素个数到达了TREEIFY_THRESHOLO【默认是8】,并且table的大小>=MIN_TREEIFY_CAPACITY【默认64】,就会进行树化【红黑树】
  7. HashSet底层就是HashMap,第一次添加时,table数组扩容到16,临界值threshold16*加载因子(loadFactor)是 0.75=12
  8. 如果table数组使用到了临界值12,就会扩容到16*2=32,新的临界值就是32*0.75=24,依次类推
  9. java8中,如果一条链表的元素个数到达了TREEIFY_THRESHOLD(默认是8),并且是table的大小>=MIN_TREEIFY_CAPACITY(默认是64),就会进行树化【红黑树】,否则仍然采用数组扩容机制
  10. 扩容是以2倍的方式进行扩容的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-raUplcQG-1647512622528)(E:\Typora笔记\java笔记\img\image-20220220105227293.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W3nPhUqa-1647512622528)(E:\Typora笔记\java笔记\img\image-20220220181220039.png)]

注意是:每次向HashSet集合中加入一个节点后,size属性值就会加1,不管是加入是节点连接着节点还是直接加入到数组中,只要满足了到临界值,数组就会进行扩容

++modCount;
if (++size > threshold)// size是修改的次数【就是加入节点的次数】,threshold是临界值
    resize();// 如果加入的次数大于临界值,则进行扩容
afterNodeInsertion(evict);

源码剖析:

	    public static void main(String[] args) {
        HashSet<String> strings = new HashSet<>();
        strings.add("湛江");
        strings.add("海康");
        strings.add("湛江");
        for (int i = 0; i < 10; i++) {
           	 strings.add("A_"+i);
        	}
        }

	步骤1:使用无参构造器:HashSet集合底层维护下面两个属性
        private transient HashMap<E,Object> map;

        // Dummy value to associate with an Object in the backing Map
        private static final Object PRESENT = new Object();// 这个属性在添加时当占位符使用
		下面是使用无参构造创建一个HashSet对象,其实底层在HashSet构造器中创建一个 HashMap集合赋给 map
		public HashSet() {
        	map = new HashMap<>();
   		}

	步骤2:添加方法
    	public boolean add(E e) {
             return map.put(e, PRESENT)==null;
    	}
		调用 HashMap 中 put 方法 并且 PRESENT 是一个占位符,是直接 new Object()对象赋给 PRESENT
            
            
    步骤3:调用 HashMap 中 put 方法
        public V put(K key, V value) {
       		 return putVal(hash(key), key, value, false, true);
    	}

		调用 hash 方法 获取一个 哈希值
       static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
   		 }

	步骤4:真正添加方法
        在 HashMap 类中 维护一个transient Node<K,V>[] table;属性第一添加时为null
        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)判断当前表是否为空和表长度是否为0
            如果是就调用 resize()方法进行扩容,就是创建一个数组
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)如果当前位置没有元素直接添加
            tab[i] = newNode(hash, key, value, null);
        else {如果有元素进行如下判断 
            Node<K,V> e; K k;
              如果 hash 值和key指定向相同位置或equals值相同时 则不添加
            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) {如果当前下一个位置为null时直接添加
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st、
                            判断一条链是否达到了8个,调用 treeifBin 方法,进行树化,注意同时还要table达到64
                            treeifyBin(tab, hash);
                        break;
                    }
                    如果两者相同则不添加
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
              如果 key 相同时就替换值
            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;
    }

	扩容算法
	final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            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) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        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;
    }

package com.hspedu.set_;

import java.util.HashSet;

@SuppressWarnings({"all"})
public class HashSetSource {
    public static void main(String[] args) {

        HashSet hashSet = new HashSet();
        hashSet.add("java");//到此位置,第1次add分析完毕.
        hashSet.add("php");//到此位置,第2次add分析完毕
        hashSet.add("java");
        System.out.println("set=" + hashSet);

        /*
        老韩对HashSet 的源码解读
        1. 执行 HashSet()
            public HashSet() {
                map = new HashMap<>();
            }
        2. 执行 add()
           public boolean add(E e) {//e = "java"
                return map.put(e, PRESENT)==null;//(static) PRESENT = new Object();
           }
         3.执行 put() , 该方法会执行 hash(key) 得到key对应的hash值 算法h = key.hashCode()) ^ (h >>> 16)
             public V put(K key, V value) {//key = "java" value = PRESENT 共享
                return putVal(hash(key), key, value, false, true);
            }
         4.执行 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 的一个数组,类型是 Node[]
                //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 (key="java",value=PRESENT)
                //(2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null)

                if ((p = tab[i = (n - 1) & hash]) == 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) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break

                        for (int binCount = 0; ; ++binCount) {
                            if ((e = p.next) == null) {
                                p.next = newNode(hash, key, value, null);
                                if (binCount >= TREEIFY_THRESHOLD(8) - 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;
                //size 就是我们每加入一个结点Node(k,v,h,next), size++
                if (++size > threshold)
                    resize();//扩容
                afterNodeInsertion(evict);
                return null;
            }
         */

    }
}

课堂练习1:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6okcHcTs-1647512622529)(E:\Typora笔记\java笔记\img\image-20220220182840366.png)]

package collection_.collectionP.set_;

import java.util.HashSet;
import java.util.Objects;

/**
 * @author: 海康
 * @version: 1.0
 */
public class HashSetExercise01 {
    public static void main(String[] args) {
        /**
         * 定义一个Employee类,该类包含:private成员属性name , age要求:
         * 1.创建3个Employee放入HashSet中
         * 2.当name 和 age 的相同时,认为是相同员工,不能添加到HashSet集合中
         */
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("海康",20));
        hashSet.add(new Employee("海康",20));
        hashSet.add(new Employee("湛江",22));
        System.out.println(hashSet);
    }
}

class Employee {
    private String name;
    private int age;

    public Employee() {
    }

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return age == employee.age && Objects.equals(name, employee.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EMvVUpPa-1647512622529)(E:\Typora笔记\java笔记\img\image-20220220203718810.png)]

package collection_.collectionP.set_;


import java.util.HashSet;
import java.util.Objects;

/**
 * @author: 海康
 * @version: 1.0
 */
public class HashSetExercise02 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee01("海康",new MyDate(2000,11,11),86888.886));
        hashSet.add(new Employee01("海康",new MyDate(2000,11,11),76888.886));
        hashSet.add(new Employee01("湛江",new MyDate(2000,11,11),76888.886));
        System.out.println(hashSet);
    }
}

class Employee01 {
    private String name;
    private MyDate birthday;
    private double salary;

    public Employee01(String name, MyDate birthday, double salary) {
        this.name = name;
        this.birthday = birthday;
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Employee01{" +
                "name='" + name + '\'' +
                ", month=" + birthday +
                ", salary=" + salary +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee01 that = (Employee01) o;
        return  Objects.equals(name, that.name) && Objects.equals(birthday, that.birthday);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, birthday);
    }
}

class MyDate {
    private int year;
    private int month;
    private int day;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MyDate myDate = (MyDate) o;
        return year == myDate.year && month == myDate.month && day == myDate.day;
    }

    @Override
    public int hashCode() {
        return Objects.hash(year, month, day);
    }

    public MyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    @Override
    public String toString() {
        return "MyDate{" +
                "year=" + year +
                ", month=" + month +
                ", day=" + day +
                '}';
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值