Java开发面试之Java集合


参考 Java开发岗高频面试题

1 三大集合接口

1.1 Map Collection(Set Map接口)

在这里插入图片描述

1.2 常见的集合接口实现

1.2.1 Collection接口

List接口(有序,可重复)

  • ArrayList:特点:底层实现是数组,查询快,增删慢。线程不安全,效率高
  • LinkedList: 特点:底层实现是链表,查询慢,增删快,线程不安全,效率高,可做堆栈,队列使用
  • Vector:特点:底层实现是数组,查询快,增删慢,线程安全,效率低

注:集合Vector,它是线程安全的ArrayList,但是已经被废弃,不推荐使用了。多线程环境下,我们可以使用CopyOnWriteArrayList替代ArrayList来保证线程安全。

Set接口(无序,唯一)

  • HashSet: 特点:底层数据结构是哈希表。(无序,唯一)。依赖hashCode和equals两个方法来保证唯一性
  • LinkedHashSet:特点:底层数据是链表和哈希表,(FIFO插入有序,唯一),由链表保证有序,由哈希表保证唯一。
  • TreeSet:特点:底层数据结构是红黑树,(有序,唯一),
    1.如何保证元素有序?
    (1)自然排序(2)比较器排序
    2.如何保证元素唯一?
    根据比较的返回值是否为0来决定。

1.2.2 Map接口

  • HashMap :特点:无序,线程不安全,效率高,key和value都允许null值,父类是AbstractMap
  • HashTable:特点:无序,线程安全,效率高,不允许null值,父类是Dictionary
  • TreeMap:特点:有序

2 HashMap原理

2.1 HashMap原理-深入浅出HashMap

2.2 关于HashMap的一些问题

Q1:HashMap的初始容量,加载因子,扩容增量是多少?

HashMap默认情况下,初始容量是16,加载因子是0.75.扩容增量是原容量的一倍。在默认情况下 HashMap扩容是指元素个数(包括数组和链表+红黑树中)超过了16*0.75=12之后开始扩容。

Q2:HashMap的长度为什么是2的幂次方?

  • 我们将一个键值对插入HashMap中,通过将Key的hash值与length-1进行&运算,实现了当前Key的定位,2的幂次方可以减少冲突(碰撞)的次数,提高HashMap查询效率
  • 如果length为2的幂次方,则length-1 转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费
  • 如果length不是2的幂次方,比如length为15,则length-1为14,对应的二进制为1110,在与h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费

总结

也就是说2的N次幂有助于减少碰撞的几率,空间利用率比较大
加载因子,如果设置太小不利于空间利用,设置太大则会导致碰撞增多,降低了查询效率,所以设置了0.75。

2.3 HasMap的存储和获取原理

当调用put()方法传递键和值来存储时,先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象,也就是找到了该元素应该被存储的桶中(数组)。当两个键的hashCode值相同时,bucket位置发生了冲突,也就是发生了Hash冲突,这个时候,会在每一个bucket后边接上一个链表(JDK8及以后的版本中还会加上红黑树)来解决,将新存储的键值对放在表头(也就是bucket中)。

当调用get方法获取存储的值时,首先根据键的hashCode找到对应的bucket,然后根据equals方法来在链表和红黑树中找到对应的值

2.4 HashMap扩容步骤

HashMap里面默认的负载因子大小为0.75,也就是说,当Map中的元素个数(包括数组,链表和红黑树中)超过了16*0.75=12之后开始扩容。将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

但是,需要注意的是在多线程环境下,HashMap扩容可能会导致死循环。
HashMap扩容死循环问题------ps:还没看,头疼,不想看

2.5 哪些类适合作为HashMap的键?

String和Interger这样的包装类很适合做为HashMap的键,因为他们是final类型的类,而且重写了equals和hashCode方法,避免了键值对改写,有效提高HashMap性能。
为了计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashCode的话,那么就不能从HashMap中找到你想要的对象。

2.6 ConcurrentHashMap和Hashtable的区别?

ConcurrentHashMap结合了HashMap和Hashtable二者的优势。HashMap没有考虑同步,Hashtable考虑了同步的问题。但是Hashtable在每次同步执行时都要锁住整个结构
ConcurrentHashMap锁的方式是稍微细粒度的,ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁上当前需要用到的桶。

