Java集合框架之Map

Map

Map的实现类结构

Map:双列数据,存储键值对的数据

  • HashMap:作为Map的主要实现类,线程不安全,效率高;可存储null的key和value

    • LinkedHashMap:保证遍历map元素时可按照添加顺序实现遍历。

      原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素

  • TreeMap:保证按照添加的key-value对进行排序,实现排序遍历,根据key进行排序。底层使用红黑树

  • Hashtable:作为Map的古老实现类,线程安全,效率低;不可存储null的key和value

    • Properties:常用来处理配置文件。key和value都是String类型

HashMap的底层:

​ 数组+链表(jdk7之前)

​ 数组+链表+红黑树(jdk8之后)

面试题

  1. HashMap的底层实现原理
  2. HashMap和Hashtable的异同
  3. ConcurrentHashMap与Hashtable的异同

Map结构理解

Map中的key:无序、不可重复,使用set存储所有的key—>key所在的类要重写equals()和hashCode()

Map中的value:无序、可重复,使用collection存储所有value—>value所在类要重写equals()

一个键值对:key-value构成了一个Entry对象

Map中的Entry:无序的、不可重复的,使用Set存储所有的Entry

HashMap底层实现原理

以jdk7为例说明HashMap的添加过程:

HashMap map = new HashMap()

实例化之后,底层创建了长度是16的一维数组Entry[] table

map.put(key1,value1)

首先调用key1所在类的hashCode()计算key1哈希值,根据一定算法计算出在Entry数组里的位置:

  • 若该位置为空,添加成功
  • 若该位置有一个或多个数据(以链表形式存在),比较key1和已经存在的一个或多个数据的哈希值:
    • 若都不相等,添加成功
    • 若key1的哈希值与某一个数据(key2,value2)的哈希值相同,则继续用equals()比较
      • 若不相等,则添加成功;
      • 若相等,则使用value1替换value2的值(修改操作)

在不断添加的过程中,涉及到扩容,默认方式:扩容为原来2倍,并将原有数据复制过去

以jdk8相对于jdk7的不同:

  1. new HashMap():底层没有创建一个长度为16的数组

  2. jdk8底层的数组是:Node[],而非Entry[]

  3. 首次调用put()方法时,底层调用长度为16的数组

  4. jdk8底层结构:数组+链表+红黑树。

    当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8且当前数组长度 > 64,此时此索引位置上的所有数据改为红黑树存储。

HashMap源码分析

JDK7源码

成员变量
//内部数组的默认初始容量,作为hashmap的初始容量,是2的4次方,2的n次方的作用是减少hash冲突
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16

//默认的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认负载因子,当容器使用率达到这个75%的时候就扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 *当数组表还没扩容的时候,一个共享的空表对象
 */
static final Entry<?,?>[] EMPTY_TABLE = {};

//内部数组表,用来装entry,大小只能是2的n次方。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

//存储的键值对的个数
transient int size;

/**
 * 扩容的临界点,如果当前容量达到该值,则需要扩容了。
 * 如果当前数组容量为0时(空数组),则该值作为初始化内部数组的初始容量
 */
int threshold;

//由构造函数传入的指定负载因子
final float loadFactor;

//threshold的最大值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

//含有所有entry节点的一个set集合
private transient Set<Map.Entry<K,V>> entrySet = null;
put()方法
public V put(K key, V value) {
            //先判断哈希表是否为空,第一次put的话肯定是为空的,
            if (table == EMPTY_TABLE) {
            // roundUpToPowerOf2方法的作用是将构造器传入的容量初始化大小
            //转成最接近2的n字方值,为什么要2的n字方,下面会提到
            int capacity = roundUpToPowerOf2(threshold);
            //临界值是加载因子*容量大小
            threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
            //创建一个Entry数组
            table = new Entry[capacity];
            //initHashSeedAsNeeded方法的作用:找到与该实例的一个哈希掩码值,使哈希碰撞几率更为小.里 
            //面会生成一个hashSeed,将会在生成哈希值里面可能会用到。
            initHashSeedAsNeeded(capacity);
        }
        //如果key为null
        if (key == null)
            //这个方法下面讲解
            return putForNullKey(value);
        //计算key的哈希值
        int hash = hash(key);
        //计算该哈希值在哈希表的下标
        int i = indexFor(hash, table.length);
        //如果刚刚计算出来的下标在哈希表里面为空的话,将不会进入循环
        //不为空将遍历table[i]的链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判断该链表上是否有相同的哈希值和相同的地址值,或者key相同
            //若存在则覆盖旧值,返回旧值
            //判断顺序->hash->equals()
            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);
        return null;
    }
