目录
- Java提供的常见集合
- List
- ArrayList底层实现与扩容机制
- ArrayList list=new ArrayList(10)中的list扩容几次
- 如何实现数组和List之间的转换
- 用Arrays.asList转List后,如果修改了数组内容,list受影响吗?List用toArray转数组后,如果修改了List内容,数组受影响吗
- ArrayList 和 LinkedList 的区别
- 如何解决ArrayList 和 LinkedList 不是线程安全问题
- HashMap
- HashMap的实现原理
- HashMap的扩容机制
- hashMap的寻址算法
- 为何HashMap的数组长度一定是2的次幂?
- hashmap在1.7的多线程死循环问题
- HashSet与HashMap的区别
- HashTable与HashMap的区别
- hashmap线程安全问题
Java提供的常见集合
主要分为两类:
- 第一个是Collection 属于单列集合,第二个是Map 属于双列集合在Collection中有两个子接口List和Set。在我们平常开发的过程中用的比较多像list接口中的实现类ArrarList和LinkedList。 在Set接口中有实现类HashSet和TreeSet。
- 在map接口中有很多的实现类,平时比较常见的是HashMap、TreeMap,还有一个线程安全的map:ConcurrentHashMap
List
ArrayList底层实现与扩容机制
ArrayList底层是用动态的数组实现的,初始容量为0,当第一次添加数据的时候才会初始化容量为10,在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组。
ArrayList list=new ArrayList(10)中的list扩容几次
在ArrayList的源码中提供了一个带参数的构造方法,这个参数就是指定的集合初始长度,所以给了一个10的参数,就是指定了集合的初始长度是10,这里面并没有扩容。
第二个构造方法是无参构造函数,默认创建一个空集合
如何实现数组和List之间的转换
数组转list,可以使用jdk自动的一个工具类Arrars,里面有一个asList方法可以转换为数组
List 转数组,可以直接调用list中的toArray方法,需要给一个参数,指定数组的类型,需要指定数组的长度。
用Arrays.asList转List后,如果修改了数组内容,list受影响吗?List用toArray转数组后,如果修改了List内容,数组受影响吗
Arrays.asList
转换list之后,如果修改了数组的内容,list会受影响!,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
list用了toArray
转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响
ArrayList 和 LinkedList 的区别
主要是底层使用的数据结构不一样,ArrayList 是动态数组,LinkedList 是双向链表,这也导致了它们很多不同的特点。
从操作数据效率来说
ArrayList按照下标查询的时间复杂度O(1)【内存是连续的,根据寻址公式】, LinkedList不支持下标查询查找(未知索引): ArrayList需要遍历,链表也需要链表,时间复杂度都是O(n)
ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)
LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)
从内存空间占用来说
ArrayList底层是数组,内存连续,节省内存
LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
从线程安全来说,ArrayList和LinkedList都不是线程安全的
如何解决ArrayList 和 LinkedList 不是线程安全问题
主要有两种解决方案:
-
我们使用这个集合,优先在方法内使用,定义为局部变量,这样的话,就不会出现线程安全问题。
-
如果非要在成员变量中使用的话,可以使用线程安全的集合来替代,但是使用同步锁属于下下策,会影响服务器性能,因为它会使并行变成串行。
-
- ArrayList可以通过Collections 的
synchronizedList
方法将 ArrayList 转换成线程安全的容器后再使用。
- ArrayList可以通过Collections 的
-
- LinkedList 换成
ConcurrentLinkedQueue
来使用
- LinkedList 换成
还可以使用ThreadLocal,把该属性绑定在本地线程中。
HashMap
HashMap的实现原理
JDK1.8之前采用的拉链法,数组+链表
JDK1.8之后采用数组+链表+红黑树,链表长度大于8
且数组长度大于64
则会从链表转化为红黑树
HashMap的扩容机制
- 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为
16
,以后每次扩容都是达到了扩容阈值(数组长度 *0.75
) - 每次扩容的时候,都是扩容之前容量的
2
倍;扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中 - 没有hash冲突的节点,则直接使用
e.hash & (newCap - 1)
计算新数组的索引位置 - 如果是红黑树,走红黑树的添加;如果是链表,则需要遍历链表,可能需要拆分链表,判断
(e.hash & oldCap)
是否为0,如果是0,该元素的位置则停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
hashMap的寻址算法
这个哈希方法首先计算出key的hashCode值,然后通过这个hash值右移16位后的二进制进行按位异或运算得到最后的hash值。
在putValue的方法中,计算数组下标的时候使用hash值与数组长度取模得到存储数据下标的位置,hashmap为了性能更好,并没有直接采用取模的方式,而是使用了数组长度-1 得到一个值,用这个值按位与运算hash值,最终得到数组的位置:e.hash & (newCap - 1)
为何HashMap的数组长度一定是2的次幂?
第一:
计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
第二:
扩容时重新计算索引效率更高:在进行扩容时会进行判断 hash值按位与运算
旧数组长租是否 == 0 如果等于0,则把元素留在原来位置 ,否则新位置是等于旧位置的下标+旧数组长度
hashmap在1.7的多线程死循环问题
jdk7的的数据结构是:数组+链表 在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环
比如说,现在有两个线程
线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入
线程二也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。
当线程一再继续执行的时候就会出现死循环的问题。
线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B,形成循环。
当然,JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题。
HashSet与HashMap的区别
HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法. 依靠HashMap来存储元素值,(利用hashMap的key键进行存储), 而value值默认是一个Object类型的常量 所以HashSet也不允许出现重复值, 判断标准和HashMap判断标准相同, 两个元素的hashCode相等并且通过equals()方法返回true.
HashTable与HashMap的区别
第一,数据结构不一样,hashtable是数组+链表,hashmap在1.8之后改为了数组+链表+红黑树
第二,hashtable存储数据的时候都不能为null,而hashmap是可以的
第三,hash算法不同,hashtable是用本地修饰的hashcode值,这可能导致更多的哈希冲突,从而影响性能。而hashmap经常了二次hash(可以看上面的寻址算法)
第四,扩容方式不同,hashtable是当前容量翻倍+1,hashmap是当前容量翻
倍
第五,hashtable是线程安全的,操作数据的时候加了锁synchronized,hashmap不是线程安全的,效率更高一些
hashmap线程安全问题
hashmap不是线程安全的,推荐采用ConcurrentHashMap进行使用,它是一个线程安全的HashMap
参考这篇博客。