【Java集合框架】篇五:HashMap、LinkedHashMap源码解析

1. Map及实现类特点

Map:存储key-value

  • HashMap:线程不安全,效率高,key和value都可以为null,底层使用 数组+单向链表+红黑树 结构(jdk8)。
    • LinkedHashMap:是HashMap的子类,在HashMap结构的基础上增加了一对双向链表,用于记录添加元素的先后顺序,便于遍历操作,开发中,对于频繁的遍历操作,建议使用此类。
  • Hashtable:古老实现类,线程安全,效率低,key和value都不可以为null,底层使用 数组+单向链表 结构存储(jdk8)。
    • Properties:是Hashtable的子类,其key和value都是String类型,常用来处理属性文件。将某些数据和代码分离,程序执行前会先读取该配置文件。
  • TreeMap:底层使用红黑树存储,可以按照添加key-value中的key元素指定规则进行遍历。考虑使用:自然排序、定制排序。

2. HashMap中元素的特点

  1. HashMap中的所有key之间是无序的、不可重复的。所有key就构成了一个Set集合。—>因而key所在类要重写hashCode()和equals()方法。
  2. HashMap中所有的value之间是无序的、可重复的。所有的value就构成了一个Collection集合。—>因而value所在类要重写equals()方法。
  3. HashMap中的一对key-value,构成了一个Entry对象。
  4. HashMap中的所有Entry对象之间是无序的、不可重复的。所有的Entry对象就构成了一个Set集合。

3. HashMap源码解析

3.1 JDK 1.7.0_07

new HashMap底层逻辑:

	HashMap<String, Integer> map = new HashMap<>();//底层执行Entry[] table = new Entry[16];
	map.put("name", 20);//将name和20封装到Entry对象中,并考虑将此对象添加到table数组中
  1. 当我们调用空参构造器时,底层自动创建一个容量为16的数组(类似饿汉式)
  2. 当我们执行put(k, v)方法时,底层将k和v封装到了Entry对象中,并将其放入数组中
  3. 因为HashMap有链表的存在,即使添加元素超出了16,依然能够添加元素,因而某些key相同的元素会形成链表,但是链表太深会影响查找的性能,因而在添加元素的个数达到临界值(12个)的时候就会触发扩容机制,默认扩容为原来的2倍。
  4. 临界值=数组的长度 * 加载因子 = 16 * 0.75 = 12。

new HashMap源码解析:

Entry实体
public class HashMap<K,V> implements Map.Entry<K,V>{
    transient Entry<K,V>[] table;
    
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;//使用key得到的哈希值2进行赋值

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        //略
    }
}
属性
//table数组的默认初始化长度
static final int DEFAULT_INITIAL_CAPACITY = 16;
//哈希表
transient Entry<K,V>[] table;
//哈希表中key-value的个数
transient int size;
//临界值、阈值(扩容的临界值)
int threshold;
//加载因子
final float loadFactor;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
构造器
public HashMap() {
    //DEFAULT_INITIAL_CAPACITY:默认初始容量16
  	//DEFAULT_LOAD_FACTOR:默认加载因子0.75
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);//调用HashMap(int initialCapacity, float loadFactor)方法
}
// 初始容量16,loadFactor加载因子0.75
public HashMap(int initialCapacity, float loadFactor) {
    ...
    //循环计算得到table数组的最终长度(保证capacity是2的整次幂)
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
	//加载因子,初始化为0.75
    this.loadFactor = loadFactor;
    // 确定了临界值为12
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //初始化table数组,容量为capacity
    table = new Entry[capacity];
   ...
}

put(key1,value1)添加/修改操作底层逻辑:

  1. 首先,需要调用key1所在类的hashCode()方法,计算当前key1的哈希值1,此哈希值经过某种算法(hash(key1))之后,得到哈希值2。
    哈希值2再经过某种算法(indexFor())之后,就确定了(key1,value1)在数组table中的索引位置i。
  • 1.1 若此索引位置i处没有元素,则(key1,value1)添加成功。 —> 情况1
  • 1.2 若此索引位置i处有元素(key2,value2),则需要比较key1和key2的哈希值2。—>哈希冲突
    • 2.1 若key1和key2的哈希值不同,则(key1,value1)添加成功。—>情况2
    • 2.2 若key1和key2的哈希值相同,则需要通过equals()比较key1和key2的value值是否相同。(在key1所在类的equals()中传入key2)
      • 3.1 若equals()返回值为false,则添加成功。—>情况3
      • 3.2 若equals()返回值为true,则key1和key2时相同的。默认为修改操作,将value1替换为value2。
  1. 说明:
  • 情况1:直接将(key1,value1)存放到索引i的位置。
  • 情况2、情况3:key1和key2的哈希值和内容不同,则(key1,value1)与(key2,value3)构成单向链表,(key1,value1)指向(key2,value2)(头插法)。

