Java基础理解 之 HashMap

喜欢而已 提交于 2020-08-04 16:32:00

1. Q:HashMap 实现原理

    A:利用key的hashCode计算出数组的下标。存储时会出现两种情况,也就是key值相同与不相同,相同的情况下会覆盖原数据,不同时则在计算好的hashcode下标位置把值放到链表中。获取则是通过hashCode的下标,再进一步判断key是否相同,从而在链表中找到对应的值。当存储超过现有容量则扩容,可以把每个空间理解为桶,桶越多执行效率越快,以空间换时间。

    源码解读:

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  
    static final int MAXIMUM_CAPACITY = 1 << 30;        //最大容量  
    static final float DEFAULT_LOAD_FACTOR = 0.75f;     //填充比                                       
    static final int TREEIFY_THRESHOLD = 8;             //当add一个元素到某个桶,其链表长度达到8时将链表转换为红黑树  
    static final int UNTREEIFY_THRESHOLD = 6;  
    static final int MIN_TREEIFY_CAPACITY = 64;  
    transient Node<k,v>[] table;                        //存储元素的数组  
    transient Set<map.entry<k,v>> entrySet;  
    transient int size;                                 //存放元素的个数  
    transient int modCount;                             //被修改的次数fast-fail机制  
    int threshold;                                      //临界值 当实际大小(容量*填充比)超过临界值时,会进行扩容  
    final float loadFactor;                             //填充比 

在源码中我们可以看到 “红黑树”。   红黑树是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees)。

//红黑树  
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;    // needed to unlink next upon deletion  
    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的put方法

public V put(K key, V value) {  
        return putVal(hash(key), key, value, false, true);  
    }  
     /** 
     * Implements Map.put and related methods 
     * 
     * @param hash hash for key 
     * @param key the key 
     * @param value the value to put 
     * @param onlyIfAbsent if true, don't change existing value 
     * @param evict if false, the table is in creation mode. 
     * @return previous value, or null if none 
     */  
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,  
                   boolean evict) {  
        Node<K,V>[] tab;   
    Node<K,V> p;   
    int n, i;  
        if ((tab = table) == null || (n = tab.length) == 0)  
            n = (tab = resize()).length;  
    /*如果table的在(n-1)&hash的值是空,就新建一个节点插入在该位置*/  
        if ((p = tab[i = (n - 1) & hash]) == null)  
            tab[i] = newNode(hash, key, value, null);  
    /*表示有冲突,开始处理冲突*/  
        else {  
            Node<K,V> e;   
        K k;  
    /*检查第一个Node,p是不是要找的值*/  
            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) {  
        /*指针为空就挂在后面*/  
                    if ((e = p.next) == null) {  
                        p.next = newNode(hash, key, value, null);  
               //如果冲突的节点数已经达到8个,看是否需要改变冲突节点的存储结构,               
            //treeifyBin首先判断当前hashMap的长度,如果不足64,只进行  
                        //resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树  
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  
                            treeifyBin(tab, hash);  
                        break;  
                    }  
        /*如果有相同的key值就结束遍历*/  
                    if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))  
                        break;  
                    p = e;  
                }  
            }  
    /*就是链表上有相同的key值*/  
            if (e != null) { // existing mapping for key,就是key的Value存在  
                V oldValue = e.value;  
                if (!onlyIfAbsent || oldValue == null)  
                    e.value = value;  
                afterNodeAccess(e);  
                return oldValue;//返回存在的Value值  
            }  
        }  
        ++modCount;  
     /*如果当前大小大于门限,门限原本是初始容量*0.75*/  
        if (++size > threshold)  
            resize();//扩容两倍  
        afterNodeInsertion(evict);  
        return null;  
    }

HashMap的扩容机制 resize(), 值得注意的是 这里扩容容易造成耗时操作

HashMap 总结一句话就是 数据+链表  形成的产物

----------------------------------------------  以下为扩展内容   -----------------------------------------------------
HashMap
  HashMap 是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。遍历时,取得数据的顺序是完全随机的。
  HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null。
  HashMap不支持线程的同步(即任一时刻可以有多个线程同时写HashMap),可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。
  Hashtable与 HashMap类似,它继承自Dictionary类。不同的是:它不允许记录的键或者值为空;它支持线程的同步(即任一时刻只有一个线程能写Hashtable),因此也导致了 Hashtable在写入时会比较慢。

 

LinkedHashMap
  LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。也可以在构造时带参数,按照应用次数排序。
在遍历的时候会比HashMap慢,不过有种情况例外:当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢。因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

 

TreeMap
  TreeMap实现SortMap接口,能够把它保存的记录根据键排序。
  默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。

 

三种类型分别在什么时候使用
  1、一般情况下,我们用的最多的是HashMap。HashMap里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map 中插入、删除和定位元素,HashMap 是最好的选择。
  2、TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
  3、LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。
 

其他

1. HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key
2. Map的key和Set都有一个共同的特性就是集合的唯一性.TreeMap更是多了一个排序的功能.
3. hashCode和equal()是HashMap用的, 因为无需排序所以只需要关注定位和唯一性即可.
  a. hashCode是用来计算hash值的,hash值是用来确定hash表索引的.
  b. hash表中的一个索引处存放的是一张链表, 所以还要通过equal方法循环比较链上的每一个对象才可以真正定位到键值对应的Entry.
  c. put时,如果hash表中没定位到,就在链表前加一个Entry,如果定位到了,则更换Entry中的value,并返回旧value
4. 由于TreeMap需要排序,所以需要一个Comparator为键值进行大小比较.当然也是用Comparator定位的.
  a. Comparator可以在创建TreeMap时指定
  b. 如果创建时没有确定,那么就会使用key.compareTo()方法,这就要求key必须实现Comparable接口.
  c. TreeMap是使用Tree数据结构实现的,所以使用compare接口就可以完成定位了.
 
 
注意:

  1、Collection没有get()方法来取得某个元素。只能通过iterator()遍历元素。 
  2、Set和Collection拥有一模一样的接口。 
  3、List,可以通过get()方法来一次取出一个元素。使用数字来选择一堆对象中的一个,get(0)...。(add/get) 
  4、一般使用ArrayList。用LinkedList构造堆栈stack、队列queue。 
  5、Map用 put(k,v) / get(k),还可以使用containsKey()/containsValue()来检查其中是否含有某个key/value。 
        HashMap会利用对象的hashCode来快速找到key。哈希码就是将对象的信息经过一些转变形成一个独一无二的int值,这个值存储在一个array中。我们都知道所有存储结构中,array查找速度是最快的。所以,可以加速查找。发生碰撞时,让array指向多个values。即,数组每个位置上又生成一个梿表。 
  6、Map中元素,可以将key序列、value序列单独抽取出来。 
    使用keySet()抽取key序列,将map中的所有keys生成一个Set。 
    使用values()抽取value序列,将map中的所有values生成一个Collection。 
    为什么一个生成Set,一个生成Collection?那是因为,key总是独一无二的,value允许重复。


  

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!