HelloCoder HelloCoder
首页
《Java小白求职之路》
《小白学Java》
计算机毕设
  • 一些免费计算机资源
  • 脚手架工具
  • 《从0到1学习Java多线程》
  • 《从0到1搭建服务器》
  • 《可观测和监控》
随笔
关于作者
首页
《Java小白求职之路》
《小白学Java》
计算机毕设
  • 一些免费计算机资源
  • 脚手架工具
  • 《从0到1学习Java多线程》
  • 《从0到1搭建服务器》
  • 《可观测和监控》
随笔
关于作者
  • 《LearnJavaToFindAJob》

    • 导读

    • 【初级】6~12k档

      • Java基础

        • Java基础面试题
        • Java多线程面试题
        • Java集合类面试题
      • JVM

      • 牛客网题库

      • MySQL

      • Linux

      • 计算机网络

      • 操作系统

      • Java框架

    • 【中级】12k-26k档

    • 【高级】26k+档

    • 大厂面试题

    • 求职建议

    • 面经

  • LearnJavaToFindAJob
  • 【初级】6~12k档
  • Java基础
#Java
码农阿雨
2022-06-02
目录

Java集合类面试题

# 1、常见的集合有哪些?

线程安全:

Vector、HashTable、StringBuffer

线程不安全:

HashMap、TreeMap、HashSet、ArrayList、LinkedList

List有序,set无序,map无序,queue消息阻塞队列。

# 2、 Arraylist与 LinkedList 异同

  1. Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向循环链表数据结构;

  2. ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。插入末尾还好,如果是中间,则(add(int index, E element))接近O(n);LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。

  3. LinkedList 不支持高效的随机元素访问,而ArrayList 实现了RandmoAccess 接口,所以有随机访问功能。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。所以ArrayList随机访问快,插入慢;LinkedList随机访问慢,插入快。

  4. ArrayList的 空间浪费 主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱地址以及数据)。

占用空间:

一般来说LinkedList的占用空间更大,因为每个节点要维护指向前后地址的两个节点。

但也不是绝对,如果刚好数据量超过ArrayList默认的临时值时,ArrayList占用的空间也是不小的,因为扩容的原因会浪费将近原来数组一半的容量。

不过,因为ArrayList的数组变量是用transient关键字修饰的,如果集合本身需要做序列化操作的话,ArrayList这部分多余的空间不会被序列化。

transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。

借用网上一张图:

# 3、 HashMap 和 Hashtable 的区别:

相同点:

  • 都是实现来Map接口(Hashtable 还实现了Dictionary 抽象类)。

不同点:

维度 Hashtable HashMap 说明
历史背景 基于Dictionary类(JDK 1.0遗留类) Map接口实现(JDK 1.2引入) HashMap废弃了Dictionary,且将contains方法优化为containsKey和containsValue,避免语义歧义
线程安全 线程安全(方法级synchronized) 线程不安全(非同步) 单线程下HashMap性能更优;多线程需用ConcurrentHashMap替代Hashtable
Null值 不允许 key或value为null 允许 1个null key,多个null value Hashtable插入null直接抛NullPointerException
初始容量 11 16 -
扩容机制 2n + 1(容量翻倍+1) 2n(容量翻倍,左移一位) Hashtable设计为素数容量(或奇数)以优化散列,HashMap使用2的幂次方简化位运算
底层结构 数组 + 链表 数组 + 链表 + 红黑树(JDK 8+) Hashtable链表无限长;HashMap当链表长度≥8且数组长度≥64时,链表转为红黑树(查询O(n) → O(log n))

HashMap可以通过下面的语句保证线程安全:

Map m = Collections.synchronizeMap(hashMap);

或者改用 ConcurrentHashMap

# 4、 HashSet 和 HashMap 区别

HashSet 底层就是基于 HashMap 实现的。只不过HashSet里面的HashMap所有的value都是同一个Object而已,因此HashSet也是非线程安全的。

这个设计是经典的"组合复用"原则的体现。

维度 HashMap HashSet
实现的接口 实现 Map 接口 实现 Set 接口
存储内容 键值对 (Key-Value) 仅对象 (只有Key,没有Value)
添加元素方法 调用 put(key, value) 方法 调用 add(e) 方法
HashCode 计算 根据**键(Key)**计算HashCode 根据成员对象计算HashCode。两个不同对象可能计算相同HashCode,需配合equals()判重
判重机制 键唯一:通过 hashCode() 和 equals() 判断Key是否重复 元素唯一:通过 hashCode() 和 equals() 判断元素是否已存在
速度/性能 相对较快:直接根据Key定位Value(O(1)理想复杂度) 相对较慢:底层实际使用HashMap,但只使用Key,Value为常量占位,逻辑相同但封装了一层
底层原理 底层是数组+链表+红黑树 底层实际上是HashMap:存值时将元素作为HashMap的Key,Value统一为一个固定的Object常量

