java面试题(容器)

点赞关注+收藏,万分感谢!!

1、Java 容器都有哪些?

Java 容器分为 Collection 和 Map 两大类,其下又有很多子类,如下所示:

Collection

List

ArrayList

LinkedList

Vector

Stack

Set

HashSet

LinkedHashSet

TreeSet

Map

HashMap

LinkedHashMap

TreeMap

ConcurrentHashMap

Hashtable

2、Collection 和 Collections 有什么区别?

Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。

Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,

比如提供的排序方法:Collections. sort(list),随机排序方法:shuffle,逆序排序:reverse,指定索引交换位置:swap,拷贝:copy。

3、List、Set、Map 之间的区别是什么?

List、Set、Map 的区别主要体现在两个方面:元素是否有序、是否允许元素重复。

三者之间的区别,如下表:

4、HashMap 和 Hashtable 有什么区别?

1、hash值不同

HashTable:直接使用对象的hashCode

HashMap:重新计算hash值

2、两个遍历方式的内部实现不同

Hashtable、HashMap两者都是使用了Iterator,但是,因为一些历史原因,Hashtable除了使用了Iterator之外,还使用了Enumeration。

3、是否提供contains方法

Hashtable:Hashtable和HashMap不同,它保留了contains、containsValue以及containsKey3个方法

HashMap:它去掉了Hashtable的contains方法,改为了containsKey和containsValue

4、内部实现使用的数组初始化和扩容方式不同

HashTable:在不指定容量的情况下的默认容量为11;不要求底层数组的容量一定要为2的整数次幂;扩容时将容量变为原来的2倍加1。

HashMap:在不指定容量的情况下的默认容量为16;要求一定为2的整数次幂;扩容时,将容量变为原来的2倍

HashTable中hash数组默认大小是11,增加的方式是old*2+1

5、key和value是否允许null值

Hashtable:key和value都不允许出现null值

HashMap:null能够作为键,这样的键只有一个,能够有一个或者是多个键所对应的值为null

6、线程安全性不同

Hashtable:Synchronize;在多线程并发的情况下,能够直接使用Hashtable,不要自己为它的方法实现同步

HashMap:在缺省情况下是非Synchronize的;使用HashMap的时候就需要自己增加同步处理;HashMap是线程不安全的

7、继承的父类不同

Hashtable:继承Dictionary类

HashMap:继承AbstractMap类

5、如何决定使用 HashMap 还是 TreeMap?

对于在 Map 中插入、删除、定位一个元素这类操作,HashMap 是最好的选择,因为相对而言 HashMap 的插入会更快,但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择。

6、说一下 HashMap 的实现原理?

1.hashMap底层是一个数组结构,数组中的每一项又是一个链表。数组+链表结构,它的常用方法 put get。

2.JDK7与JDK8的HashMap区别

jdk8中添加了红黑树,当链表长度大于等于8的时候链表会变成红黑树

链表新节点插入链表的顺序不同(jdk7是插入头结点,jdk8因为要把链表变为红黑树所以采用插入尾节点)

hash算法简化 ( jdk8 )

扩容resize的逻辑修改(jdk7会出现死循环,jdk8不会)

3.为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?

负载因子是用来计算扩容阈值用。

① 阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,

他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子x容量的结果是一个整数,这个值是0.75(4/3)比较合理,因为这个数和任何2的次幂乘积结果都是整数。

② 理论上来讲,负载因子越大,导致哈希冲突的概率也就越大,负载因子越小,费的空间也就越大,这是一个无法避免的利弊关系,

所以通过一个简单的数学推理,可以测算出这个数值在0.75左右是比较合理的

4.为什么计算扩容后容量要采用位移运算呢,怎么不直接乘以2呢?

因为cpu毕竟它不支持乘法运算,所有的乘法运算它最终都是再指令层面转化为了加法实现的,这样效率很低,如果用位运算的话对cpu来说就非常的简洁高效。

5.为啥HashMap中初始化大小为什么是16呢?

首先我们看hashMap的源码可知当新put一个数据时会进行计算位于table数组(也称为桶)中的下标:

int index =key.hashCode()&(length-1);

hahmap每次扩容都是以 2的整数次幂进行扩容

因为是将二进制进行按位于,(16-1) 是 1111,末位是1,这样也能保证计算后的index既可以是奇数也可以是偶数,并且只要传进来的key足够分散,均匀那么按位于的时候获得的index就会减少重复,这样也就减少了hash的碰撞以及hashMap的查询效率。

那么到了这里你也许会问? 那么就然16可以,是不是只要是2的整数次幂就可以呢?

