Java集合框架
- 1、List、Set、Map的区别
- 2、ArrayList、LinkedList、Vector区别
- 3、为什么数组索引从0开始,而不是从1开始?
- 4、ArrayList底层的实现原理
- 5、红黑树、散列表
- 6、HashMap的底层原理
- 7、HashMap的put方法具体流程
- 8、HashMap的扩容机制
- 9、HashMap是怎么解决哈希冲突?
- 10、HashMap寻址算法
- 11、HashMap在1.7下的多线程死循环问题
- 12、HashMap、Hashtable、LinkedHashMap、TreeMap、ConcurrentSkipListMap选择
- 13、CocurrentHashMap底层原理
- 14、CopyOnWriteArrayList
- 15、
1、List、Set、Map的区别
2、ArrayList、LinkedList、Vector区别
- ArrayList基于动态数组实现,LinkedList基于双向链表实现
- ArrayList比LinkedList在随机访问时效率要更高,因为LinkedList是链表存储,要移动指针从前往后寻找。
- 在非首尾的增删操作时,LinkedList要比ArrayList效率高,因为ArrayList增删数据要先将插入或删除的后续元素后依次向后或向前移动。
- ArrayList和Vector的迭代器实现都是fail-fast的,两者允许null值,也可以使用索引值对元素进行随机访问。两者都是基于索引的,内部由一个数组支持,即都维护插入的顺序,可以根据插入顺序来获取元素。
- ArrayList是非线程安全的,Vector是线程安全的,ArrayList比Vector快,但一般需要保证线程安全可使用CopyOnWriteArrayList.来代替Vector。
3、为什么数组索引从0开始,而不是从1开始?
- 在根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式:数组的首地址 + 索引*存储数据的类型大小。
- 如果数组的索引从1开始,寻址公式中,就需要增加一次减法操作,对于CPU来说就多了一次指令,性能不高。
4、ArrayList底层的实现原理
- ArrayList底层是用动态数组实现的
- ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
- ArrayList在进行扩容的时候是按原来的1.5倍,每次扩容都需要拷贝数组
- ArrayList在添加数据时
1)确保数组已使用长度加1之后足够存下下一个数据
2)计算数组的容量,如果当前数组已使用长度+ 1后大于当前的数组长度,则会调用扩容方法,扩大为原来的1.5倍
3)确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
4)返回是否添加成功
ArrayList list = new ArrayList(10)中list扩容几次:
- 该语句只是声明和实例了一个ArrayList对象,指定了初始容量为10,未扩容
数组与ArrayList之间的转换:
- 可以使用Arrays.asList()来将数组转换成List,使用List中的toArray方法转成数组
- 使用Arrays.asList()来将数组转换成List后,如果改变原数组的数据,list的数组也会随之改变,因为asList的底层使用的是Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装,最终指向的都是同一个内存地址。
- list用toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是进行了数组的拷贝,跟原来的数组没关系了。
5、红黑树、散列表
-
红黑树:
红黑树是一种自平衡的二叉搜索树(BST);
所有的红黑规则都是希望红黑树能够保证平衡;
红黑树的时间复杂度:查找、添加、删除都是O(logn) -
红黑树的性质:
1)节点要么是红色、要么是黑色
2)根节点是黑色
3)叶子节点都是黑色的空节点
4)红黑树中红色节点的子节点都是黑色
5)从任一节点到叶子节点的所有路径都包含相同数目的黑色节点 -
散列表:
散列表又称哈希表,是根据键直接访问在内存存储位置值的数据结构,是由数组演化而来的,利用了数组支持按照下标进行随机访问的特性。
在散列表中,数组的每个下标位置我们可以称之为桶或槽,每个桶会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。
6、HashMap的底层原理
- HashMap底层使用hash表数据结构,即数组+链表+红黑树实现的。添加数据时,计算key的值确定元素在数组中的下标,key相同则替换,不同则存入链表或红黑树中。
7、HashMap的put方法具体流程
- 判断键值对数据table是否为空或null,否则执行resize()进行扩容(初始化)
- 根据键值key计算hash值得到数组索引
- 判断table[i] 是否等于null,如果元素为空,则直接新建节点添加
- 如果table[i]元素不为空,则分情况讨论:
1)判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
2)判断table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对
3)遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value。 - 插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold(数组长度*0.75),如果超过,进行扩容。
8、HashMap的扩容机制
- 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了扩容阈值(数组长度*0.75)
- 每次扩容时,都是扩容之前容量的2倍;
- 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
1)没有hash - 冲突的节点,则直接计算新数组的索引位置(e.hash & (newCap - 1))
- 如果是红黑树,走红黑树的添加
- 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & newCap)是否为0,该元素要么停留在原始位置,要么移动到原址位置+增加的数组大小这个位置上。
9、HashMap是怎么解决哈希冲突?
10、HashMap寻址算法
- 计算对象的hashCode值,再进行调用hash()方法进行二次哈希,hashcode值右移16位再异或运算,让哈希分布更均匀,最后(capacity - 1) & hash 得到索引。
- HashMap的数组长度为什么一定是2的次幂?
1)计算索引时效率更高:如果是2的n次幂可以使用位与运算代替取模
2)扩容时重新计算索引效率更高:hash & oldCap == 0 的元素留在原来位置,否则新位置= 旧位置 + oldCap
11、HashMap在1.7下的多线程死循环问题
- 在jdk1.7的HashMap中在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环。
如:有两个线程:
线程一:读到当前的HashMap数据,数据中一个链表,在准备扩容时,线程二介入。
线程二:也读取HashMap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来,比如原来的顺序是AB,扩容后的顺序变成了BA,线程二执行结束。
线程一:继续执行的时候就会出现死循环问题。
线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B形成循环。当然,jdk8将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中的死循环问题。
12、HashMap、Hashtable、LinkedHashMap、TreeMap、ConcurrentSkipListMap选择
- HashMap:没有锁机制,非线程安全,但效率比HashTable要高,允许key和value为null。不保证有序性,即遍历的顺序与put顺序不一定一致。
- Hashtable:内部方法都是synchronized(同步锁)修饰的,即线程安全;键值只要有一个是null,就出现空指针异常。
- HashMap提供对key的Set进行遍历,因此它是fail-fast的,但HashTable提供对key的Enumeration进行遍历,它不支持fail-fast。
- LinkedHashMap:LinkedHashMap保持有序性,遍历的顺序与put顺序一致。在]ava1.4中引入了LinkedHashMap,是HashMap的一个子类,假如你想要遍历顺序,你很容易从HashMap转向LinkedHashMap,但是Hashtable不是这样的,它的顺序是不可预知的。
- TreeMap:实现了SortedMap接口,保证了有序性,默认排序是根据key值进行排序,也可重写comparator方法根据key进行排序。
- ConcurrentSkipListMap:对于并发环境下的有序 Map 操作,ConcurrentSkipListMap 的性能优于 TreeMap。TreeMap 不是线程安全的,而 ConcurrentSkipListMap 提供了一种线程安全且高并发的有序 Map 实现。
总结:不要求保证有序性,使用HashMap,要求不改变put顺序使用LinkedHashMap,根据key排序使用TreeMap。需保证线程安全问题使用CocurrentHashMap。Hashtable是被认为一个遗留类,效率低,一般不使用。既要求线程安全,又要求有序可使用ConcurrentSkipListMap。
13、CocurrentHashMap底层原理
- jdk1.7底层采用分段的数组+链表实现。采用Segment分段锁,底层使用的是ReentrantLock。
- jdk1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表+红黑树。采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好。
14、CopyOnWriteArrayList
- 首先CopyOnWriteArrayList内部也是用数组来实现的,线程安全的并发集合,向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行。并且,写操作会加锁,防止出现并发写入丢失数据的问题,写操作结束后会把原数组指向新数组,CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时更新的数据,所以不适合实时性要求很高的场景