Java集合-Map

本文详细介绍了Java中的Map接口,重点讲解了AbstractMap、HashMap和HashTable的实现原理。HashMap在JDK1.7和1.8的工作原理对比,包括负载因子、扩容策略和插入操作。同时,文章提到了HashTable的线程安全性以及TreeMap的升序特性。

类图

Map类图

图解

Map

Map

特点

键值对的存储
元素需要重写hashCode()和equals()

api

增加:put(K key, V value);
删除:clear(); remove(Object key);
查看:get(Object key); size(); entrySet(); keySet(); values();
判断:containsKey(Object key);containsValue(Object value);
equals(Object o); isEmpty();

AbstractMap

特点

  1. 元素无序、唯一
  2. 底层结构:哈希表(数组 + 链表)

HashMap

特点

线程不安全,效率高
key可以存入null,并且唯一

属性

// 数组初始化长度 ==> 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 数组最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 负载因子/加载因子的默认值:0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 底层主数组
transient Entry<K,V>[] table;
// 元素的数量
transient int size;
// 数组扩容的边界值/门槛值,默认为 0
int threshold;
// 装填因子、负载因子、加载因子,影响扩容边界值
final float loadFactor;

内部类

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
负载因子为什么是0.75

数组扩容边界值 = 数组长度 * 负载因子

  1. 如果设置为1:空间利用率高;但哈希碰撞的几率也变高,而链表的查询效率较低
  2. 如果设置为0.5:哈希碰撞的几率低,产生链表的几率低,查询效率高;但扩容频繁,空间利用率低
  3. 所以在 0.5 ~ 1 之间,取中间值:0.75
JDK1.7
工作原理

HashMapJDK7工作原理

  1. 元素: key,value
  2. 调用key的hashCode(),得到hash值,hash值对数组table长度进行取余hash & (length - 1) 等效于 hash % length(& 的效率比 % 要高),得到元素在数组中的位置(i)
  3. 通过节点的next属性,遍历table[i]上链表的节点e
  4. 比较e.key 和 key是否相等
    • e.hash == hash:首先判断hash值,hash不相等,则key一定不相等
    • e.key == key:判断key的地址值,地址值一样,则key一定相等
    • key.equals(e.k):最后调用equals() 判断key 是否相等
  5. 若相等,value替换oldValue,并return oldValue
  6. 判断是否需要扩容
  7. 采用头插法,获取table[i]上的节点e,封装元素new Entry(hash, k, v, e)next指向e,table[i]重新指向新节点,成为新的头节点
  8. return null
构造函数
// 无参构造
public HashMap() {
    // this(16, 0.75)
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
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);

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
	// 左移1位,等同于 * 2
	// 确保数组的长度为 2的整数倍
        capacity <<= 1;
    // 确定使用的负载因子
    this.loadFactor = loadFactor;
    // threshold = capacity * loadFactor = 16 * 0.75 = 12;
    // 确定数组扩容的边界值:12
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 创建主数组,长度为16
    table = new Entry[capacity];
    useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    init();
}
  1. 默认数组长度为16,负载因子为0.75
  2. 调整数组长度,变为2的整数倍
  3. 确定数组扩容边界值:数组长度 * 负载因子
  4. 创建指定长度的Entry数组table

为什么数组长度必须是2的整数倍?

  1. h & (length - 1) 等效于 h % length 的前提就是 length == 2^n
  2. 减少哈希冲突
put()

添加元素

public V put(K key, V value) {
    // 对key == null处理,允许key的值为空
    if (key == null)
        return putForNullKey(value);
    // 获取key的哈希值
    int hash = hash(key);
    // 得到元素在数组中的位置
    int i = indexFor(hash, table.length);
    // 哈希碰撞
    // 对table[i]上的链表元素进行循环,key相等则替换value
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
		// e.hash == hash:首先判断hash值,hash不相等,则key一定不相等
		// e.key == key:判断key的地址值,地址值一样,则key一定相等
		// key.equals(k):最后调用equals() 判断key 是否相等
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
	    // 获取原value
            V oldValue = e.value;
	    // 替换成新value
            e.value = value;
            e.recordAccess(this);
	    // 返回原value
            return oldValue;
        }
    }

    modCount++;
    // 添加元素
    addEntry(hash, key, value, i);
    return null;
}
hash()

计算key的哈希值