答案是肯定的。那为什么不是8,4呢? 因为是8或者4的话很容易导致map扩容影响性能,如果分配的太大的话又会浪费资源,所以就使用16作为初始大小。

6.JDK7与JDK8及以后的HashMap结构与存储原理有所不同

Jdk1.7:数组 + 链表 ( 当数组下标相同,则会在该下标下使用链表)

Jdk1.8:数组 + 链表 + 红黑树 (预值为8 如果链表长度 >=8则会把链表变成红黑树 )

Jdk1.7中链表新元素添加到链表的头结点,先加到链表的头节点,再移到数组下标位置

Jdk1.8中链表新元素添加到链表的尾结点

(数组通过下标索引查询,所以查询效率非常高,链表只能挨个遍历,效率非常低。jdk1.8及以

上版本引入了红黑树,当链表的长度大于或等于8的时候则会把链表变成红黑树,以提高查询效率)

7.HashMap存储原理

① 获取到传过来的key,调用hash算法获取到hash值

② 获取到hash值之后调用indexFor方法,通过获取到的hash值以及数组的长度算出数组的下标

(把哈希值和数组容量转换为二进,再在数组容量范围内与哈希值进行一次与运算,同为1则1,不然则为0,

得出数组的下标值,这样可以保证计算出的数组下标不会大于当前数组容量)

③ 把传过来的key和value存到该数组下标当中。

④ 如该数组下标下以及有值了,则使用链表,jdk7是把新增元素添加到头部节点 jdk8则添加到尾部节点。

8.HashMap存储流程

前面寻址算法都是一样的,根据key的hashcode经过高低位异或之后的值,再按位与 &(table.lingth - 1),

得到一个数组下标,然后根据这个数组下标内的状况,状况不同,然后情况也不同,大概分为了4种状态:

( 1.)第一种就是数组下标下内容为空:这种情况没什么好说的,为空据直接占有这个slot槽位就好了,然后把当前.put方法传进来的key和value包装成一个node对象,放到这个slot中就好了。

( 2.)第二种情况就是数组下标下内容不为空,但它引用的node还没有链化:这种情况下先要对比一下这个node对象的key与当前put对象的key是否完全.相等,如果完全相等的情况下,就行进行replace操作,把之前的槽位中node.下的value替换成新的value就可以了,否则的话这个put操作就是一个正儿.八经的hash冲突,这种情况在slot槽位后面追加一个node就可以了,用尾插法 ( 前面讲过,jdk7是把新增元素添加到头部节点,而jdk8则添加到尾部节点)。

( 3.)第三种就是该数组下标下内容已经被链化了:这种情况和第二种情况处理很相似,首先也是迭代查找node,看看链表上中元素的key,与当前传过来的key是否完全一致,如果完全一致的话还是repleace操作,用put过来的新value替换掉之前node中的value,否则的话就是一致迭代到链表尾节点也没有匹配到完全一致的node,就和之前的一样,把put进来数据包装成node追加到链表的尾部,再检查一下当前链表的长度,有没有达到树化阈值,如果达到了阈值就调用一个树化方法,树化操作都是在这个方法里完成的。

( 4.)第四种情况就是冲突很严重的情况下,这个链表已经转化成红黑树了:红黑树就比较复杂 要将清楚这个红黑树还得从TreeNode说起 TreeNode继承了Node结构,在Node基础上加了几个字段,分别是指向父节点parent字段,指向左子节点left字段,指向右子节点right字段,还有一个表示颜色的red字段,这就是TreeNode的基本结构,然后红黑树的插入操作,首先找到一个合适的插入点,就是找到插入节点的父节点,然后红黑树它又满足二叉树的所有特性,所以找这个父节点的操作和二叉树排序是完全一致的,然后说一下这个二叉树排序,其实就是二分查找算法映射出来的结构,就是一个倒立的二叉树,然后每个节点都可以有自己的子节点,本且左节点小于但前节点,右节点大于当前节点,然后每次向下查找一层就能那个排除掉一半的数据,查找效率非常的高效,当查找的过程中也是分情况的。

首先第一种情况就是一直向下探测,直到查询到左子树或者右子树位null,说明整个树中,并没有发现node链表中的key与当前put key一致的TreeNode,那此时探测节点就是插入父节点的所在了,然后就是判断插入节点的hash值和父节点的hash值大小决定插入到父节点的左子树还是右子树。当然插入会打破平衡,还需要一个红黑树的平衡算法保持平衡。

其次第二种情况就是根节点在向下探测过程中发现TreeNode中key与当前put的key完全一致,然后就也是一次repleace操作,替换value。

9.jdk8中HashMap为什么要引入红黑树?

