HashMap、TreeMap详解

java面试总结(三)------HashaMap、TreeMap

HashMap和TreeMap作为最常用同时也是最容易被考察的点来说,掌握是至关重要的

  • HashMap
    基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。

    基于数组(Node[] table)和链表结合组成的复合结构,数组被分为一个个桶(bucket),通过哈希值决 定了键值对在这个数组的寻址;哈希值相同的键值对,则以链表形式存储,参考下面的示意图。这里 需要注意的是,如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),图中的链表就会被改造为树形结构。
    在这里插入图片描述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;
        this.threshold = tableSizeFor(initialCapacity);
    }
    
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

从上至下分别是 无参(以默认负载因子构造HashMap)、带初始容量参(以初始容量参和默认负载因子构造HashMap)、以初始容量和负载因子为参以另一个Map为参(此处不做重点)

我们着重看第三个构造函数,即以初始容量和负载因子为参的构造函数,在源码中,先经历了一系列的非法性判断,然后初始化负载因子,然后 tableSizeFor(initialCapacity) ,其中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;
    }

看不懂?没关系 ,看这篇资料,这里不做赘述,其实这个函数的意思是 返回大于输入参数且最近的2的整数次幂的数,即输入 10 返回 16,记住这个函数,这个会非常重要,那么为什么要这么做呢?请看文章最下面。

然后下面就讲最基础的几个api

 	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
		Node<K,V>[] tab; Node<K,V> p; int , i;
 		if ((tab = table) == null || (n = tab.length) = 0)
 			n = (tab = resize()).legth;
 		if ((p = tab[i = (n - 1) & hash]) == ull)
 		tab[i] = newNode(hash, key, value, nll);
 		 else {
		 // ...
 		if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for frs 
		 treeifyBin(tab, hash);
 		// ... 
 		}
	}
	public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    public void clear() {
        Node<K,V>[] tab;
        modCount++;
        if ((tab = table) != null && size > 0) {
            size = 0;
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
    }

在构造函数中,发现仅仅只是初始化了参数,并没有进行其他操作,是按照lazy-load原则,在首次使用时被初始化(拷贝构造函数除外),然后看 put函数,里面调用了putVal() 函数,
putVal()解析如下:

  • 如果表格是null,resize方法会负责初始化它,这从tab = resize()可以看出。

  • resize方法兼顾两个职责,创建初始存储表格,或者在容量不满足需求的时候,进行扩容(resize)。

  • 在放置新的键值对的过程中,如果发生下面条件,就会发生扩容。

	if (++size > threshold)
 		resize();
  • 具体键值对在哈希表中的位置(数组index)取决于下面的位运算
	i = (n - 1) & hash

为什么要做以上的位运算呢,也请看文章最最下面的问题部分。

2. TreeMap

TreeMap稍后详解

问题

  1. 扩容后,HashMap中原来的元素是怎么储存的
    参考资料
    即 :
    如果无链,原来的元素,要么在原位置,要么在原位置+原数组长度 那个位置上。
    如果有链,

    查阅权威资料再更

  2. 为什么每次扩容后大小必须是2的n次方&&为什么求下标是(n - 1) & hash?

    这两个问题可以一起回答,在源码中可以看到,每次扩容包括初始容量16必须是2的n次方,为什么呢?
    其实很容易回答,先回答另一个问题,在默认情况下(容量=16)怎么保证一个32位的二进制串在0-15中分布?大部分同学可能回答是取余,是的,在大部分情况看来,取余似乎是个不错的选择,但是取余会进行除法,比较慢,所以java8中提供了这么一种方法:

    对于默认情况,16=2的4次方,转成二进制即10000,然后按照源码公式,(10000-1)&hash,如图

在这里插入图片描述

这也就很好的解释了为什么容量必须是2的n次方,是为了满足按位与得出下标值的运算的条件,其原理是容量-1的二进制一定全是1,然后再与hash值做 按位与 运算,就能得到一个处于 0 - 容量 的大小的二进制串,也就得到它的下标,所以直接使用位运算速度快,且分布尽量在均匀范围内。

如果容量不是2的n次方,那么容量-1的二进制一定不全是1,如果用此值进行按位与操作,那么某一位是0的情况下会导致某个哈系桶将永远得不到储存,就违背了尽量均匀分布的原则.

  1. 如果两个键的hashcode相同,如何获取值对象
    找到参考资料再更

  2. 为什么默认负载因子是0.75
    在理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素个数和概率的对照表。

    从上面的表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的.
    参考

参考资料 :
HashMap

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章