put(key1,value1)添加/修改操源码解析:

public V put(K key, V value) {
    // HashMap允许添加key为null的值,调用putForNullKey()方法将此(key,value)放到table[0]位置处。
    if (key == null)
        return putForNullKey(value);//该方法看下一段代码
    // 阿静key传入hash(),内部使用了key的哈希值1,此方法执行结束后,返回哈希值2.
    int hash = hash(key);//该方法看下一段代码
    // 确定当前(key,value)在数组中的存放位置i。
    int i = indexFor(hash, table.length);//该方法看下一段代码
    //检查table[i]下面有没有key与新的key是否重复,如果重复替换value
    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;//如果put()时修改操作,会返回原有旧的value值
        }
    }

    addEntry(hash, key, value, i);//将(key,value)封装为一个Entry对象,并保存在此索引i位置处。
    return null;//如果put()是添加操作,会返回null。
}
//如果key是null,直接存入[0]的位置
private V putForNullKey(V value) {
    //判断是否有重复的key,如果有重复的,就替换value
    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++;
    //把新的映射关系存入[0]的位置,而且key的hash值用0表示
    addEntry(0, null, value, 0);
    return null;
}
final int hash(Object k) {
    int h = 0;
    if (useAltHashing) {
        if (k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h = hashSeed;
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by 
	// constant multiples at each bit position have a bounded 
	// number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
    return h & (length-1);//没有使用取模运算计算索引位置,该方式效率更高
}
void addEntry(int hash, K key, V value, int bucketIndex) {
    //判断是否需要扩容
    //扩容:(1)size达到阈值(2)table[i]正好非空
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //table扩容为原来的2倍,并且扩容后,会重新调整所有key-value的存储位置
        resize(2 * table.length); 
        //新的key-value的hash和index也会重新计算
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
	//存入table中
    createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    //原来table[i]下面的映射关系作为新的映射关系next
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    //个数增加
    size++; 
}

3.2 JDK 1.8.0_271 与 JDK 1.7.0_07 的区别

  1. jdk8中,我们调用空参构造器,创建实例时,底层没用立即初始化数组容量,在我们第一次put元素时,才初始化一个容量为16的数组table,并将元素添加到table(类似懒汉式)
  2. jdk8中,将内部类Entry改名为Node(名称合理化),意味着我们创建的数组式Node[],不再是Entry[]。
  3. jdk8中,key1和key2的哈希值和内容不同,则(key1,value1)与(key2,value3)构成单向链表,(key2,value2)指向(key1,value1)(尾插法)。因为头插法在多线程的情况下会出现循环引用的问题。(七上八下)
  4. 存储结构:
    • jdk7:数组+单向链表
    • jdk8:数组+单向链表+红黑树
      什么时候会将单项链表改为红黑树存储?当数组某一个索引位置上的元素个数达到8,并且数组的长度达到64时,该位置上的元素会由单向链表改为使用红黑树进行存储。(因为红黑树在进行put、get、remove操作时比单项链表性能更好,树形化)
      什么时候会将红黑树改为单向链表存储?当红黑数上的元素倍remove到低于6时,就会将红黑树改为单向链表存储。(因为红黑树占用的空间时单项链表的2倍)
  5. 其他与jdk7中相同

Node实体

将Entry更名为Node,因为在链表或数中名为Node节点更合适

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

属性

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量 16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量  1 << 30
static final float DEFAULT_LOAD_FACTOR = 0.75f;  //默认加载因子
static final int TREEIFY_THRESHOLD = 8; //默认树化阈值8,当链表的长度达到这个值后,要考虑树化
static final int UNTREEIFY_THRESHOLD = 6;//默认反树化阈值6,当树中结点的个数达到此阈值后,要考虑变为链表

//当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
//当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量64

transient Node<K,V>[] table; //数组
transient int size;  //记录有效映射关系的对数,也是Entry对象的个数
int threshold; //阈值,当size达到阈值时,考虑扩容
final float loadFactor; //加载因子,影响扩容的频率

4. LinkedHashMap源码解析

LinkedHashMap复用了HashMap判断元素添加位置的逻辑,但是,在真正执行添加操作的时候重写了HashMap的newNode()方法,以为LinkedHashMap多了一个双向链表记录元素添加时的顺序。源码如下:

//重写newNode()
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);//连接元素添加的先后顺序
    return p;
}
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);
	}
}

5. 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 value):是否包含指定的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集合

你可能感兴趣的:(Java,java,数据结构,链表)