其实主要就是为了解决jdk1.8以前hash冲突所导致的链化严重的问题,因为链表结构的查询效率是非常低的,他不像数组,能通过索引快速找到想要的值,链表只能挨个遍历,当hash冲突非常严重的时候,链表过长的情况下,就会严重影响查询性能,本身散列列表最理想的查询效率为O(1),当时链化后链化特别严重,他就会导致查询退化为O(n)为了解决这个问题所以jdk8中的HashMap添加了红黑树来解决这个问题,当链表长度>=8的时候链表就会变成红黑树,红黑树其实就是一颗特殊的二叉排序树嘛,这个时间复杂…反正就是要比列表强很多。

10.扩容后的新table数组,那老数组中的这个数据怎么迁移呢?transfer()

迁移其实就是挨个桶位推进迁移,就是一个桶位一个桶位的处理,主要还是看当前处理桶位的数据状态把,这里也是分了大概四种状态:这四种的迁移规则都不太一样

(1.)第一种就是数组下标下内容为空:这种情况下就没什么可说的,不用做什么处理。

( 2.)第二种情况就是数组下标下内容不为空,但它引用的node还没有链化:当slot它不为空,但它引用的node还没有链化的时候,说明这个槽位它没有发生过hash冲突,直接迁移就好了,根据新表的tableSize计算出他在新表的位置,然后存放进去就好了。

( 3.)第三种就是slot内储存了一个链化的node:当node中next字段它不为空,说明槽位发生过hash冲突,这个时候需要把当前槽位中保存的这个链表拆分成两个链表,分别是高位链和低位链

设某个hash值为20,二进制0001 0100

当长度为16时,15&20=0000 1111 & 0001 0100=0000 0100=4

当长度为32时(扩容后),31&20=0001 1111 & 0001 0100 =0001 0100=20

可以看到,相同hash在不同的node数组长度的情况下得到的下标是不一样的。

再来看,当hash值为60时

60 & 15=0011 1100&0000 1111=0000 1100=12

60 & 31=0011 1100&0001 1111=0001 1100=28

可以看到两种hash在不同的node数组长度下,当计算下标不同时,相差都是16,也就是扩容的长度。

将低位链表的位置保持不动(即旧的node数组中是什么下标,那么在新的node数组中就是什么下标),将高位链表的位置由原先的下标n放到新node数组的n+16(扩容增加的长度)。

总结:当发现链表节点的hash和旧数组长度进行与运算fh & n的第X位为1时(为1时是高位,为0时是低位),说明这个节点的hash在扩容后和(新数组长度-1)的与运算得到的下标与原下标不同,而这个差值刚好是扩容长度16(32-16)。所以将高位直接迁移到原数组下标+扩容长度(16)的位置。

(4.)第四种就是该槽位储存了一个红黑树的根节点TreeNode对象:

处理方式与第3点大概相同,也是将元素拆分为高位链和低位链。TreeNode仍然保留了Node中的next字段,也就是说红黑树内部结构中仍然维护着一个链表,只是查询的时候并不会使用到它,但是在新增节点或者删除节点的时候仍然需要维护这个链表。这个next指针构造的链表结构也就是为了方便在去构建高位链和低位链时使用。不同的是,在拆分出来高位链和低位链后,会判断高位链链表长度是否小于等于6,若成立,则将treeNode转为普通Node链表,否则需要重建红黑树并放入到新数组中。

7、说一下 HashSet 的实现原理?

1.hashSet的数据结构

JDK8之前,底层采用数组+链表实现。

JDK8以后,底层进行了优化。由数组+链表+红黑树实现。

2.HashSet1.7版本原理解析

① 当我们用空参构造 创建一个Hashset 时 底层创建一个默认长度16,默认加载因子0.75的数组,数组名table

② 第一次添加元素时 根据元素的哈希值跟数组的长度计算出应存入的位置 例如:4在判断当前 4 索引位置是否为null,如果是null直接存入

③ 第二次添加元素时 根据元素的哈希值跟数组的长度计算出的结果 是 10 索引位置在判断当前位置是否为null,如果是null直接存入

④ 第三次添加元素时 根据元素的哈希值跟数组的长度计算出的结果 还是 4 索引位置此时 4 索引上 已经有了一个元素 不为null ,则会调用equals方法比较 内部的属性值

如果一样,则不存,如果不一样,则存入数组,老元素挂在新元素下面 形成链表结构

⑤ 第四次添加元素时 根据元素的哈希值跟数组的长度计算出的结果 还是 4 索引位置 此时 4 索引上 已经有了两个元素 会通过equals方法比较 从上到下 一 一比较

