Java集合面试题

Java中常用的容器有哪些?

常见容器主要包括Collection和Map两种,Collection存储着对象的集合,而Map存储着键值对(两个对象)的映射表

Collection

  • Set

  1. TreeSet:基于红黑树实现,支持有序性操作,例如:根据一个范围查找元素的操作。但是查找效率不如HashSet,HashSet查找的时间复杂度为O(1),TreeSet则为O(logN)。

  2. HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用Iterator遍历HashSet得到的结果是不确定的。

  3. LinkedHashSet:具有HashSet的查找效率,且内部使用双向链表维护元素的插入顺序。

  • List

  1. ArrayList:基于动态数组实现,支持随机访问。

  2. Vector:和ArrayList类似,但它是线程安全的(这里需要注意:Vector的单个操作时原子性的,也就是线程安全的。但是如果两个原子操作复合而来,这个组合的方法是非线程安全的,需要使用锁来保证线程安全,具体可以看这篇文章:Vection的非线程安全操作

  3. LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList还可以用作栈、队列和双向队列。

  • Queue

  1. LinkedList:可以用它来实现双向队列。

  2. PriorityQueue:基于堆结构实现,可以用它来实现优先队列。

Map

  1. TreeMap:基于红黑树实现。

  2. HashMap:基于哈希表实现。

  3. HashTable:和HashMap类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入HashTable并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用ConcurrentHashMap来支持线程安全,并且ConcurrentHashMap的效率会更高,因为ConcurrentHashMap引入了分段锁。

  4. LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或最近最少使用(LRU)顺序。

ArrayList和LinkedList的区别?

ArrayList:底层是基于数组实现的,查找快,增删较慢;

LinkedList:底层是基于链表实现的。确切的说是循环双向链表(JDK1.6之前是双线循环链表、JDK1.7之后取消了煦暖),查找慢、增删快。LinkedList链表由一系列表项连接而成,一个表项包含3个部分:元素内容、前驱表和后驱表。链表内部有一个header表项,既是链表的开始也是链表的结尾。header的后继表项是链表中的第一个元素,header的前驱表项是链表中的最后一个元素。

ArrayList的增删未必就是比LinkedList要慢:

  1. 如果增删都是在末尾来操作【每次调用的都是remove()和add()】,此时ArrayList就不需要移动和复制数组来进行操作。如果数据量具有百万级的时,速度是会比LinkedList要快的。

  2. 如果删除操作的位置是在中间。由于LinkedList的消耗主要是在遍历上,ArrayList的消耗主要是在移动和复制上(底层调用的是arraycopy()方法,是native方法)。LinkedList的遍历速度是要慢于ArrayList的复制移动速度的,如果数量有百万级的时候,还是ArrayList要快。

ArrayList 实现 RandomAccess 接口有何作用?为何 LinkedList 却没实现这个接口?

  1. RandomAccess接口只是一个标志接口,只要List集合实现这个接口,就能支持快速随机访问。通过查看Collections类中的binarySearch()方法,可以看出,判断List是否实现RandomAccess接口来实行indexedBinarySearch(list,key)或iteratorBinarySerach(list, key)方法。再通过查看这两个方法的源码发现:实现RandomAccess接口的List集合采用一般的for循环遍历,而未实现这接口则采用迭代器,即ArrayList一般采用for循环遍历,而LinkedList一般采用迭代器遍历;

  2. ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快。所以说,当我们在做项目时,应该考虑到List集合的不同子类采用不同的遍历方式,能够提高性能。

Array和ArrayList有何区别?什么时候更适合用Array?

  1. Array可以指定容纳基本类型和对象,而ArrayList只能容纳对象;

  2. Array可以指定大小,而ArrayList大小是不固定的。

什么时候更适合用Array:

  1. 如果列表的大小已经指定,大部分情况下是存储和遍历他们;

  2. 对于遍历基本数据类型,尽管Collection使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢;

  3. 如果使用多维数组,使用 [ ] [ ] 比List<<>>更容易。

HashMap的实现原理/底层数据结构?JDK1.7和JDK1.8

JDK1.7:Entry数组+链表

JDK1.8:Node数组+链表/红黑树,当链表上的元素超过8个并且数组长度>=64时自动转化成红黑树,节点变成树节点,以提高搜索效率和插入效率到O(logN)。Entry和Node都包括key、value、hash、next属性。

HashMap的put方法的执行过程?

但我们想往一个HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依次比较其key的hash值。如果两个hash值相等且key值相等(e.hash hash && ((k = e.key) key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等,则进行插入操作。

不过呢,插入操作在JDK1.7和JDK1.8是有所不同的,JDK1.7底层采用数组+链表,插入时采用头插法,JDK1.8,底层采用数组+链表/红黑树,并且吧头插法改成了尾插法,主要是为了减少线程安全的问题,另外,当链表长度大于8,且数组长度大于64时,会把链表转化为红黑树处理,这个时候,就无关是头插还是尾插了,得按照红黑树的规则来插了。

HashMap 的get方法的执行过程?

通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。

在这里能够根据key快速的取到value除了和HashMap的数据结构密不可分外,还和Entry有莫大的关系。HashMap在存储过程中并没有将key,value分开来存储,而是当做一个整体key-value来处理,这个整体就是Entry对象。同时value也只相当于key的附属而已。在存储的过程中,系统根据key的HashCode来决定Entry在table数组中的存储位置,在取得过程中同样根据key的HashCode取出相对应的Entry对象(value就包含在里面)。

HashMap的resize方法的执行方法?

有两种情况会调用resize方法:

  1. 第一次调用HashMap的put方法时,会调用resize方法对table数组进行初始化,如果不传入指定值,默认大小为16。

  2. 扩容时会调用resize,即size > threshold时,table数组大小翻倍。

HashMap的size为什么必须是2的整数次方?

  1. 这样做总是能够保证HashMap的底层数组长度为2的n次方。当length为2的n次方时,h&(length-1)就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化。而且每次扩容都是翻倍。

  2. 如果length为2的次幂,则length-1转化为二进制必定是1111.......的形势,在与h的二进制进行与操作时效率会非常快,为15,则length-1为14,对应的二进制为1110,在于h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率,这样就会造成空间的浪费。

HashMap的get方法能否判断某个元素是否在map中?

HashMap的get函数的返回值不能判断一个key是否包含在map中,因为get返回null有可能是不包含key,也有可能该key对应的value为null。因为HashMap中允许key为null,也允许value为null。

HashMap与HashTable的区别是什么?

  1. HashTable基于Dictionary类,而HashMap是基于AbstractMap。Dictionary是任何可将键映射到相应值的类的抽象父类,而AbstractMap是基于Map接口的实现,他以最大限度地减少实现此接口所需的工作。

  2. HashMap的key和value都允许为null,而HashTable的key和value都不允许为null。HashMap遇到key为null的时候,调用putForNullKey方法进行处理,而对value没有处理;HashTable遇到null,直接返回空指针异常(NullPointException)。

  3. HashTable是线程安全的,HashMap不是线程安全的,但是我们也可以通过Collection.synchronizedMap(HashMap),使其实现同步。

HashTable的补充:

HashTable和HashMap的实现原理几乎都一样,差别无非是

  1. HashTable不允许key和value为null;

  2. HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronize的,这相当于给整个哈希表上了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。

HashMap与ConcurrentHashMap的区别是什么?

HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。

ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了小的片段segment,而且每个小的segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁,这样做明显减小了锁的粒度。

ConcurrentHashMap的实现原理是什么?

数据结构

JDK 7 中:ConcurrentHashMap采用了数组 + Segment + 分段锁的方式实现。

JDK 8 中:ConcurrentHashMap参考了JDK 8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。

ConcurrentHashMap采用了非常精妙的“分段锁”策略,ConcurrentHashMap的主干是Segment数组。

final Segment<K,V>[] segment;

Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护一个HashEntry树组,并发环境下,对于不同Segment的数据进行操作是不应考虑锁竞争的。就按默认的ConcurrentLevel为16来讲,理论上就允许16个线程并发执行。

所以,对于同一个Segment的操作才需要考虑线程同步,不同的Segment则无需考虑。Segment类似有HashMap,一个Segment维护着一个HashEntry数组:

transient volatile HashEntry<K,V>[] table;

HashEntry是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。因此,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。

HashSet的实现原理?

HashSet的实现是依赖于HashMap的,HashSet的值都是存储在HashMap中的。在HashSet的构造法中会初始化一个HashMap对象,HashSet不允许值重复因此,HashSet的值是作为HashMap的key存储在HashMap中的,当存储的值已经存在时返回FALSE。

HashSet怎么保证元素不重复?

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
 }

因为HashSet的值是作为HashMap的key存储在HashMap中的,而HashMap中的key是不能重复的,所以HashSet的元素固然不会重复。

LinkedHashMap的实现原理?

LinkedHashMap也是基于HashMap实现的,不同的是它定义了一个Entry header,这个header不是放在Table里,它是额外独立出来的。LinkedHashMap通过继承HashMap中的Entry,并添加两个属性Entry before,after和header结合起来组成一个双线链表,来实现按插入或访问顺序排序。

LinkedHashMap定义了排序模式accessOrder,该属性boolean型变量,对于访问顺序,为true;对于插入顺序,则为false。一般情况下,不必指定排序模式,其迭代顺序即为默认插入顺序。

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

迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被成为“轻量级“对象,因为创建它的代价小。Java中的Iterator功能比较简单,并且只能单向移动:

  1. 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,他返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。

  2. 使用next()获得序列中的下一个元素。

  3. 使用hashNext()检查序列中是否还有元素。

  4. 使用remove()迭代器新返回的元素删除。

Iterator和ListIterator有什么区别?

Iterator可以用来遍历Set和List集合,但是ListIterator只能用来遍历List。Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引等等。

Iterator和Enumeration接口的区别?

与Enumeration相比,Iterator更加安全,因为当一个集合正在被遍历的时候,它会阻止其他线程去修改集合。否则会抛出ConcurrentModificationException 异常。这其实就是fail-fast机制。具体区别有三点:

  1. Iterator的方法名比Enumeration更科学;

  2. Iterator有fail-fast机制,比Enumeration更安全;

  3. Iterator能够删除元素,Enumeration并不能删除元素。

fail-fast与fail-safe有什么区别?

Iterator的fail-fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。

Java.util包中的所有集合类都被设计为fail-fast的,而java.util.concurrent中的集合类都为fail-safe的。当检测到正在遍历的集合的结构被改变时,fail-fast迭代器抛出ConcurrentModificationException ,而fail-safe迭代器从不抛出ConcurrentModificationException 。

Collection和Collections有什么区别?

Collection:是最基本的集合接口,一个Collection代表一组Object,即Collection的元素。他的直接继承接口有List,Set和Queue。

Collections:是不属于Java的集合框架的,它是集合类的一个工具类/帮助类。此类不能被实例化,服务于Java的Collection框架。它包含有关集合操作的静态多态方法,实现对各种集合的搜索、排序、线程安全等操作。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值