final int hash(Object k) {
    int h = 0;
    if (useAltHashing) {
        if (k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h = hashSeed;
    }
    // 二次散列,没有直接使用hashCode()的值,减少哈希冲突
    h ^= k.hashCode();

    // 扰动函数--核心:增加值的不确定性
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
indexFor()

计算元素对应数值的位置

static int indexFor(int h, int length) {
    // 计算元素对应数组位置的公式
    // 等效 h % length(前提:length为2的整数倍),&的效率高
    return h & (length-1);
}
addEntry()

创建、添加节点

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果元素总个数大于等于数组扩容的临界值 && 添加的位置已有元素
    // 则进行数组扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
	// 扩容为原数组长度的2倍
        resize(2 * table.length);
	// 扩容后重新计算hash值
        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) {
    // 头插法
    // 获取数组位置的节点 e
    Entry<K,V> e = table[bucketIndex];
    // 创建新节点,并且next指向 e
    // 新节点赋值到 数组位置节点,成为该数组位置新的头节点
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    // 元素总个数 + 1
    size++;
}
resize()

扩容:原数组长度的2倍

resize(2 * table.length)

void resize(int newCapacity) {
    // 获取原数组
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    // 如果原数组的长度已达最大容量,则放弃扩容
    if (oldCapacity == MAXIMUM_CAPACITY) {
	// 扩容临界值设置为Integer的最大值
        threshold = Integer.MAX_VALUE;
        return;
    }

    // 根据指定容量,创建新的数组
    Entry[] newTable = new Entry[newCapacity];
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() &&
            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;
    // 把原数组的元素(所有头节点)复制到新的数组上
    transfer(newTable, rehash);
    // 新数组替换原数组
    table = newTable;
    // 重新计算数组扩容临界值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
JDK1.8
构造函数

无参构造

负载因子默认为 0.75; 其他属性均为默认值

// 无参构造
public HashMap() {
    // 负载因子默认为 0.75; 其他属性均为默认值
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}

有参构造

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的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;
}
put()
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

hash():计算key的hash值

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

putVal()

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab:底层数组table
    // p:tab[i]的元素节点
    // n:底层数组table的长度length
    // i:元素存放数组下标
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 一、是否需要初始化
    if ((tab = table) == null || (n = tab.length) == 0)
	// 1.table为null或者长度为 0,resize()进行初始化
	// 扩容后的数组和数组长度分别重新赋值到 tab 和 n
        n = (tab = resize()).length;
    // 二、插入元素
    // (n - 1) & hash,等同 hash % n,计算存放数组下标
    if ((p = tab[i = (n - 1) & hash]) == null)
	// 2.1.如果tab[i] == null,创建新节点,并赋值到tab[i]作为头节点 
        tab[i] = newNode(hash, key, value, null);
    else {
	// 2.2.如果tab[i] != null,即哈希冲突
	// e:存放集合中与添加元素的key相等的元素
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
	    // 解决哈希冲突1:如果添加元素的key 与 p节点(即tab[i]) 的key相等,
	    // p赋值给e节点
            e = p;
        else if (p instanceof TreeNode)
	    // 解决哈希冲突2:如果p节点属于树节点,则通过putTreeVal()添加节点
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
	    // 解决哈希冲突3:遍历tab[i]所在的链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
		    // p.next == null,尾插法
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
			// 如果链表长度 >= 7,则尝试进行树化
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
		    // 如果链表上存在元素的key与添加元素的key相等,终端循环
                    break;
                p = e;
            }
        }
	// 3.解决哈希冲突过程中,如果e != null,
	// 则表示集合中存在key与添加元素的key相等,需要替换value 
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 三、元素插入成功,后续处理
    ++modCount;
    // 添加后如果size大于扩容临界值,则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
  1. 判断table是否为null或者长度为 0,resize()对table进行初始化,长度为n
  2. (n - 1) %hash取余,计算元素存放数组下标i
  3. tab[i]如果为null,创建新节点,并赋值到tab[i]作为头节点
  4. 否则即是发生哈希冲突,准备变量e:记录key相等的节点
    • 如果添加元素的key 与 tab[i] 的key相等(hash, 地址值, equals),tab[i]赋值给e
    • 如果tab[i]属于树节点,调用putTreeVal()添加节点
    • 遍历tab[i]所在链表
      • 当前节点的key与元素key相等,赋值给e,跳出循环
      • 使用尾插法,通过next,遍历找到尾节点:next == null,将元素封装成节点,赋值给next;同时如果链表长度 >= 7,则尝试进行树化,treeifyBin()
    • 如果e != null,表示存在节点key与元素key相等,元素value替换节点value,并返回oldValue
  5. ++size
  6. 判断是否需要进行扩容,resize()
