java8 HashMap的底层实现

最近在复习集合类的知识,尽可能将常见的集合类的底层代码都分析一遍。

1.理解HashMap

前面已经分析了ArrayList和LinkedList,两种数据结构实现的List存储有极端的差别:

线性表存储

  • 数组存储区建是连续的,占用内存严重,空间复杂度大
  • 寻址容易,但是插入删除复杂(如果都是在数组末尾操作,复杂度会相对较小)

链表存储

  • 存储区间离散,占用内存比较宽松,空间复杂度小
  • 寻址困难,插入删除简单

而哈希表结合了两种存储的特点,在HashMap中通过拉链表的方式(即链表的数组)实现
这里写图片描述
这里写图片描述

从上图中可以看到,哈希表结合数组和链表,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。

而java8中则结合了链表、数组和红黑树,当链表中的个数超过8个时,会转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法
这里写图片描述

2.HashMap的底层实现

  • 初始化
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    //默认装载因子0.75f.。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行rehash操作(扩容操作)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

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

    // 默认的初始容量16.
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    //临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
    int threshold;

     // 填充因子
    final float loadFactor;
   }

  • HashMap的构造方法
 //初始化,设置默认加载因子
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public HashMap(int initialCapacity) {
        this(initialCapacity, 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;
    // 指定容量后,tableSizeFor方法计算出临界值,put数据的时候如果超出该值就会扩容,该值肯定也是2的倍数
    // 指定的初始容量没有保存下来,只用来生成了一个临界值
    this.threshold = tableSizeFor(initialCapacity);
}


//生成临界值
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;
} 
  • Map中的节点,同样是以内部类的方式存在(这里面所有的方法都是final修饰的,不可以被重写,可以重载)
//链表节点
 static class Node<K,V> implements Map.Entry<K,V> {
        //节点的hashcode是固定的
        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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        //重写equals一般要重写hashCode方法
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }


//红黑树节点
 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

        /**
         * Returns root of tree containing this node.
         */
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
  • 获取哈希值(hashMap中的hash值并不是直接取对象的哈希值,而是把key的hashcode做一些运算以得到最终的哈希值,最后的哈希值表示对象的链表位置)
static final int hash(Object key) {
        int h;
        // h = key.hashCode() 为第一步 取hashCode值
        // h ^ (h >>> 16)  为第二步 高位参与运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • Map中的put方法(put的时候根据 h & (length - 1) 定位到那个桶然后看是红黑树还是链表再putVal,如果是链表则讲解点放在数组后的第一个节点,原先该位置的节点往后移,如果是红黑树,放入节点之后还要进行自旋并染色,已达到红黑树要求)
 //放入键值对
 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;
         // 如果tab为空或长度为0,则分配内存resize()
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // (n - 1) & hash找到put位置,如果为空,则直接put
        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)
                 // 红黑树的put方法比较复杂,putVal之后还要遍历整个树,必要的时候修改值来保证红黑树的特点
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 链表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                         // e为空,表示已到表尾也没有找到key值相同节点,则新建节点
                        p.next = newNode(hash, key, value, null);
                        // 新增节点后如果节点个数到达阈值,则将链表转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 容许空key空value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 更新hash值和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;
    }
  • Map的get方法(先获取key的hash值,并根据hash的值判断数组头节点是不是对应的值,不是在遍历链表或树的节点)
public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
 } 

//根据hash和key获取value
final Node<K,V> getNode(int hash, Object key) {  
        Node<K,V>[] tab;//Entry对象数组  
    Node<K,V> first,e; //在tab数组中经过散列的第一个位置  
    int n;  
    K k;  
    /*找到插入的第一个Node,方法是hash值和n-1相与,tab[(n - 1) & hash]*/  
    //也就是说在一条链上的hash值相同的  
        if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {  
    /*检查第一个Node是不是要找的Node*/  
            if (first.hash == hash && // always check first node  
                ((k = first.key) == key || (key != null && key.equals(k))))//判断条件是hash值要相同,key值要相同  
                return first;  
      /*检查first后面的node*/  
            if ((e = first.next) != null) {  
                if (first instanceof TreeNode)  
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);  
                /*遍历后面的链表,找到key值和hash值都相同的Node*/  
                do {  
                    if (e.hash == hash &&  
                        ((k = e.key) == key || (key != null && key.equals(k))))  
                        return e;  
                } while ((e = e.next) != null);  
            }  
        }  
        return null;  
    }  

3.最后再区分一下几个常见的Map

(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它继承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,使用双链表和哈希表的形式实现,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序

linkedHashMap的链表结构如下:
这里写图片描述

//以插入顺序遍历
Map<String,String> map=new LinkedHashMap<String,String>();

//以访问次序顺序遍历
Map<String,String> map=new LinkedHashMap<String,String>(16,0.75f,true);

实现原理:
以插入顺序遍历:插入一个新的节点,该节点的前指针指向上一个节点,上一个节点的后指针指向新节点

以访问次序顺序遍历:在LinkedHashMap中调用map方法后,会将这次访问的元素移至链表尾部,不断访问可以形成访问顺序排序的链表。

(4) TreeMap:TreeMap实现SortedMap接口,是通过红黑树实现的map,映射根据其键的自然顺序进行排序 ,或者根据 创建映射时提供的 Comparator 进行排序 ,具体取决于使用的构造方法。TreeMap的基本操作containsKey、get、put、remove方法,它的时间复杂度是log(n)。默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

(5)ConcurentHashMap:ConcurentHashMap是线程安全的HashMap,而ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁,可以简单理解成把一个大的HashTable分解成多个,形成了锁分离。(以前的HashTable是对整个map加锁)

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。

  上述Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。

set就是基于map实现的,其中set放入的对象是放到key值得位置,因为map要求key值不能重复,因此set中要求元素不能重复

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值