首页 > 技术文章 > java集合——Map

jinb 2017-03-28 18:35 原文

声明:以下内容都是来自网络总结,将会参考很多,没有声明转载来源。

一、Map接口

1.HashMap

  HashMap和HashTable的区别:http://blog.csdn.net/shohokuf/article/details/3932967

  以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小得到数组下标。

  插入元素时,如果两条Key落在同一个桶上,称为hash冲突,JDK的做法是链表法,Entry用一个next属性实现多个Entry以单向链表存放。发生hash冲突时,先遍历桶里所有元素,逐个比较Hash值和Key值。在JDK8里,新增默认为8的阈值,当一个桶里的Entry超过阈值,就不以单向链表的结构来扩充而是采用红黑树的形式,这样可以提高查找的速度,单向链表是O(n),红黑树是O(logn)。

  最好的方法是每个桶里只有一个entry,如果桶数组的entry数量达到了一定的值,例如这个值为桶数组大小的3/4,就需要最这个桶数组进行扩容,并重新分配所有原来的Entry,这样扩容的成本很高,所以事先选择一个适当的大小给应用程序是很重要的。

  取模用与操作(hash&(arrayLength-1))会比较快,所以数组的大小永远是2的N次方。你随便给一个初始值比如17会转为32。默认第一次放入元素时的初始值是16。

  Iterator()时顺着哈希桶数组来遍历,不是有序的。

2.LinkedHashMap

  扩展HashMap,每个Entry增加双向链表,号称是最占用内存的数据结构。

  支持iterator()时按Entry的插入顺序来排序(如果设置accessOrder属性为true,则所有读写访问都排序)

  插入时,Entry把自己加到Header Entry的前面去。如果所有所有读写访问都要排序,还要把前后before/after拼接起来在链表中删除自己,所以此时读操作也是线程不安全的。

3.TreeMap

  以红黑树实现,红黑树又叫自平衡二叉树:

  http://www.cnblogs.com/v-July-v/archive/2010/12/29/1983707.html

  对于任一节点而言,其到叶节点的每一条路径都包含相同数目的黑节点。

  上面的规定,使得树的层数不会差的太远,使得所有操作的复杂度不超过 O(lgn),但也使得插入,修改时要复杂的左旋右旋来保持树的平衡。

  支持iterator()时按Key值排序,可按实现了Comparable接口的Key的升序排序,或由传入的Comparator控制。可想象的,在树上插入/删除元素的代价一定比HashMap的大。

  支持SortedMap接口,如firstKey(),lastKey()取得最大最小的key,或sub(fromKey, toKey), tailMap(fromKey)剪取Map的某一段

4.EnumMap

  

  EnumMap的原理是,在构造函数里要传入枚举类,那它就构建一个与枚举的所有值等大的数组,按Enum. ordinal()下标来访问数组。性能与内存占用俱佳。

  美中不足的是,因为要实现Map接口,而 V get(Object key)中key是Object而不是泛型K,所以安全起见,EnumMap每次访问都要先对Key进行类型判断,在JMC里录得不低的采样命中频率。

5.ConcurrentHashMap

  ConcurrentHashMap详解:http://ifeve.com/concurrenthashmap/;http://blog.csdn.net/liuzhengkang/article/details/2916620;

  并发优化的HashMap。

  在Jdk5里的经典设计,默认16把写锁(可以设置更多)有效分散了阻塞的概率。数据结构为Segment【】,每个Segment一把锁。Segment里面才是哈希桶数组。Key先算出它在那个Segment里,再去算它在那个哈希桶里。

  没有读锁,因为put/remove动作是个原子操作(比如put的整个过程是对一个数组元素/Entry指针引用的赋值操作),读操作不会看到一个更新 动作的中间状态。

  在Jdk8里,Segment【】的设计被抛弃了,改为精心的设计,只在需要锁的时候加锁。

  支持ConnectionMap接口,如putIfAbsent(key,value)与相反的replace(key,value)与以及实现的CAS的replace(key,oldValue,newValue).

  乐观锁:CAS算法,http://www.cnblogs.com/dongguacai/p/6003078.html

