对HashMap的认识了解

HashMap是Map的一个实现类,Map没有继承自Collection。
关于Map

1、 Map是一个接口,不能进行实例化,如果要进行实例化,只能通过它的两个实现类TreeMap和HashMap
2、 Map中存放键值对的Key是唯一的,value是可以重复的
3、在Map中插入键值对时,Key不能为null,否则会报出NullPointerException异常,但是value可以为null
4、Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复),通过Set keySet() 方法
5、Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复),通过Collection values()方法
6、Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。

在这里插入图片描述

在这里插入图片描述

HashMap允许key,value为null值,通过上面的集合框架,可以看到HashMap是直接继承自Map,下图是两个实现类的区别。
在这里插入图片描述
哈希表
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( ),搜索的效率取决于搜索过程中元素的比较次数。
而最理想的搜索方式是:可以不经过任何比较,直接可以从表中获取数据。如果可以构造如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素,这种方法我们称之为哈希(散列)方法,哈希方法中使用到的是函数就成=称之为哈希函数,构造出来的结构我们称之为哈希表(散列表)。
我们先来看一个例子
在这里插入图片描述
对于上面的问题,我们引入一个哈希冲突的概念,不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率

  • 1、降低重冲突率,我们可以从哈希函数进行入手,哈希函数设计的不够合理
    哈希函数设计的原则:

    1、 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址 时,其值域必须在0到m-1之间
    2、哈希函数计算出来的地址能均匀分布在整个空间中
    3、哈希函数应该比较简单

    我们来说明常用的哈希函数
    直接定制法
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况
    除留余数法
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

  • 2、调节负载因子
    散列表的载荷因子=填入表中的元素个数/散列表的长度
    所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。
    已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小

那对于存在冲突的问题,要怎样解决这个问题,常见的两种方式就是开散列和闭散列

  • 1、闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,寻找下一个空位值的方法,第一个就是线性探测(从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止),另一个就是二次探测
  • 2、开散列又叫链地址法(开链法),就是下面这种
    在这里插入图片描述

HashMap底层实现
当进行插入操作的时候,HashMap会根据Key值来计算出它的Hash,通过Hash来确认存放到数组的位置,如果发生Hash冲突,我们就会以链表的形式进行存储,当链表过于长的时候,HashMap会将这个链表转换为红黑树进行存储
在这里插入图片描述
我们先来通过源码看一些最基本的内容

//默认初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
//最大容量为2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 若链表中节点个数超过8时,会将链表转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 若红黑树中节点小于6时,红黑树退还为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 如果哈希桶中某条链表的个数超过8,并且桶的个数超过64时才会将链表转换为红黑树,否则直接扩容
static final int MIN_TREEIFY_CAPACITY = 64;

HashMap桶中放置的节点,该节点是一个单链表的结构

// HashMap将其底层链表中节点封装为其内部的静态类
// 节点中带有:<key, value>键值对以及key所对应的哈希值
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//哈希值
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

		//重写Object类的hashCode()方法
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);//表示相同的返回0,不相同的返回0
        }
        
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        // 重写Object类的equals方法
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

哈希函数

//1.key如果为空,返回的是0号桶
//2. key如果不为空,返回该key所对应的哈希码,从该位置可以看出,如果key是自定义类型,必须要重写Object类的hashCode方法
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

扩容机制

// 每次都是将cap扩展到大于cap最近的2的n次幂
static final int tableSizeFor(int cap) {
	int n = cap - 1;
	n |= n >>> 1;
	n |= n >>> 2;
	n |= n >>> 4;
	n |= n >>> 8;
	n |= n >>> 16;
	return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

构造方法

// 构造方法一:带有初始容量的构造,负载因子使用默认值0.75
public HashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 构造方法二:负载因子使用默认值0.75
public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public HashMap(int initialCapacity, float loadFactor) {
// 如果容量小于0,抛出非法参数异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 如果初始容量大于最大值,用2^30代替
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 检测负载因子是否非法,如果负载因子小于0,或者负载因子不是浮点数,抛出非法参数异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
	// 给负载因子和容量赋值,并将容量提升到2的整数次幂
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

插入

1、当table数组为空的时候,通过扩容的方式初始化table
2、 通过哈希函数计算出下标之后,若该位置没有元素,则将新建的Node进行插入
3、如果发生了冲突,遍历链表查找要插入的key是否已经存在,存在的话根据条件判断是否用新值替代旧值
4、如果不存在则将元素插入到链表的尾部,并根据相关条件判断是否将其转换为红黑树
5、判断键值对数量是否大于阈值,大于的话进行扩容

	public V put(K key, V value) {
	        return putVal(hash(key), key, value, false, true);
	}
 	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;//指向hash数组
        Node<K,V> p; //初始化table中第一个结点
        int n, i;//n为数组长度,i为索引
        //
        if ((tab = table) == null || (n = tab.length) == 0)
            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;
            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;
                }
            }
            //说明插入的key-value已经存在
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //根据传入的onlyIfAbsent 判断是否进行旧值的更新
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //数量超过阈值,进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

猜你喜欢

转载自blog.csdn.net/qq_44723296/article/details/105898969