一、Java底层数据存储结构介绍
在Java语言中,数据存储方式最底层的两种结构,一种是数组,另外一种是链表。其中数组的特点是空间连续,根据下标寻址较快,但是在删除或者添加元素的时候需要较大幅度的移动,总结就是查询速度快,增删速度慢;而链表的特点是空间不连续,通过首尾指针连接前一个元素和后一个元素,因此,这样的结构寻址困难,但增删元素较快,只需修改指针的指向,总结就是查询慢,增删快。
今天的主角HashMap,它的实现就采用了Java语言中数据存储方式最底层的两种结构,也就是数组+链表的方式实现。
二、关于效率
获取 | 查找 | 添加/删除 | 空间 | |
---|---|---|---|---|
ArrayList | O(1) | O(1) | O(N) | O(N) |
LinkedList | O(N) | O(N) | O(1) | O(N) |
HashMap | O(N/Bucket_size) | O(N/Bucket_size) | O(N/Bucket_size) | O(N) |
上面的表格就是对比HashMap的效率,ArrayList底层的实现结构是数组,LinkedList的底层实现结构是链表,而HashMap的底层实现结构是数组+链表,这样也就对比了数组、链表、数组+链表在各个功能上的执行效率。
三、关于HashMap<K,V>的key值
从源码中我们可以看出,HashMap的key值可以是基本数据类型,也可以是引用数据类型,如果是对象,我们最好要实现两个方法,也就是Object类中的equals方法和hashCode方法,hashCode方法是用来计算存储在数组中的哪一个位置,而equals方法是用来比较数组展开的链表的元素是否相等。这样做的目的是尽量减少hash碰撞,所谓hash碰撞,也称为hash冲突,是指两个不同的key,它们计算的hash值是相同的,那么就根据equals来存到对应的链表上,如果equals值也相同,那么就会覆盖原值,如果hashCode方法实现的太不尽如人意,那么HashMap就会退化成链表了!不过JDK8中,对HashMap做了优化,如果散列表中的元素过多,会把链表转为红黑树。方法treeifyBin()就是树形化的方法:
例如我们经常使用String类型做key值,String 就自个实现了equals方法和hashCode方法:
四、关于HashMap线程安全问题
一旦问道HashMap是否是线程安全的,我们都会异口同声的说:不是。HashMap基于效率的问题,设计的不是线程安全的,线程安全的Map有HashTable、ConcurrentHashMap、SychronizedMap,HashTable相比与HashMap是线程安全的,因为它的所有方法都加了锁,所以效率非常低,部分源码如下:
SychronizedMap是在方法中加入了同步代码块,效率也可见一般:
在线程安全方面,性能最好的是ConcurrentHashMap,是解决线程安全方面的大神Doug Lea的良心之作,ConcurrentHashMap的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响,采用分段加锁的思想提高了性能,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,Segment数组中每一个元素就是一把锁,每一个Segment元素存储的是HashEntry数组+链表。
五、关于HashMap的扩容问题
HashMap默认大小是16,当HashMap中的元素个数超过数组大小*loadFactor(0.75)时,就会把数组的大小扩展为原来的两倍大小,不然效率会下降的比较快(链表的问题),然后重新计算每个元素在数组中的位置。扩容是要遍历整个表的,并且重新计算每个元素的位置,所以要注意避免集合的扩容,它会很耗性能,根据元素的数量给它一个初始大小的值。
相信随着技术的成熟与跟进,HashMap的性能会越来越好,或者HashMap的替代集合会越来越好,毕竟不缺少智慧的大脑,只是缺少动手的设计与实现!