6.ConcurrentSkipListMap

  JDK6新增的并发优化的SortedMap,以SkipList结构实现。Concurrent包选用它是因为它支持基于CAS的无锁算法,而红黑树则没有好的无锁算法。

原理上,可以想象为多个链表组成的N层楼,其中的元素从稀疏到密集,每个元素有往右与往下的指针。从第一层楼开始遍历,如果右端的值比期望的大,那就往下走一层,继续往前走。

 

  典型的空间换时间。每次插入,都要决定在哪几层插入,同时,要决定要不要多盖一层楼。

它的size()同样不能随便调,会遍历来统计。

二、HashMap的实现原理

1.概述

  1、什么时候使用HashMap,他有什么特点

  2、HashMap原理,get和put的原理,equals()和hashCode()

  3、hash的实现

  4、扩容

import java.util.*;
import java.util.Map.Entry;
import java.io.*;
import java.util.Map;
public class CopyOfCopyOfCopyOfY{
    public static void main(String[] args){
        Map <String,Integer> map1 = new HashMap<String,Integer>();
        map1.put("yuwen", 1);
        map1.put("yuwen2", 2);
        map1.put("yuwen3", 3);
        map1.put("yuwen4", 4);
        map1.put("yuwen5", 5);
        map1.put("yuwen6", 6);
        map1.put("yuwen7", 7);
        map1.put("yuwen8", 8);
        map1.put("yuwen9", 9);
        map1.put("yuwen10", 10);
        for(Entry<String,Integer> entry:map1.entrySet()){
            System.out.println(entry.getKey()+":"+entry.getValue());
        }
    }
}

 

  

  几个重要的参数

  

/**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry[] table;

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    /**
     * The next size value at which to resize (capacity * load factor).
     * @serial
     */
    int threshold;

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient volatile int modCount;

 

  Capacity:桶(bucket)的大小,每个桶中存放的元素个数。

  Load factor:桶填满程度的最大比例。

  当bucket中的entries的数目大于cpacity*laod factory时就需要调整bucket的大小为当前的2倍。

  

    /**
     * Applies a supplemental hash function to a given hashCode, which
     * defends against poor quality hash functions.  This is critical
     * because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     */
    static int hash(int h) {
        // 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);
    }

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

 

2.put函数的实现

  1.对key的hashCode()做hash,然后再计算index

  2.如果没有碰撞直接放到bucket里;

  3.如果碰撞了,以链表的形式存在buckets后;

  4.如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD阈值初始值为8,jdk6中应该是4),就把链表转换成红黑树。

  5.如果节点已经存在就替换old value(保证key的唯一性);

  6.如果bucket满了(超过load factor*current capacity),就要2倍的resize。

  以下是基于JDK8版本的源码

 

 public V put(K key, V value){
  //对key的hashCode()做hash
  return putVal(hash(key),K key, V value,false,true);
}
 fianl V putVal(int hash,K key,V value,boolean onlyIfAbsent,boolean evict){
  Node<K,V> tab;Node<k,V> p;int n,i;
  //tab为空则创建
  if((tab=table) == null || (n = tab.length)==0){
    n = (tab = resize()).length;
  //1.计算index,并对null做处理
  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){
      e = ((TreeNode<K,V>)p).putTreeVal(this,tab,hash,key,value);
    //4.该链为链表
    else{
      for(int binCount = 0;;+binCount){
        if((e = p.next) == null){
          //把节点放到链表后
          p.next = newNode(hash,key,value,null);
          //判断是否大于阈值,进行链表和红黑树的转换
          if(binCount>=TREEIFY_THRESHOLD-1)
            treeifyBin(tab,hash);
            break;
          }
          //插入重复节点的话,继续后移链表
          if(e.hash == hash &&
            ((k = e.key)== key || (key != null &&key.equals(k))))
            break;
        p = e;
        }
    //写入
    if(e != null){
      V oldValue = e.value;
      //存在空值映射
      if(!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }      
  }
  ++modCount;
  //超过laod factor*current capcity,resize
  if(++size > threshold)
    resize();
  afterNodeInsertion(evict);
  return null;

}
}
}
}

