HashMap的数据结构

目录

一、底层数据结构

红黑树

数据类型性能分析

二、源码分析

2.1、数据结构定义

2.1.1、数组类型为Node[ ]

2.1.2、链表结点类 Node 

2.1.3、红黑树结点类 TreeNode

2.2、 HashMap构造函数

2.2.1、默认无参构造,指定一个默认的加载因子

 2.2.2、可指定容量的有参构造

2.2.3、可指定容量和加载因子

2.2.4、可传入一个已有的map

2.3、当新添加一个KV键值对元素时:

三、解决Hash冲突

3.1、开放定址法

3.2、再Hash法

3.3、链地址法(Java就是采用这种方法)

3.4、建立公共溢出区

四、基本属性

五、添加--put()方法

六、调用addEntry()

七、链表和红黑树互转

7.1、链表转红黑红树

7.2、红黑树转换为链表

7.3、小结


一、底层数据结构

JDK版本

底层实现

JDK<=1.7

数组+链表

JDK>=1.8

数组+链表+红黑树

红黑树

    红黑树在数据量大的时候性能会比链表要好,是一个自平衡的二叉搜索树,

    使得查询的时间复杂度降为O(logn)

特点:

  • 每个节点只有两种颜色:红色或者黑色
  • 根节点必须是黑色
  • 每个叶子节点(NIL)都是黑色的空节点
  • 从根节点到叶子节点,不能出现两个连续的红色节点
  • 从任一节点出发,到它下边的子节点的路径包含的黑色节点数目都相

性能临界点:

Hash值产生碰撞后,链表长度>8时会由链表转换为红黑树

而当红黑树的节点<6时,会由红黑树转换为链表,这就是二者的性能临界点

//当链表长度过长时,会有一个阈值,超过此阈值8就会转化为红黑树
static final int TREEIFY_THRESHOLD = 8;


//当红黑树上的元素个数,减少到6个时,就退化为链表
static final int UNTREEIFY_THRESHOLD = 6;

数据类型性能分析

数据类型

查询速度

优缺点

时间复杂度

数组

插入和删除比较困难

O(1)

链表

插入和删除操作比较容易

O(N)

红黑树

自平衡的二叉搜索树

O(logn)

二、源码分析

2.1、数据结构定义

2.1.1、数组类型为Node[ ]


//存放所有Node节点的数组
transient Node<K,V>[] table;

2.1.2、链表结点类 Node 

每个Node都保存某个KV键值对元素的key、value、hash、next等值。

由于next的存在,所以每个Node对象都是一个单向链表中的组成结点。


//普通单向链表节点类
static class Node<K,V> implements Map.Entry<K,V> {
	//key的hash值,put和get的时候都需要用到它来确定元素在数组中的位置
	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;
	}

2.1.3、红黑树结点类 TreeNode

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
	TreeNode<K,V> parent;  	//当前节点的父节点
	TreeNode<K,V> left	//左孩子节点
	TreeNode<K,V> right;//右孩子结点

	TreeNode<K,V> prev;   	//指向前一个节点

	boolean red;	//当前节点是红色或者黑色的标识

	TreeNode(int hash, K key, V val, Node<K,V> next) {
		super(hash, key, val, next);
	}
}

2.2、 HashMap构造函数

2.2.1、默认无参构造,指定一个默认的加载因子


public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

 2.2.2、可指定容量的有参构造

         但是需要注意当前我们指定的容量并不一定就是实际的容量


