【Java 】 集合结构:HashMap、ArrayList等详解

键值对Map家族说明

Java为数据结构中的键值对定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap,类继承关系如下图所示:
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时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
  4. TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

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

HashMap存储结构详解

首先来看JDK8中HashMap存储结构示例代码

/** Jdk8 HashMap 分解详解 */
public class HashMapAnalysis<K,V> {

    /** Node实现了Map.Entry<K,V>用于数据存储键值对,如果K的hashCode相同,存储在Node实例中链表结构next当中,后出现的相同hashCode的键值对会出现在链表的前面。如果Key的相同(equals()方法),会替换相同hashCode相同Key的值 */
    transient Node<K,V>[] table;

    /** 当前Map的键值对的数量 */
    transient int size;

    /** Map需要扩容的负载因子,默认0.75 */
    final float loadFactor = 0.75f ;

    /** 下一个元素大小,如果达到扩容数字(容量*加载因子),需要resize(双倍扩容) */
    int threshold;

    /** HashMap中存储对象Node对象结构 */
    private class Node<K, V> implements Map.Entry<K,V>{
        /** 当前Key的hashCode值 */
        final int hash;
        /** 当前Entry中Key对象 */
        final K key;
        /** 当前Entry中Value对象 */
        V value;
        /** 当前Entry中Key的hashCode一样Entry的下一个Entry(链表存储) */
        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;
        }
    }
}

看属性元素的说明,就能知道意思了,也可以使用下图来理解:
HasHMap存储模型
HashMap使用table数组+链表存储Node<K,V>的,但是如何将键值对Key的hashCode值对应到table数组下标呢,看如下源码:

    //获得Key的扰动hashcode值,称为“扰动函数”
    static final int hash(Object key) {
        int h;
        /** 1:>>>无符号右移16位,等于取hashCode的前16位,高位补0(hashCode值为int类型,占4个直接)
		    2:^位异或运算(相同为0、不同为1)
		    目的:此函数一般称为“扰动函数”,将hash值的高16位与低16位进行异或操作,
		      混合了高位信息和低位信息,使得得到的新值更具有随机性,
		      可以规避hashCode低位上有许多冲突造成HashMap出现较大的桶碰撞几率。
		 */
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

	//取模jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的(如下putVal()方法第4行)  
	static int indexFor(int h, int length) {
		/** length为当前table桶数组的大小(都是2个次方大小),
		    length-1就是数组长度减一相当于一个低位掩码(低位全是1,以数组初始长度16为例,16-1=15,二进制表示就是00000000 00000000 00000000 00001111),
		    length-1与hashcode值做位与&运算,就相当于取模运算,刚好获得数组长度范围内的下标,示例如下
					00000000 00000000 00000000 00001111
				  & 11101010 00101000 10010101 10101110
				-------------------------------------------------------
				    00000000 00000000 00000000 00001110
*/ 
	    return h & (length-1); 
	} 
	//HashMap的put()方法源码,包含取模操作的jdk1.8源码
    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;
        //上面的n就是当前table[]桶数组的大小
        //下面就是n-1与原hash值取模的结果,作为table[]桶数组的下标值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
 /** 这里的(length-1)^hash刚好论证了,为什么HashMap扩容大小都是2的幂次方,方面低位掩码取模操作 */

HashMap非线程安全导致死循环问题说明

在多线程工作环境下,HashMap容量达到临界值的put操作后,其他线程get操作容易导致死循环。是因为HashMap容量达到临界值会进行2倍扩容,扩容完成之后会进行数据转移的工作在transfer(JDK1.8中已没有此方法)方法中,单线程时相同hashCode的Entry链表数据转移完成后元素会倒序,如果此时发生多线程操作,就容易造成死循环,死循环原理详解本文不详细说明(内容较多可另起文章说明)。ConcurrentHashMap将原本HashMap的单个数组存储形式,分解成多个段(Segment)的形态,每个段里面的结构和以前HashMap结构类似,这样使用分段锁,来减少锁的范围,提高并发效率。

HashMap常见面试题

HashTable, HashMap,TreeMap区别?

  • HashTable线程同步,HashMap非线程同步
  • HashTable不允许<键,值>有空值,HashMap允许<键,值>有空值
  • HashTable使用Enumeration,HashMap使用Iterator
  • HashTable中hash数组的默认大小是11,增加方式的old*2+1,HashMap中hash数组的默认大小是16,增长方式一定是2的指数倍。
  • TreeMap能够把它保存的记录根据键排序,默认是按升序排序

HashMap是不是有序的连环炮式发问

【你肯定回答说,不是有序的】。那面试官就会继续问你,有没有有顺序的Map实现类? 【你说有TreeMap和LinkedHashMap】。 那么面试官接下来就可能会问你,TreeMap和LinkedHashMap是如何保证它的顺序的?【LinkedHashMap根据put的顺序保证有序,TreeMap根据Key元素的Comparable接口实现方法保证有序】。 如果你依然回答上来了,那么面试官还会继续问你,你觉得它们两个哪个的有序实现比较好? 如果你依然可以回答的话,那么面试官会继续问你,你觉得还有没有比它更好或者更高效的实现方式? 如果你还能说出来的话,那么就你所说的实现方式肯定依然可以问你很多问题。 以上就是一个面试官一步一步提问的例子。所以,如果你了解的不多,千万不要敷衍,因为可能下一个问题你就暴露了,还不如直接说不会,把这个问题结束掉,赶紧切换到你熟悉的领域。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值