Mr. Cappuccino的第17杯咖啡——金三银四面试题之Java容器篇

1. Java集合概述

集合类存放于Java.util包中,主要有3种:set(集)、list(列表包含Queue)和map(映射)。

  1. Collection:Collection 是集合 List、Set、Queue的最基本的接口。
  2. Iterator:迭代器,可以通过迭代器遍历集合中的数据
  3. Map:是映射表的基础接口
    在这里插入图片描述
2. Set、List和Map有什么区别?

Set:无序,元素不可重复;
List:有序,元素可以重复;
Map:使用键值对(key-value)存储元素(key:无序,不可重复;value:无序,可重复);

3. ArrayList、LinkedList和Vector有什么区别?

共同点:有序,元素可以重复
ArrayList:基于数组实现,下标查询的时间复杂度为O(1),查询效率高,增删效率低(需要扩容),扩容是原来的1.5倍,线程不安全;
LinkedList:基于双向链表实现,下标查询的时间复杂度为O(log2n)(采用二分查找),查询效率低,增删效率高,线程不安全;
Vector:基于数组实现,下标查询的时间复杂度为O(1),查询效率高,增删效率低(需要扩容),扩容是原来的2倍,线程安全(synchronized同步锁);
补充:ArrayList采用数组存储,插入和删除的时间复杂度受位置影响,执行add(E e)方法的时候,时间复杂度为O(1),但是在指定位置插入元素执行add(int index, E element)方法时,时间复杂度则是O(n-i),需要将第i和第i个元素之后的(n-i)个元素向后移动一位;LinkedList采用双向链表存储,在头尾增删效率高,在中间位置增删效率低。ArrayList空间浪费主要体现在数组的结尾会预留一定容量的空间,而LinkedList空间浪费主要体现在每一个元素都会存放前驱结点和后继结点。ArrayList和Vector实现了RamdomAccess接口,表示其支持快速随机访问,而LinkedList不支持快速随机访问。

4. HashSet、LinkedHashSet和TreeSet有什么区别?

共同点:单列存储,元素不可重复,线程不安全;
HashSet:底层基于HashMap实现,可以存储null值,无序;
LinkedHashSet:是HashSet的子类,有序;
TreeSet:通过实现Compareable接口保证元素唯一,默认升序存放元素;

5. 为什么重写equals()方法时,必须要求重写hashCode()方法?

两个对象的hashcode值相等,但是两个对象的内容值不一定相等;
两个对象的值equals比较相等的情况下,则两个对象的hashcode值一定相等;

6. HashMap与Hashtable有什么区别?
  1. 线程是否安全、效率:HashMap线程不安全,效率高,Hashtable线程安全,效率低;
  2. 对键值为null的支持:HashMap的键值对都能允许为null,key值为null存放在数组的第0个位置,Hashtable的键值都不允许为null;
  3. 初始容量和扩容容量的大小不同:HashMap默认初始化大小为16,扩容大小为原来的2倍,而Hashtable默认的初始化大小为11,扩容大小为原来的2n+1;如果在创建时给定了容量初始值,HashMap会将其扩充为2的幂次方大小,而Hashtable会直接使用指定的大小;
  4. 继承的父类不同:Hashtable是继承自Dictionary类,而HashMap是继承自AbstractMap类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
  5. 遍历方法不同:Hashtable使用Enumeration遍历,HashMap使用Iterator进行遍历。
7. HashMap如何避免内存泄漏问题?

如果使用自定义对象作为HashMap集合的key,注意需要重写hashCode()和equals()方法,避免内存泄漏问题。

8. 什么是hash冲突?

内容值不相同,但是hashCode值相同,则计算的index值会发生冲突。

9. HashMap1.7与1.8底层如何实现(put方法底层实现)?

Java1.7底层实现:
基于数组+链表实现(Key和value封装成Entry对象)

  1. 根据key的hash值,计算该key存放在数组的第index个位置;
  2. 如果index发生冲突,则会使用单向链表存放,同一个链表中存放的都是hashCode值相同,但内容值不同的元素,如果index发生冲突,采用链表存放查询的时间复杂度是为 O(n),效率非常低,所以在JDK1.8开始优化改为红黑树;
  3. 使用头插入法(并发下扩容可能会产生死循环问题)

Java1.8底层实现:
基于数组+链表+红黑树实现(Key和value封装成Entry对象)

  1. 根据key的hash值,计算该key存放在数组的第index个位置;
  2. 如果index发生冲突,则会使用单向链表存放,当数组的容量>=64且链表长度>8,则会将链表转化成红黑树。红黑树查询的时间复杂度为O(logn),当红黑树的节点个数<6,则会将红黑树转化成链表;
  3. 使用尾插法解决了HashMap1.7版本并发下扩容产生的扩容死循环问题。
10. HashMap1.7与1.8底层如何实现(get方法底层实现)?

Java1.7底层实现:

  1. 根据key的hash值,计算该key存放在数组的第index个位置;
  2. 根据index下标找到数组中的链表,如果index没有发生冲突,时间复杂度为O(1),如果index发生了冲突,时间复杂度为O(n),从头到尾遍历链表比较值是否相等,效率非常低。