在实际的开发中,我们在单线程环境下可以使用HashMap,多线程环境下可以使用ConcurrentHashMap. 至于Hashtable已经不被推荐使用了(也就是说Hashtable只存在于面试题目中了)。

3.一些常见问题

3.1 TreeMap的特性

  • 底层使用红黑树来实现的,TreeMap存储的键值对按照键值来排序
  • 如果Key存入的是字符串等类型,那么会按照字典默认顺序排序
  • 如果传入的是自定义引用类型,比如说User,那么该对象必须实现Comparable接口,并且覆盖其compareTo方法;或者在创建TreeMap的时候,我们必须指定使用的比较器。如下所示:
    在这里插入图片描述
    Comparable接口和Comparator接口有哪些区别呢?
  • Comparable实现比较简单,但是当需要重新定义比较规则的时候,必须修改源代码,即修改User类里边的compareTo方法
  • Comparator接口不需要修改源代码,只需要在创建TreeMap的时候重新传入一个具有指定规则的比较器即可。

3.2 Iterator和ListIterator的区别是什么?

  • Iterator可以遍历list和set集合;ListIterator只能用来遍历list集合
  • Iterator只能前向遍历集合;ListIterator可以前向和后向遍历集合
  • ListIterator其实就是实现了前者,并且增加了一些新的功能。

在这里插入图片描述

3.3 数组和集合List之间的转换:

主要通过Arrays.asList以及List.toArray方法来搞定
在这里插入图片描述
注意:

  • 通过Arrays.asList方法将数组转换成List,转换之后不可以使用add/remove等修改集合的相关方法,因为该方法返回的其实是一个Arrays的内部私有的一个类ArrayList,该类继承于Abstractlist,并没有实现这些操作方法,调用将会直接抛出UnsupportOperationException异常。这种转换体现的是一种适配器模式,只是转换接口,本质上还是一个数组。
  • List.toArray方法搞定了集合转换成数组,这里最好传入一个类型一样的数组,大小就是list.size()。因为如果入参分配的数组空间不够大时,toArray方法内部将重新分配内存空间,并返回新数组地址;如果数组元素个数大于实际所需,下标为list.size()及其之后的数组元素将被置为null,其它数组元素保持原值。所以,建议该方法入参数组的大小与集合元素个数保持一致。
    若是直接使用toArray无参方法,此方法返回值只能是Object[ ]类,若强转其它类型数组将出现ClassCastException错误。

3.4 collection和collections的区别

  • Collection,是单列集合的接口,有子接口List和Set
  • Collections,是针对集合操作的工具类,其中包含对集合进行排序和二分查找的方法

Collections常用方法

  • public static void sort (List list):排序,默认情况下是自然排序
  • public static int binarySearch (List<?> list, T key):二分查找
  • public static T max (Collection<?> coll):最大值(最小值类似用法)
  • public static void reverse (List<?> list) :顺序反转
  • public static void shuffle (List<?> list):随机置换

3.5 Array和Arrays的区别

数组类Array

  • Java中最基本的一个存储结构。
  • 提供了动态创建和访问 Java 数组的方法。其中的元素的类型必须相同。
  • 效率高,但容量固定且无法动态改变。
  • 它无法判断其中实际存有多少元素,length只是告诉我们array的容量。

静态类Arrays

  • 此静态类专门用来操作array ,提供搜索、排序、复制等静态方法
  • equals():比较两个array是否相等。array拥有相同元素个数,且所有对应元素两两相等。
  • sort():用来对array进行排序。
  • binarySearch():在排好序的array中寻找元素。
  • Arrays.asList(array):将数组array转化为List

注:类似的还有Executor和Executors有什么区别与联系

4 解决Hash冲突的方法有哪些?

参考链接
hash算法解决冲突的四种方法

5 Java集合中的快速失败(fast-fail)机制:

迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedModCount值,是的话就返回遍历;否则抛出异常,终止遍历。:

猜你喜欢

转载自blog.csdn.net/weixin_42684418/article/details/105364703