Map知识梳理

文章详细阐述了HashMap的内部结构,包括其Node和TreeNode节点类型,以及插入、扩容的机制。特别提到了当链表长度超过8时,HashMap会将链表转换为红黑树。此外,还介绍了LinkedHashMap的特点,即按照插入或访问顺序保持元素顺序。
摘要由CSDN通过智能技术生成

1) Map用于保存具有映射关系的数据:Key-Value

2) Map 中的 key 和 value 可以是任何引用类型的数据,会封装到HashMap$Node对象中
3) Map 中的 key 不允许重复

4) Map 中的 value 可以重复
5) Map 的key 可以为 null, value 也可以为null ,注意 key 为null,只能有一个value 为null ,可以多个.
6) 常用String类作为Map的 key

底层实现: 

1.HashSet 底层是 HashMap

2.添加一个元素时,先得到hash值转成索引值

3.找到存储数据表table,看这个索引位置是否已经存放的有元素如果没有,直接加入如果有,调用 equals 比较,如果相同,就放弃添加,如果不相同,则添加到最后6在Java8中如果一条链表的元素个数超过 TREEIFY THRESHOLD(默认是8).并且table的大小 >=MIN TREEIFY CAPACITY(默认64)就会进行树化(红黑树) 

HashMap一些字段信息:

// table的默认长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16

// 默认最大table长度
static final int MAXIMUM_CAPACITY = 1 << 30; // 1073741824

// 加载因子,75%
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// table中一个元素链表达到某个长度转化为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;

// 红黑树转化回链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;

//  桶中的Node被树化时最小的hash表容量(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN TREEIFYCAPACITY时,此时应执行resize扩容操作这个MIN TREEIFYCAPACITY的值至少是TREEIFY THRESHOLD的4倍。)
static final int MIN_TREEIFY_CAPACITY = 64;	 

// 存储node的表,hash桶
transient Node<K,V>[] table;

// 记录所有kv数据,遍历使用
transient Set<Map.Entry<K,V>> entrySet;

// HashMap中存储的键值对的数量
transient int size;

// HashMap扩容和结构改变的次数
transient int modCount;

// 扩容的临界值 = 容量 * 填充因子
int threshold;

// 扩容百分比(填充因子)
final float loadFactor;

 首先,每一个键值对都是由Node来实现的,如果链表长度达到8,是由TreeNode来实现节点的

static class Node<K,V> implements Map.Entry<K,V> {
     final int hash;
     final K key;
     V value;
     Node<K,V> next;
	// ...省略
}

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);
	}
	final TreeNode<K,V> root() {
	    for (TreeNode<K,V> r = this, p;;) {
	        if ((p = r.parent) == null)
	            return r;
	        r = p;
	    }
	}	
	// ...省略
}

HashMap的四个构造方法

在jdk1.8中,调用这前三种构造器之后,并没有立刻创建一片空间,而是在首次调用put()方法时,才创建空间。最后一个初构造器是直接传了一个Map作为参数进行初始化,并将内容读取做hash放到table中

调用put()方法插入键值对

    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);
    }

先判断表是否为空或长度为0,如果是,就扩容

通过(n-1)&hash来获得要插入的位置     //n是tab.length

如果为空,直接插入,否则就会根据节点类型来进行不同的处理