# 5、ConcurrentHashMap和Hashtable的区别

JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类,都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;

② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

# 6、Set和List的区别

维度 Set List
数据顺序 无序:元素存取顺序不一定一致(部分实现类如TreeSet可排序,但非插入顺序) 有序:元素存取顺序一致(按插入顺序排列)
数据重复 不允许重复:元素唯一,最多包含一个null 允许重复:可包含多个null元素
底层结构 基于 HashMap 实现(元素作为Key) 基于 数组(如ArrayList)或 双向链表(如LinkedList)实现
性能特点 删除/插入快:无需移动元素;查询慢:需遍历(HashSet O(1) 实际是特例,指contains依靠hash定位快) 查询快(数组实现):下标直接访问 O(1);删除/插入慢:需移动元素(链表实现在头尾操作快,但中间操作慢)
常用实现类 HashSet(哈希表,最快)、TreeSet(红黑树,可排序)、LinkedHashSet(链表维护插入顺序) ArrayList(数组,查询快)、LinkedList(链表,增删快,首尾操作)、Vector(线程安全,已过时)
Null值存储 最多一个null 可多个null

# 7、 ArrayList 与 Vector 的区别:

共同点: 都实现了List接口,都是有序的集合,我们可以按位置的索引号取出元素,其中数据都是可以重复的,这是与hashSet最不同的,hashSet不可以按照索引号去检索其中的元素,也不允许有重复的元素。

维度 ArrayList Vector
线程安全 线程不安全(非同步) 线程安全(方法级synchronized)
适用场景 单线程环境:效率更高 多线程环境:无需额外同步代码,但性能较低(现代并发推荐CopyOnWriteArrayList)
扩容机制 扩容为原来的 1.5 倍(1.5倍扩容是时间和空间的折中:扩容太频繁影响性能,扩容太大浪费内存) 扩容为原来的 2 倍(可自定义增长量)
初始容量 默认 10(懒加载:JDK 8+ 首次添加时创建数组) 默认 10(构造时立即创建数组)
容量设置 可设置初始容量,但无法设置扩容增量 可设置初始容量和扩容增量(capacityIncrement)
遗留性 JDK 1.2 引入,非遗留类 JDK 1.0 遗留类,官方推荐优先使用 ArrayList
性能 较高:无同步开销,适合大多数场景 较低:同步开销大,方法级锁粒度粗
迭代器 fail-fast:迭代过程中检测到修改会抛出ConcurrentModificationException fail-fast(同ArrayList),但elements()返回的旧式枚举非fail-fast

# 8、HaspMap与TreeMap的区别:

维度 HashMap TreeMap
底层结构 哈希表(数组+链表+红黑树,JDK 8+) 红黑树(自平衡的二叉查找树)
元素顺序 无序:不保证元素的顺序(甚至顺序可能随时间变化) 有序:元素按照自然顺序或自定义比较器排序
时间复杂度 O(1):理想情况下,基于哈希的直接定位 O(log n):基于红黑树的查找、插入、删除
null 值处理 允许一个 null key 和多个 null value 不允许 null key(会抛 NullPointerException),但允许 null value(需注意比较器处理)
实现接口 实现 Map 接口 实现 SortedMap 和 NavigableMap 接口
依赖方法 依赖 hashCode() 和 equals() 方法 依赖 Comparable 或 Comparator 接口
适用场景 插入、删除、查找操作频繁,不关心顺序 需要有序遍历(如范围查询、排序输出)
性能特点 插入/删除/查找快(哈希直接定位) 插入/删除/查找较慢(需维护树平衡),但有序遍历快

# 9、Collection和Collections的区别

Collection是单列集合的顶层接口,Map是双列集合的顶层接口

Collections是一个集合的工具类,提供了排序、查找等操作集合的一些常用方法。

# 10、 Collection框架中实现比较要怎么做?

第一种,实体类实现Comparable<T>接口,并实现 compareTo(T t) 方法,我们称为内部比较器。

第二种,创建一个外部比较器,这个外部比较器要实现Comparator接口的 compare(T t1, T t2)方法。

comparable 和 comparator的区别:

  • comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
  • comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序

一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌手名排序,第二种代表我们只能使用两个参数版的Collections.sort().

例子:

