Java中常用的集合以及底层的数据结构的拓展问题

拓展

1. arraylist、vector存储的都是数组,它们有什么不同呢?

线程安全方面,Vector是线程安全的,ArrayList不是线程安全的。其中,Vector在关键性的方法前面都加了synchronized关键字,来保证线程的安全性。如果有多个线程会访问到集合,那最好使用Vector,因为使用vector的话,就不需要去编写线程安全的代码。

扩容机制方面,ArrayList在底层数组不够用时,会扩容到原容量的1.5倍,Vector是扩容到原容量的2倍,相比之下,ArrayList就比较节约内存空间。

2. ArrayList的扩容机制是怎么样的呢?

ArrayList扩容的本质,就是计算出新的扩容数组的长度,这个长度是原来长度的1.5倍,然后把新数组的实例创建出来,并将原有数组内容复制到新数组中去。

3. ArrayList和LinkedList的相同点和不同点?

线程安全:ArrayList和LinkedList都不保证线程安全。

底层数据结构:ArrayList的底层是数组,LinkedList的底层是双向链表。

插入删除元素:ArrayList采用数组存储元素,插入删除元素的时间复杂度约为O(n),LinkedList采用链表存储元素,插入删除元素的时间复杂度约为O(1)。

快速随机访问:ArrayList可以根据元素的序号快速访问,因为它的底层是数组,LinkedList就不支持,因为它的底层是链表。

内存空间:ArrayList在列表的结尾会预留一部分容量空间,因此会造成空间浪费。而LinkedList没有这种空间浪费,但是LinkedList存储的每个元素都要消耗比ArrayList更多的空间,因为LinkedList的底层是双向链表,还有额外存储指向前面和后面的指针。

4. 红黑树的特点是什么?

红黑树,是自平衡的可排序的二叉树,它通过在每个节点上增加颜色属性(红色或黑色)来维持特定的平衡条件,从而确保了插入、删除和查找操作的时间复杂度保持在O(log n)。

5. hashmap的扩容机制是什么?

JDK1.8之前的扩容机制:当HashMap中的元素个数超过容量与加载因子的乘积时,即 size > capacity * loadFactor,HashMap会触发扩容操作。扩容时,创建一个新的哈希桶数组,容量为原数组的2倍,然后将旧数组的所有元素重新进行哈希并迁移到新的数组中

JDK1.8及之后的扩容机制:扩容条件与1.7版相同,依旧是HashMap中的元素个数超过容量与加载因子的乘积时扩容。但是做了两点优化,第一,对索引计算进行优化,利用扩容以后容量仍然为2的幂次方的性质,巧妙地计算哈希值,避免了对每个元素重新计算hash值,极大提升了扩容的效率。(具体来说,假设原哈希表长度为oldCap(2^n),扩容后的新长度变为newCap(2^(n+1))。在转移元素时,只需检查原来的hash值新增的那个高位bit是0还是1来决定元素新的位置。如果该位为0,则元素在新数组中的索引不变;若该位为1,则索引变成“原索引 + oldCap”。)第二,对链表迁移方式进行优化,在JDK1.7中,链表迁移采用头插法,将旧链表节点迁移到新数组对应的链表中,这可能导致链表顺序反转。而从JDK1.8开始,HashMap在扩容时采用了尾插法,即新节点始终添加到链表的尾部。这种做法保持了链表节点原有的顺序,在多线程环境下更容易实现整个迁移过程的线程安全,更适应并发的场景。

6. hashmap为什么线程不安全

多线程下扩容死循环。JDK1.7中的HashMap的链表迁移使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。

多线程的put可能导致元素的丢失。多线程同时执行put操作,如果计算出来的索引位置是相同,那会造成前一个key被后一个key覆盖,从而导致元素的丢失。此问题在JDK1.7和 JDK1.8中都存在。

put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出限制而进行扩容,线程2此时执行get,有可能导致get为null。此问题在JDK1.7和JDK1.8中都存在。

因为,hashmap是线程不安全的,那么多线程情况下,我们就应该使用线程安全的 ConcurrentHashMap。

7. ConcurrentHashMap底层是怎么实现的?

JDK1.7:ConcurrentHashMap是由Segment数组结构和HashEntry链表结构组成,即 ConcurrentHashMap把哈希桶切分成多个小数组(Segment),每个小数组有n个HashEntry 链表。其中,Segment继承了ReentrantLock,ReentrantLock是可重入锁,所以Segment也是可重入锁,HashEntry则用于存储键值对数据。

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其它线程可以访问其它段的数据,能够实现并发访问。

JDK1.8:在数据结构上,JDK1.8的ConcurrentHashMap选择了与HashMap相同的数据结构,数组+链表+红黑树的结构;在锁的实现上,抛弃了原有的Segment分段锁,采用CAS + synchronized实现更加低粒度的锁。将锁的级别控制在了更细粒度的哈希桶元素级别,也就是说只需要锁住这个链表头结点(红黑树的根节点),就不会影响其它的哈希桶元素的读写,大大提高了并发度。

8. JDK1.7与JDK1.8中ConcurrentHashMap有什么区别?(见上)

数据结构、线程安全、锁的粒度、segment数组+链表转化成数组+红黑树(节点数量大于8)、时间复杂度(遍历链表O(n),遍历红黑树O(logN))。

9. ConcurrentHashMap和Hashtable的效率哪个更高?为什么?

ConcurrentHashMap 的效率要高于Hashtable,因为Hashtable给整个哈希表加了一把大锁从而实现线程安全。而ConcurrentHashMap 的锁粒度更低,在JDK1.7中采用分段锁实现线程安全,在JDK1.8 中采用 CAS+Synchronized 实现线程安全。

10. HashSet和HashMap区别是什么?

HashSet的底层其实就是HashMap,只不过HashSet是实现了Set接口,并且把数据作为K值,而V值一直使用一个相同的虚值来保存。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值