else {
        Node<K,V> e; K k;

		// 当前第一个节点和要插入的key相等
        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) {
            
            	// 到了尾巴还是没有要找的key,就往这个节点后面创建并插入这个节点
                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的值相同的,就覆盖
        if (e != null) { 
            V oldValue = e.value;

			// 根据参数查看是否要修改存在的值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
                
			// 看用户实现,空
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;

put()大致内容为:

  • 确定是否已经初始化table,没有就先初始化一下。
  • 定位插槽后确定是否有节点存在,没有就创建一个节点插入。
  • 如果有一个节点并且就是我要找的那一个,就将值进行覆盖,如果不是我要找的就根据这个节点判断是树结构还是链表结构,根据数据结构不同进行遍历查找。
  • 如果都查不到就新建一个节点插入,如果找到就覆盖,在链表插入后需要判断当前是否到达树化的阈值,判断到达阈值进行树化。

 扩容机制 resize()

  • 如果当前table没有内容说明是初始化,那确定新table它的容量与扩容阈值后直接返回。
  • 如果table有内容,需要先判断是否还能继续扩容(达到最大容量就会直接返回)。没有达到最大容量就直接增加一倍,然后根据旧table的容量是否大于等于16来确定新table的扩容阈值。
  • 如果旧table的容量大于等于16,那么新table的扩容阈值就是旧table容量的两倍,否则就是新table容量*0.75。
  • 到了这里就需要分三种情况来处理旧table的节点转移了。
  • ** 只有一个节点就直接定位新数组位置转移即可。
  • ** 如果是树节点就确定是否可以取消树化,转换链表转移
  • ** 最后一种就是链表结构了,那就确定每个节点是否要转移,然后进行转移处理即可。
final Node<K,V>[] resize() {
   	Node<K,V>[] oldTab = table;
   	
   	// 等于0说明刚进来初始化,不等于零就是某个put方法调用的逻辑
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    
    // table有内容,非初始化
    if (oldCap > 0) {
    
    	// 最大容量返回
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        
        // 将新table的容量扩容一倍,然后判断不是最大值并且旧table容量大于等于16,
        // 就将新数组扩容阈值扩大一倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    }
    
    // 初始化传入threshold就会大于0
    else if (oldThr > 0)
        newCap = oldThr;
        
    // 初始化无参,就会给默认值
    else {               
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    // 进入这个方法说明上面经过 if (oldCap > 0)处理但没有进入else if中,
    // newCap > MAXIMUM_CAPACITY 或者 oldCap < 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;
    
    // 初始化就直接返回了(初始化oldTable是空的),这里需要重定位
    if (oldTab != null) {
	
		// 遍历所有table插槽
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            
            if ((e = oldTab[j]) != null) {

				// 清空这个节点使gc能够及时回收,这个节点已经赋值给变量e
                oldTab[j] = null;
	
				// 这个插槽只有只有一个节点,重定位放入新table
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                    
                // 这个节点已经转换为树结构了,
                else if (e instanceof TreeNode)

					// 红黑树拆分,拆分后的高低位树过小(节点小于等于6个),则取消树化,将其转为链表结构。
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { 

					// 链表结构,拆分成新table需要重定位与不需要的
                    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;
}

 Map常见方法

添加、删除、修改操作:

object put(object key,object value): 将指定key-value添加到(或修改)当前map对象中

void putALL(Map m):m中的所有key-value对存放到当前map中

Object remove(Object key): 移除指定key的key-value对,并返回value

void clear():清空当前map中的所有数据

元素查询的操作:

Object get(Object key):获取指定key对应的value

boolean containsKey(object key):是否包合指定的key

boolean containsValue(Object valueT是否包合指定的vaLue

int size():返回map中key-value对的个数

boolean isEmpty():判断当前map是否为空

boolean equals(object obj):判断当前map和参数对象obj是否相等

元视图操作的方法:

Set keySet():返回所有key构成的Set集合

ColLection values():返回所有value构成的Collection集合

Set entrySet():返回所有key-value对构成的Set集合

 

 LinkedHashMap

1. LinkedHashMap 加入顺序和取出元素/数据的顺序一致
2. LinkedHashMap 底层结构 (数组table+双向链表)
3. 添加第一次时,直接将 数组table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry
4. 数组是 HashMap$Node[] 存放的元素/数据是 LinkedHashMap$Entry类型

	// 头节点
	transient LinkedHashMap.Entry<K,V> head;

	// 尾节点
    transient LinkedHashMap.Entry<K,V> tail;

	// 访问顺序
	// true可以按最近访问顺序遍历,最近访问的优先读
	final boolean accessOrder;

	// 添加LinkedHashMap节点独有的定义(双向指针: before,after)
	static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

请添加图片描述

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值