为什么容量大小只能是2的n次方

答案在indexFor()方法中

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

元素在Entry数组中决定位置的方式是使用hash按位与length-1,如果length是15而不是16,则length-1 = 1110,则hash=1110和hash=1111按位与1110的结果都是1110,则会产生冲突。因此都是length都是2的倍数最为合适

对null的处理

HashMap与Hashtable的区别之一就是key是否能处理null

private V putForNullKey(V value) {
    //查找哈希表中0索引的位置,是否不为空,如果不为空,则遍历0索引的链表
    //查找key==null的键值,覆盖并返回旧值。
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    //将null的键放入哈希表0索引的位置
    addEntry(0, null, value, 0);
    return null;
}

key为null的键值对一定放在table[0]的链表中

添加新结点
void addEntry(int hash, K key, V value, int bucketIndex) {
    //如果结点个数大于或等于临界值和该哈希表指定的索引位置不为null
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //扩容会在重点讲解
        resize(2 * table.length);
        //这一步就是对null的处理,如果key为null,hash值为0,
        //也就是会插入到哈希表的表头table[0]的位置
        hash = (null != key) ? hash(key) : 0;
        //因为扩容了,需要重新计算哈希表的位置
        bucketIndex = indexFor(hash, table.length);
    }
    //创建Entry结点的操作
    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++;
}
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;

    /**
     * Creates new entry.
     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
	}
}

主要注意createEntry()方法中,使用了table[bucketIndex]作为Entry构造函数的第四个参数,并且将返回值传给table[bucketIndex],这句话实现了hashMap添加新节点的头插法:可以看到Entry构造函数中第四个参数n被作为新产生的Entry的下一个元素,因此原来的链表的头被接在了当前Entry的next上。

扩容
void resize(int newCapacity) {
    //引用扩容前的Entry数组 
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //如果扩容前的数组大小如果已经达到最大(2^30)了 
    //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //创建一个新的哈希表
    Entry[] newTable = new Entry[newCapacity];
    //将当前所有的哈希表数据复制到新的哈希表
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    //计算临界值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

//将当前所有的哈希表数据复制到新的哈希表
void transfer(Entry[] newTable, boolean rehash) {
   int newCapacity = newTable.length;
   //遍历旧的哈希表
   for (Entry<K,V> e : table) {
       while(null != e) {
           //保存旧的哈希表对应的链表头的下一个结点
           Entry<K,V> next = e.next;
           if (rehash) {
               e.hash = null == e.key ? 0 : hash(e.key);
           }
           //因为哈希表的长度变了,需要重新计算索引
           int i = indexFor(e.hash, newCapacity);
           //第一次循环的newTable[i]为空,赋值给当前结点的下一个元素,
           //下面有图会讲解这句代码的含义
           e.next = newTable[i];
           //将结点赋值到新的哈希表
           newTable[i] = e;
           e = next;
       }
   }
}

hashMap的转移也使用了头插法,因此得到的顺序应该是与原来相反的。

JDK8源码

Entry改名为Node

put()方法
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; Node<K,V> p; int n, i;
    // 第一次执行put的时候分配数组内存
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果当前key在数组中对应的位置没有元素,插入成功
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 对应位置已经存在了链表或者红黑树
    else {
        Node<K,V> e; K k;
        // 如果该位置第一个元素的key就和插入的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) {
                // 走到null了,进行尾插
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 链表长度过长了,转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 找到了某个位置的key与插入key相同
                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;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值