如果一样,则不存,如果不一样,则存入数组,老元素挂在新元素下面

扩容:当数组里面存了16*0.75 =12个元素的时候,数组就会扩容为原先的两倍 32

总结

  • 底层结构∶哈希表。(数组+链表)
  • 数组的长度默认为16,加载因子为0.75
  • 首先会先获取元素的哈希值,计算出在数组中应存入的索引
    • 判断该索引处是否为null
    • 如果是null,直接添加
    • 如果不是null,则与链表中所有的元素,通过equals方法比较属性值只要有一个相同,就不存,如果都不一样,才会存入集合。

3.HashSet1.8版本原理解析

通过对1.7的了解 可以发现 如果链表长度很长时,新存入的元素,需要通过equals比较的次数就越多,性能就会降低

即链表长度越长,添加元素时,效率越低

1.8对其经行优化 当链表长度为8时,再次添加元素自动转换成红黑树 ,当链表长度小于6时,自动转为链表

好处 equals比较次数减少,小了往左边比较,大了往右边比较

存储的过程:存储过程没有改变

总结

  • 底层结构∶哈希表。(数组、链表、红黑树的结合体)。
  • 当挂在下面的元素过多,那么不利于添加,也不利于查询,所以在JDK8以后,
    • 当链表长度超过8的时候,自动转换为红黑树。
    • 当链表长度小于6的时候,自动转换为链表。
    • 存储过程没有改变
  • 底层就是用的HashMap

8、ArrayList 和 LinkedList 的区别是什么?

数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。

增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

9、如何实现数组和 List 之间的转换?

数组转 List:使用 Arrays. asList(array) 进行转换。

List 转数组:使用 List 自带的 toArray() 方法。

10、ArrayList 和 Vector 的区别是什么?

线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。

性能:ArrayList 在性能方面要优于 Vector。

扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。

11、Array 和 ArrayList 有何区别?

Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。

Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。

Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

12、在 Queue 中 poll()和 remove()有什么区别?

相同点:都是返回第一个元素,并在队列中删除返回的对象。

不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。

13、哪些集合类是线程安全的?

Vector、Hashtable、Stack 都是线程安全的,而像 HashMap 则是非线程安全的,不过在 JDK 1.5 之后随着 Java. util. concurrent 并发包的出现,它们也有了自己对应的线程安全类,比如 HashMap 对应的线程安全类就是 ConcurrentHashMap。

ConcurrentHashMap总结

JDK1.7:分段锁,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是锁分离技术。

JDK1.8:取消Segment,直接用table存储,采用NODE+CAS+同步锁来保证并发安全。

① 首先,put()方法是没有用 synchronized修饰的,做插入操作时,首先进入乐观锁(死循环)。

② 然后,在乐观锁中判断容器是否初始化,如果没初始化则初始化容器(此时,这轮循环结束,因为被乐观锁锁住,开始下一轮循环。第二轮循环已经初始化,所以跳过)

③ 如果已经初始化,则判断该hash位置的节点是否为空,如果为空,则通过CAS操作进行插入。

通过key的 hash值来判断 table中是否存在相同的key,如果不存在,执行casTabAt()方法。

(注意,这个操作时不加锁的,看到里面的那行注释了么// no lock when adding to empty bin。位置为空时不加锁。这里其实是利用了一个CAS操作。)(

CAS(比较并交换,原子操作):

  1. 实际值和预期值相同

相同时,直接将值插入,因为此时是线程安全的。好了,这时插入操作完成。使用

break;跳出了乐观锁。循环结束。

  1. 实际值和预期值不同

不同时,不进行操作,因为此时这个值已经被其他线程修改过了,此时这轮操作就结束了,因为还被乐观锁锁住,进入第三轮循环。

第三轮循环中,前面的判断又会重新执行一次。

④ 如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。(MOVED表示在扩容中)

⑤ 如果没有扩容,则进行最后一步(第四次循环),先加排它锁,然后找到hash值相同的那个节点(hash冲突),

重新判断当前节点是不是第二轮判断过的节点,如果不是,表示节点被其他线程改过了,进入下一轮循环,

如果是,再次判断是否在扩容中,如果是,进入下一轮循环,如果不是,其他线程没改过,继续走,

循环判断这个节点上的链表,决定做覆盖操作还是插入操作。

循环结束,插入完毕。

14、迭代器 Iterator 是什么?

Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。

迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。

15、Iterator 怎么使用?有什么特点?

list.iterator while(it.hasNext()){it.next()}

Iterator 的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

16、Iterator 和 ListIterator 有什么区别?

Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。

Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。

ListIterator 从 Iterator 接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

17、怎么确保一个集合不能被修改?

可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值