Java知识点之Map(一)

Map

Map相关的内容在面试过程中都是一个重要的点。问深了会涉及到很多数据结构和线程相关的问题。

  1. 你了解Map吗?常用的Map有哪些?
    Map是定义了适合存储“键值对”元素的接口
    常见的Map实现类有HashMap、Hashtable、LinkedHashMap、TreeMap、ConcurrentHashMap
  2. HashMap的底层原理
    HashMap底层使用的数据结构是哈希表(又叫散列表)。哈希表又由数组+链表实现。数组的特点是空间连续,查询快,而增删慢;链表的特点是空间不连续,通过指针寻址,所以查询慢,而增删快。
    哈希表的核心思想就是让关键码值和存储位置建立一一映射关系,以通过Key直接快速查找到相应的Value。用来建立一一映射关系过程的函数,就是哈希函数。
    提到哈希函数就不可避免的扯到它的一些实现方式(如:直接寻址法、数字分析法、平方取中法、折叠法、随机数法、除留余数法等),以及在遇到哈希冲突后的解决方法(开放地址法、再哈希法、链地址法、公共溢出区法)。

HashMap实际采用的是位运算+链地址法解决哈希地址冲突的方案。
位运算计算hash值,如下:

  /**
   * 下面是jdk1.8版本中的hash算法,相比jdk1.7中少了三次位运算
   */
  static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

  /**
   * 经过hash()后的int值比较大,不适合我们数组。
   * 使用h&(len-1)操作,返回的索引值比较小适合我们的数组
   * 位运算法相比于除留余数法的模运算效率更高
   * jdk1.7版本中还有此静态方法;1.8中取消了此方法,在使用此方法处直接使用链式编程,实质不变
   */
  static int indexFor(int h, int length) {
      return h & (length-1);
  }

用下面一幅图描绘下此次hash的过程,用心感受一下
Java知识点之Map(一)_第1张图片

链地址法解决hash冲突,如下:
当两个Key的hash值一致时,就需要像table数组中的Entry对象(即:链表)中追加或替换要put的节点。
jdk1.7和jdk1.8对于此处的实现还是有些异同的。本处只讨论基本原理,不对源码在一段一段的详细分析。若有兴趣可从两个版本中的put()方法可对比看出。
jdk1.7: 若需要新增节点,则向链表头部新增节点,新节点的next域指向原头部节点。
jdk1.8: 若需要新增节点,则向链表尾部新增节点,原尾部节点next域指向新节点。重点来了,jdk8中在HashMap中就开始引入红黑树的数据结构,一旦链表中的节点个数超过了TREEIFY_THRESHOLD - 1这个阈值,就将链表转换成红黑树的结构

  1. HashMap什么时候扩容(resize、rehash),如何扩容
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;	// 数组容量:默认16
static final float DEFAULT_LOAD_FACTOR = 0.75f;		// 负载因子:默认0.75
static final int MAXIMUM_CAPACITY = 1 << 30;		// 最大容量

jdk1.7中的触发条件:

   /**
    * jdk1.7中对threshold定义如下:
    * threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    */
if ((size >= threshold) && (null != table[bucketIndex])) {
           resize(2 * table.length);
           hash = (null != key) ? hash(key) : 0;
           bucketIndex = indexFor(hash, table.length);
}

jdk1.8中的触发条件:

    /**
    * jdk1.8中对threshold定义如下:
    * threshold = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
    */
if (++size > threshold)
           resize();

由此可看出触发resize的条件均是map中存放的元素数量size值大于threshold值。差异点在threshold和resize()的具体实现。
1)对于threshold其实差异并不大,在元素数量不达到MAXIMUM_CAPACITY这个量级之前均可认作threshold=负载因子容量值。所以初始化后map的容量一旦超过0.7516=12,就会resize()。
2)由于对于jdk1.8中对HashMap的数据结构进行了优化,在超过一定阈值后会将链表优化成红黑树,在此不做详细分析。对于jdk1.7中,便可通过代码直观的看出,将容量扩充为当前table长度的2倍。
由于扩容后数组容量增加,还需要对原有数据重新分配索引,是一个比较耗性能的操作。若事先能估算出Map中预要存放的元素数量,建议初始化时就设置其容量,规则为:初始化容量*装载因子>预估元素数量,初始化容量建议取2的幂次方。

  1. HashMap、Hashtable的比较
    在jdk1.8版本以前HashMap和Hashtable采用相同的存储结构、实现基本类似。不同的是:
    (1)HashMap的key和value是允许为空的,key为null的节点放在table[0]中;而Hashtable则都不允许。
    (2)HashMap是非线程安全的;HashTable中的方法都加synchronized进行同步,是线程安全的。正因此,在线程安全环境下,HashMap的效率比HashTable的效率高。
    (3)HashMap的初始化数组大小为16,阈值为160.75=12,每次扩容都将数组容量扩大2倍;
    Hashtable的初始化数组大小为11,阈值为(int)11
    0.75=8,每次扩容执行int newCapacity = (oldCapacity << 1) + 1,即乘2加1;
    (4)HashMap中的hash值是取得key的hashcode后进行了一次位运算,同时计算索引时使用位运算;
    而Hashtable则直接使用key的hashcode作为hash值,在计算索引时,用取模运算。
    // 上面已经分析过HashMap的hash和index方式,不在赘述
    // 下面代码是Hashtable中实现hash和index的方式
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    

(5) 两个遍历方式的内部实现上不同。HashMap,Hashtable都使用了 Iterator。但由于历史原 因,Hashtable还使用了Enumeration的方式 。
5. TreeMap的特点(未完待续)

你可能感兴趣的:(Java基础相关)