Java1.8底层实现:

  1. 根据key的hash值,计算该key存放在数组的第index个位置;
  2. 根据index下标找到数组中的链表或红黑树,如果index没有发生冲突,时间复杂度为O(1),如果index发生了冲突,该数组下存放的是链表,时间复杂度为O(n),该数组下存放的是红黑树,时间复杂度为O(logn);
11. HashMap中Key为null的元素存放在什么位置?

存放在数组的第0个位置。

12. HashMap1.8为什么需要引入红黑树?

在JDK1.7中,如果发生了hash冲突,则会将其存放在同一个链表中,当链表的长度过长时,查询效率非常低,链表的时间复杂度为O(n),从JDK1.8开始引入了红黑树,当数组容量>=64且链表长度>8,则会将链表转化成红黑树,红黑树的时间复杂度为O(logn),性能有所提升。

13. HashMap1.8链表在什么时候转化成红黑树?

当数组的容量>=64且链表长度>8,则会将链表转化成红黑树。

14. HashMap1.7扩容死循环的问题有了解过吗?

HashMap1.7版本使用头插入法(在并发下扩容可能会产生死循环问题),在HashMap1.8版本采用尾插法解决了该问题。
注意:JDK官方不承认该问题,因为在多线程的情况下不推荐使用HashMap,而应该使用Hashtable或者是ConcurrentHashMap。

15. HashMap是有序存放的吗?

HashMap是无序、散列存放的,遍历的时候从数组的第0个位置开始遍历每个链表,遍历结果与存储顺序不保证一致,如果需要根据存储顺序保存,可以使用LinkedHashMap,因为LinkedHashMap基于双向链表实现,存储的顺序是有序的。

16. HashMap如何解决hash冲突问题?

JDK1.7版本的HashMap:如果发生了hash冲突问题,则使用链表进行存放;
JDK1.8版本的HashMap:如果发生了hash冲突问题,则使用链表进行存放,但是如果数组的容量>=64且链表的长度>8,则会将链表转换成红黑树进行存放。

17. 谈谈HashMap根据key查询的时间复杂度?

如果key没有发生hash冲突:时间复杂度为O(1);
如果key发生了hash冲突:链表为O(n),红黑树为O(logn);

18. HashMap底层如何降低hash冲突概率?
  1. HashMap计算hash值使用扰动函数进行处理,将key的hashCode值进行高16位异或运算得到hash值,(h = key.hashCode()) ^ (h >>> 16);
  2. HashMap计算index值使用与运算(i = (n - 1) & hash)减少index冲突的概率;
  3. HashMap总是使用2的幂次方作为哈希表的容量大小。
19. HashMap存放1万条数据怎么样效率才会最高?

集合初始化时,指定集合初始值大小,initialCapacity = (需要存储的元素个数 / 负载因子) + 1,initialCapacity = 10000 / 0.75 + 1 = 13334.

20. HashMap的负载因子为什么是0.75而不是1?
  1. 如果负载因子越大,空间利用率越高,但是hash冲突的概率也会越大;
  2. 如果负载因子越小,hash冲突的概率越小,但是空间利用率不高;
  3. 空间和时间上的平衡点:0.75,统计学概率:泊松分布是统计学和概率学常见的离散概率分布。
21. 为什么HashMap使用2的幂次方作为哈希表的容量大小?

目的是为了减少hash冲突的概率,将数据均匀存放。

22. HashMap在什么时候需要进行扩容?

当HashMap的size达到阈值时(即++size > load factor * capacity)会进行扩容。

23. HashMap中的modCount有什么作用?

由于HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器的初始化过程中会将这个值赋给迭代器的 expectedModCount。在迭代过程中,判断modCount和expectedModCount是否相等,如果不相等就表示已经被其他线程修改了。

24. LinkedHashMap和TreeMap底层是如何保证有序的?

LinkedHashMap使用双向链表保证有序,而TreeMap是根据key元素的compareTo()方法实现排序(默认按key进行升序排序)。

25. 谈谈ConcurrentHashMap底层实现的原理?

Hashtable保证线程安全问题,是采用synchronized锁将整个Hashtable中的数组锁住,在多个线程中只允许一个线程进行put或者get操作,效率非常低。
在多线程的情况下推荐使用ConcurrentHashMap。

ConcurrentHashMap1.7 实现原理:

  1. ConcurrentHashMap采用分段锁设计,将一个大的HashMap集合拆分成n个不同的Hashtable(Segment),默认的情况下是分成16个不同的Segment,每个Segment中都有自己独立的HashEntry<K,V>[] table;
  2. 基于数组+Segments分段锁+HashEntry链表实现;
  3. 使用Lock锁+CAS乐观锁+UNSAFE类
  4. put()方法的流程:第一次需要计算出:key存放在哪个Segment对象中,然后还需要计算key存放在Segment对象中具体的index位置。

ConcurrentHashMap1.8 实现原理:
ConcurrentHashMap1.8基于数组+链表+红黑树实现,取消了Segment分段锁,采用CAS+synchronized保证线程安全问题,将锁的粒度拆分到每个index下标位置,synchronized锁从JDK1.6开始做了优化,默认实现锁的升级过程。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值