resize()
final Node<K,V>[] resize() {
    // 获取原数组、原长度、原扩容边界值
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    // 新长度、新扩容边界值 默认为 0
    int newCap, newThr = 0;
    // =========================确定新长度、新边界值start==============	
    if (oldCap > 0) { // 第n次扩容:原长度大于0 
        if (oldCap >= MAXIMUM_CAPACITY) {
	    // 如果原长度大于等于最大长度,调整扩容边界值为Integer.max,返回原数组
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
	// 新长度 = 原长度的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
	    // 如果新长度小于最大长度并且原长度大于等于默认长度16
	    // 新扩容边界值 = 原扩容边界值的2倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // 有参第一次扩容:如果原长度 == 0 && 原边界值 > 0 
	// 新长度 = 原边界值	
        newCap = oldThr;
    else { // 无参第一次扩容:原长度 == 0 && 原边界值 == 0 
	// 新长度 = 默认值16
	// 新边界值 = 默认值:0.75 * 16 = 12               
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    // 如果新边界值 == 0 (有参第一次扩容)
    if (newThr == 0) {
	// 新边界值 = Min(新长度 * 负载因子, Integer.max)
        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;
    // ==========================确定新长度、新边界值end===============	
    if (oldTab != null) {
	// 如果原数组 != null,遍历数组上的每个元素oldTab[j],复制到newTab中
        for (int j = 0; j < oldCap; ++j) {
	    // e:原数组上的节点:oldTab[j]
            Node<K,V> e; 
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
		    // 如果e所在链只有自己一个节点,
		    // 直接重新计算下标,放入到新数组中
                    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;
}
  1. 确定新长度、新边界值
    • 原长度 > 0 :不是第一次扩容
      • 新长度为原长度的2倍
      • 如果新长度小于最大长度 并且 原长度大于等于默认长度16,那么 新边界值为原边界值的2倍
    • 原长度 == 0 && 原边界值 > 0:有参的第一次扩容
      • 新长度 = 原边界值
    • 原长度 == 0 && 原边界值 == 0:无参的第一次扩容
      • 新长度 = 16
      • 新边界 = 0.75 * 16 = 12
    • 如果新边界值 == 0,新边界值 = Min(新长度 * 负载因子, Integer.max)
  2. 如果原数组 != null,遍历数组上的每个元素oldTab[j],复制到newTab中
    • 如果oldTab[j]所在链表只有自己一个元素,则重新计算下标,放入到新数组中
treeifyBin()

树化

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
	// 如果 数组 == null || 长度 < 64,进行扩容,放弃树化 
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
	// 长度 >= 64 && tab[index] != null,进行树化
	// 1.单向链表转化为双向链表
	// hd:头节点
	// tl:尾节点
        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)
	    // 2.双向链表转为红黑树
            hd.treeify(tab);
    }
}
  1. 如果 数组 == null || 长度 < 64,进行扩容,放弃树化
  2. 树化
    • 单向链表转为双向链表
    • 双向链表转为红黑树

HashTable

特点

线程安全,效率低
key不可以存入null

TreeMap

特点

唯一、升序
底层:红黑树
放入集合的key必须实现比较器(内部或外部比较器)

属性

// key的外部比较器
private final Comparator<? super K> comparator;
// 树的根节点
private transient Entry<K,V> root;
// 集合中元素个数
private transient int size = 0;

内部类


static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;
}

构造函数

// 无参构造
public TreeMap() {
    // 使用无参构造,key必须实现Comparable接口(内部比较器)
    comparator = null;
}
// 指定外部构造器
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

put()

public V put(K key, V value) {
    Entry<K,V> t = root;
    // 第一次put, t == null,新节点作为根节点,返回null
    if (t == null) {
	// 比较key,这里主要判断 key 是否使用了比较器
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }

    // 第n次put,t != null
    int cmp;
    Entry<K,V> parent;
    // 1.根据比较器,选择父节点
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
	// 1.1.使用外部比较器
        do {
	    // 根节点出发,循环查找新节点的父节点
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
		// 左节点作为父节点
                t = t.left;
            else if (cmp > 0)
		// 右节点作为父节点
                t = t.right;
            else
		// cmp == 0,替换value,并返回原value
                return t.setValue(value);
        } while (t != null);
    }
    else {
	// 1.2.使用内部比较器
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left; // 左节点作为父节点
            else if (cmp > 0)
                t = t.right;// 右节点作为父节点
            else
		// cmp == 0, 替换value,并返回原value	
                return t.setValue(value);
        } while (t != null);
    }
    // 2.构造新节点,并根据比较结果cmp,选择新节点成为父节点的左节点或右节点
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    // 3.为节点上色
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}
  1. 第一次put,即root == null
    • 调用compare(),判断key是否使用比较器
    • 数据封装成Entry节点,赋值给root,size = 1
    • return null
  2. 第n次put
    • 根据比较器,从根节点开始遍历,调用compare()的结果cmp比较key,选择合适的父节点parent;如果存在cmp为0,即key 相等,替换value,并返回oldValue
    • 创建新节点,根据cmp,选择新节点成为父节点parent的左节点或右节点
    • 为节点上色
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值