3.get函数的实现源码

  1.bucket里的第一节点,直接命中

  2.如果有冲突,通过key.equals(k)去查找对应的entry;

    若为树,则在树中通过key.equals(k)查找,O(logn);

    若为链表,则在链表中通过Key。equals(k)查找,O(n);

 1 public V get(Object key){
 2   Node<K,V> e;
 3   return(e = getNode(hash(key),key)) == null?null:e.value;    
 4 }
 5 
 6 fianl Node<K,V> getNode(int hash,Object key){
 7   Node<k,V>[] tab;Node<K,V> first,e;int n;K k;
 8   if((tab = table) != null && (n =tab.length)>0 &&
 9       (first = tab(n-1)&hash]) != null){
10      //直接命中.
11      if(first.hash == hash && ((k = first.key) == key||(key!=null && key.equals(k))))
12         return first;
13      //未命中
14      if((e = first.next) != null){
15        //在树中获得
16        if(first instanceof TreeNode)
17        {
18            return ((TreeNode<k,V>)first).getTreeNode(hash,key);
19        }
20       //在链表中获得
21       do{
22           if(e.hash == hash && ((k = e.key) == key || (key!= null && key.equals(k))))
23           return e;
24 }while((e = e.next)!=null);
25     
26 }
27 
28 }  
29 return null;
30 }    
View Code

 4.hash函数的实现和数组下标

  在get和put的过程中,计算下标时,先对hashCode进行hash操作,然后再通过hash值进一步计算下标,

hash

 在对hashCode()计算hash时具体实现是这样的:

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看到这个函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或。
在设计hash函数时,因为目前的table长度n为2的幂,而计算下标的时候,是这样实现的(使用&位操作,而非%求余):
(n-1)&hash
  1. 首先根据hashCode()做hash,然后确定bucket的index;
  2. 如果bucket的节点的key不是我们需要的,则通过keys.equals()在链中找
5.resize的实现
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 超过最大值就不再扩充了,就只好随你碰撞去吧
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 没超过最大值,就扩充为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 计算新的resize上限
    if (newThr == 0) {

        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;
    if (oldTab != null) {
        // 把每个bucket都移动到新的buckets中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    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;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

https://my.oschina.net/placeholder/blog/180069

三、HashMap和Hashtable

HashMap和Hashtable的比较是Java面试中的常见问题,用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决问题。HashMap的工作原理、ArrayList与Vector的比较以及这个问题是有关Java 集合框架的最经典的问题。Hashtable是个过时的集合类,存在于Java API中很久了。在Java 4中被重写了,实现了Map接口,所以自此以后也成了Java集合框架中的一部分。Hashtable和HashMap在Java面试中相当容易被问到,甚至成为了集合框架面试题中最常被考的问题,所以在参加任何Java面试之前,都不要忘了准备这一题。

这篇文章中,我们不仅将会看到HashMap和Hashtable的区别,还将看到它们之间的相似之处。

HashMap和Hashtable的区别

HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。

  1. HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
  2. HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
  3. 另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
  4. 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
  5. HashMap不能保证随着时间的推移Map中的元素次序是不变的。

要注意的一些重要术语:

1) sychronized意味着在一次仅有一个线程能够更改Hashtable。就是说任何线程要更新Hashtable时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新Hashtable。

2) Fail-safe和iterator迭代器相关。如果某个集合对象创建了Iterator或者ListIterator,然后其它的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。但其它线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用set()方法,将会抛出IllegalArgumentException异常。

3) 结构上的更改指的是删除或者插入一个元素,这样会影响到map的结构。

我们能否让HashMap同步?

HashMap可以通过下面的语句进行同步:
Map m = Collections.synchronizeMap(hashMap);

结论

Hashtable和HashMap有几个主要的不同:线程安全以及速度。仅在你需要完全的线程安全的时候使用Hashtable,而如果你使用Java 5或以上的话,请使用ConcurrentHashMap吧

推荐阅读