public class ComparatorTest {
    public static void main(String[] args) {
        Student xiaoming = new Student("xiaoming", 20);
        Student xiaohong = new Student("xiaohong", 21);
        Student xiaogang = new Student("xiaogang", 19);

        List<Student> list = new ArrayList<Student>();
        list.add(xiaohong);
        list.add(xiaoming);
        list.add(xiaogang);
        //因为Student类已经重写了compareTo方法
        System.out.println("已重写后的list --->>>" + list);
		//自定义排序
        Collections.sort(list, new Comparator<Student>() {
            @Override
            //升序
            public int compare(Student o1, Student o2) {
                return o1.getAge().compareTo(o2.getAge());
            }
        });
        //自定义排序
        System.out.println("自定义排序list --->>>" + list);

    }
}

 class ComparatorTest2 implements Comparator<Student> {

    public static void main(String[] args) {
        Student xiaoming = new Student("xiaoming", 20);
        Student xiaohong = new Student("xiaohong", 21);
        Student xiaogang = new Student("xiaogang", 19);
        List<Student> list = new ArrayList<>();
        list.add(xiaohong);
        list.add(xiaoming);
        list.add(xiaogang);
        System.out.println("已重写后的list --->>>" + list);

        ComparatorTest2 comparatorTest = new ComparatorTest2();
        System.out.println("自定义比较--->>>"+comparatorTest.compare(xiaoming, xiaohong));

    }

    //重写compare方法,用于单独比较
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getAge().compareTo(o2.getAge());
    }


}

@Data
class Student implements Comparable<Student> {
    String name;
    Integer age;

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Student o2) {
        //降序
        return this.getAge().compareTo(o2.getAge());
    }
}

第一个main输出:

已重写后的list --->>>[Student(name=xiaohong, age=21), Student(name=xiaoming, age=20), Student(name=xiaogang, age=19)]
自定义排序list --->>>[Student(name=xiaogang, age=19), Student(name=xiaoming, age=20), Student(name=xiaohong, age=21)]

第二个main输出:

已重写后的list --->>>[Student(name=xiaohong, age=21), Student(name=xiaoming, age=20), Student(name=xiaogang, age=19)]
自定义比较--->>>-1

# 11、ConcurrentHashMap分段锁

jdk1.7中:

ConcurrentHashMap 是由 Segment 数组结构 + HashEntry 数组结构 + 链表 组成。Segment 是一种可重入锁 ReentrantLock,在 ConcurrentHashMap 里扮演锁的角色,HashEntry 则用于存储键值对数据。

Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。

jdk1.8中:

放弃了Segment,直接用 Node数组+链表+红黑树 的数据结构来实现,并发控制使用Synchronized + CAS来操作,整个看起来就像是优化过且线程安全的HashMap。

# 12、遍历一个 List 有哪些不同的方式?

  1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
  2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
  3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
System.out.println("-----------forEach遍历-------------");
list.parallelStream().forEach(k -> {
    System.out.println(k);
});
System.out.println("-----------for遍历-------------");
for (Student student : list) {
    System.out.println(student);
}
System.out.println("-----------Iterator遍历-------------");
Iterator<Student> iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

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

  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法。
// list to array
List<String> list = new ArrayList<String>();
list.add("HaC");
list.add("HelloCoder");
Object[] str = list.toArray();

// array to list
String[] array = new String[]{"HaC", "HelloCoder"};
List<String> mylist = Arrays.asList(array);

# 14、hashCode()与equals()

  1. 如果两个对象相等,则hashcode一定也是相同的
  2. 两个对象相等,对两个equals方法返回true
  3. 两个对象有相同的hashcode值,它们也不一定是相等的(哈希冲突)
  4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
  5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

# 15、HashMap原理、哈希冲突的解决

HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。

当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞、哈希冲突)

为什么1.8中引入红黑树?

当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);

JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间

简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:

  1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;

  2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;

  3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

HashMap什么时候进行扩容?

当HashMap中的元素个数超过数组大小 * loadFactor 时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16 * 0.75=12的时候,就把数组的大小扩展为2 * 16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设数组的大小能够有效的提高HashMap的性能。

比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面已经说过,即使是1000,HashMap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1024 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,避免了resize的问题。

可以这样说:

1.添加元素的时候会检查容器当前元素个数。当HashMap的容量值超过了临界值(默认16 * 0.75=12)时扩容。

2.HashMap将会重新扩容到下一个2的指数幂(16->32->64)。

3.调用resize方法,定义长度为新长度(32)的数组,然后对原数组数据进行再Hash。这个过程是一个性能损耗点。

阅读全文
×

(为防止恶意爬虫)
扫码或搜索:HelloCoder
发送:290992
即可永久解锁本站全部文章

解锁
#Java
上次更新: 2026-03-28 17:00:16
最近更新
01
MySQL支持的锁有哪些
03-28
02
HTTP 是不保存状态的协议, 如何保存用户状态
03-28
03
用户态和内核态的区别
03-28
更多文章>
Theme by Vdoing | Copyright © 2020-2026 码农阿雨
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式