public HashMap(int initialCapacity) {
	//同样使用默认加载因子
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

2.2.3、可指定容量和加载因子


public HashMap(int initialCapacity, float loadFactor) {
	if (initialCapacity < 0)
		throw new IllegalArgumentException("Illegal initial capacity: " +
										   initialCapacity);
	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity = MAXIMUM_CAPACITY;
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
		throw new IllegalArgumentException("Illegal load factor: " +
										   loadFactor);
	this.loadFactor = loadFactor; 
	this.threshold = tableSizeFor(initialCapacity);
}

2.2.4、可传入一个已有的map

前三个方法都没有进行数组的初始化操作,即使调用了构造方法此时存放HaspMap中数组元素的table表长度依旧为0 。

在第四个构造方法中调用了inflateTable()方法完成了table的初始化操作,并将m中的元素添加到HashMap中。


public HashMap(Map<? extends K, ? extends V> m) {
	this.loadFactor = DEFAULT_LOAD_FACTOR;
	putMapEntries(m, false);
}

2.3、当新添加一个KV键值对元素时:

1.通过该元素的key的hash值,计算该元素在数组中应该保存的下标位置。

2.如果该下标位置如果已经存在其它Node对象,则采用链地址法(下面会讲到)处理hash冲突, 

    即将新添加的KV键值对元素将以链表形式存储。

3.将新元素封装成一个新的Nod对象,插入到该下标位置的链表尾部(尾插法)。

4.当链表的长度超过8并且数组长度大于64时,为了避免查找搜索性能下降,该链表会转换成一个红黑树。 

三、解决Hash冲突

3.1、开放定址法

该方法也叫做再散列法,其基本原理是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi 。

3.2、再Hash法

这种方法就是同时构造多个不同的哈希函数: Hi=RH1(key)  i=1,2,…,k。当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

3.3、链地址法(Java就是采用这种方法)

其基本思想: 将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

3.4、建立公共溢出区

这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

四、基本属性

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始化大小 16 
static final float DEFAULT_LOAD_FACTOR = 0.75f;     //负载因子0.75
static final Entry<?,?>[] EMPTY_TABLE = {};         //初始化的默认数组
transient int size;     //HashMap中元素的数量
int threshold;          //判断是否需要调整HashMap的容量  

五、添加--put()方法

在该方法中,添加键值对时,

1.进行table是否初始化的判断,如果没有进行初始化(分配空间,Entry[]数组的长度)。

2.然后进行key是否为null的判断,如果key==null ,放置在Entry[]的0号位置。

3.调用一个Hash()方法,得到当前key的一个hash值,

    ====>用于确定当前key应该存放在数组的那个下标位置

4.计算在Entry[]数组的存储位置,判断该位置上是否已有元素,

如果已经有元素存在,则遍历该Entry[]数组位置上的单链表。

5.判断key是否存在,如果key已经存在,

    则用新的value值,替换点旧的value值,并将旧的value值返回。

6.如果key不存在于HashMap中,程序继续向下执行。

  将key-vlaue, 生成Entry实体,添加到HashMap中的Entry[]数组中,调用addEntry()方法


public V put(K key, V value) {
        if (table == EMPTY_TABLE) { //是否初始化
            inflateTable(threshold);
        }
        if (key == null) //放置在0号位置
            return putForNullKey(value);
        int hash = hash(key); //计算hash值
        int i = indexFor(hash, table.length);  //计算在Entry[]中的存储位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i); //添加到Map中
        return null;
}

六、调用addEntry()

添加到方法的具体操作:

1.在添加之前先进行容量的判断,如果当前容量达到了阈值,并且需要存储到Entry[]数组中====>先进性扩容操作,空充的容量为table长度的2倍。

2.重新计算hash值和数组存储的位置,扩容后的链表顺序与扩容前的链表顺序相反。

3.然后将新添加的Entry实体存放到当前Entry[]位置链表的头部。

特此说明:

4.在1.8之前,新插入的元素都是放在了链表的头部位置,

     但是这种操作在高并发的环境下容易导致死锁,

     所以1.8之后,新插入的元素都放在了链表的尾部。

/*
 * hash hash值
 * key 键值
 * value value值
 * bucketIndex Entry[]数组中的存储索引
 * / 
void addEntry(int hash, K key, V value, int bucketIndex) {
     if ((size >= threshold) && (null != table[bucketIndex])) {
     //扩容操作,将数据元素重新计算位置后放入newTable中,
       链表的顺序与之前的顺序相反
         resize(2 * table.length); 
         hash = (null != key) ? hash(key) : 0;
         bucketIndex = indexFor(hash, table.length);
     }

    createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

七、链表和红黑树互转

public V put(K key, V value) {
    //调用putVal()方法完成
    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; Node<K,V> p; int n, i;
    //判断table是否初始化,否则初始化操作
    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);
                    //链表长度8,将链表转化为红黑树存储
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //key存在,直接覆盖
                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;
}

7.1、链表转红黑红树

链表的长度大于8的时候,就转换为红黑树

先判断table的长度是否大于64,如果小于64,就通过扩容的方式来解决,避免红黑树结构化。
链表长度大于8有两种情况:

  • table长度足够,hash冲突过多
  • hash没有冲突,但是在计算table下标的时候,由于table长度太小,导致很多hash不一致的
    第二种情况是可以用扩容的方式来避免的,扩容后链表长度变短,读写效率自然提高。另外,扩容相对于转换为红黑树的好处在于可以保证数据结构更简单。
    由此可见并不是链表长度超过8就一定会转换成红黑树,而是先尝试扩容
 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //首先tab的长度是否小于64,
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        //小于64则进行扩容
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            //否则才将列表转换为红黑树
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

7.2、红黑树转换为链表

所需条件:
     扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。
 

static final int UNTREEIFY_THRESHOLD = 6;//退化链表的临界值

//扩容时判断是红黑树结构时会执行split方法
else if (e instanceof TreeNode)
	((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
	
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {

    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    
    //把红黑树中的结点依次添加到 low 和 high 两颗红黑树中
    //还是依靠 (e.hash & bit) == 0 的位运算来判断属于哪颗树,bit是传过来的旧数组下标
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
    	......
        if ((e.hash & bit) == 0) {
        	 ++lc;     	//添加到 loHead
        }
        else {
            ++hc;//添加到 hiHead
        }
    }
	
	if (loHead != null) {
		//如果low树的元素个数小于等于6,退化成链表
    	if (lc <= UNTREEIFY_THRESHOLD)
     //并插入到新数组 tab[index] 的位置上,index是当前红黑树所在旧数组坐标
        	tab[index] = loHead.untreeify(map);
    	else {
	       	 tab[index] = loHead;
        	if (hiHead != null) 
            	loHead.treeify(tab);
    		}
		}
	if (hiHead != null) {
    	if (hc <= UNTREEIFY_THRESHOLD)
	       //bit是旧数组长度
        	tab[index + bit] = hiHead.untreeify(map);
    	else {
        	tab[index + bit] = hiHead;
        	if (loHead != null)
            	hiHead.treeify(tab);
    	}
	}
}

7.3、小结

1、hashMap并不是在链表元素个数大于8就一定会转换为红黑树,而是先考虑扩容,扩容达到默认限制后才转换。
2、hashMap的红黑树不一定小于6的时候才会转换为链表,而是只有在resize的时候才会根据 UNTREEIFY_THRESHOLD 进行转换。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值