java集合面试题

1.Java 中常用的容器有哪些?

Collection

  • Set

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

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

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

  • List

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

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

Map

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

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

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

总结:

  • List (对付顺序的好帮⼿): 存储的元素是有序的、可重复的。

  • Set (注重独⼀⽆⼆的性质): 存储的元素是⽆序的、不可重复的。

  • Map (⽤ Key 来搜索的专家): 使⽤键值对(kye-value)存储,类似于数学上的函数y=f(x),“x”代表 key,”y”代表 value

2.ArrayList 和 LinkedList 的区别?

  • ArrayList:基于数组实现,支持快速随机访问元素,但插入和删除操作相对较慢。

  • LinkedList:基于链表实现,支持高效地在任意位置插入和删除元素,但访问元素需要遍历链表。

注意:ArrayList 的增删未必就是比 LinkedList 要慢,比如增删头尾

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

  • ArrayList 实现了 RandomAccess 接口,是因为 ArrayList 内部使用数组实现,通过索引可以快速访问元素,具有较好的随机访问性能。

  • LinkedList 没有实现 RandomAccess 接口,是因为 LinkedList 内部使用链表实现,需要通过遍历来访问元素,无法直接通过索引进行快速访问。

  • ArrayList 一般采用 for 循环遍历,而 LinkedList 一般采用迭代器遍历。ArrayList 用 for 循环遍历比 iterator 迭代器遍历快,LinkedList 用 iterator 迭代器遍历比 for 循环遍历快。

RandomAccess 接口是 Java 集合框架中的一个接口,它是一个标记接口,即不包含任何方法定义。该接口标识着实现了它的类具有良好的随机访问性能。

它的存在是为了让调用方可以根据接口类型来判断容器的访问性能。如果一个类实现了 RandomAccess 接口,就表示该类支持高效的随机访问操作;反之,如果一个类没有实现 RandomAccess 接口,则可能在随机访问操作上性能较差。

在集合框架中,例如 ArrayList 实现了 RandomAccess 接口,而 LinkedList 没有实现。这意味着在对 ArrayList 进行随机访问时,可以通过索引直接访问元素,具有较好的性能;而在对 LinkedList 进行随机访问时,需要进行遍历,性能会相对较差。

通过判断一个容器是否实现了 RandomAccess 接口,可以在编写代码时选择更适合的访问方式,从而提升程序的性能。

4.ArrayList 的扩容机制?

ArrayList 的扩容机制是在元素数量超过当前容量时进行自动扩容,以保证数组的容量足够存储更多的元素。具体的扩容机制如下:

  1. 初始化容量:在创建 ArrayList 对象时,会分配一个初始容量(默认为 10)的数组用于存储元素。

  2. 添加元素时的扩容:

    • 当添加元素时,如果当前元素数量已经达到数组的容量上限,则需要进行扩容。

    • 扩容操作会创建一个新的数组,其大小为当前容量的 1.5 倍(即将当前容量乘以 1.5),然后将原数组中的元素复制到新数组中。

    • 然后将新元素添加到新数组的末尾。

    • 最后,将 ArrayList 的底层数组引用指向新数组,并更新当前容量。

  3. System.arraycopy() 方法:在进行数组复制时,ArrayList 使用了 System.arraycopy() 方法来提高性能。该方法可以直接将源数组中的元素复制到目标数组中,比使用循环逐个复制元素的效率更高。

通过使用自动扩容,ArrayList 在添加元素时能够灵活地动态增加存储空间,避免频繁的数组重新分配和复制操作。但在实际使用中,如果预先知道 ArrayList 的大致容量,也可以使用带初始容量参数的构造方法来避免多次扩容,以提高性能。例如,可以使用 ArrayList(int initialCapacity) 构造方法指定初始容量。

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

  • Array 可以容纳基本类型和对象,而 ArrayList 只能容纳对象

  • Array 是指定大小固定的,而 ArrayList 大小是动态增长和缩小

  • 如果需要固定大小的集合,且元素类型是已知的且相同的,可以使用 Array。

  • 如果需要动态大小的集合,或者元素类型可能变化,或者需要频繁地插入和删除元素,可以使用 ArrayList。

Array(数组)和 ArrayList(动态数组)是 Java 中用于存储多个元素的数据结构,它们有以下区别:

  1. 固定大小 vs 动态大小:

    • Array 的大小在创建时就确定了,并且无法改变。一旦数组被创建,它的大小就是固定的。

    • ArrayList 的大小可以根据需要动态增长和缩小。它会自动进行扩容和收缩,以适应元素的添加和移除操作。

  2. 存储类型:

    • Array 可以存储基本数据类型(如 int、char 等)和引用类型(如对象的引用)。

    • ArrayList 只能存储引用类型(对象的引用),不能存储基本数据类型。如果需要存储基本数据类型,需要使用其对应的包装类(如 Integer、Character)或者使用 Java 8 引入的自动装箱和拆箱功能。

  3. 编译时类型检查:

    • Array 具有编译时类型检查。在声明和创建数组时,必须指定元素的类型,并且只能存储相同类型的元素。

    • ArrayList 在声明和创建时不需要指定元素的类型,可以存储不同类型的元素。但是,在编译时无法检查元素的类型安全性,需要在运行时进行动态类型检查。

在选择使用 Array 还是 ArrayList 时,可以根据具体情况进行考虑:

  • 如果需要固定大小的集合,且元素类型是已知的且相同的,可以使用 Array。例如,存储一个固定长度的整数数组,或者存储一个已知大小的对象数组。

  • 如果需要动态大小的集合,或者元素类型可能变化,或者需要频繁地插入和删除元素,可以使用 ArrayList。例如,需要根据运行时的条件来添加和移除元素的情况下,ArrayList 更加方便。

总之,Array 适合在已知大小且元素类型相同的情况下使用,而 ArrayList 则更适用于需要动态调整大小以及处理灵活的元素类型的场景。

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

JDK1.7:Entry数组 + 链表

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

7.HashMap 的 put 方法的执行过程?

  1. 首先,根据要插入的 key 计算出它的 hash 值。

  2. 然后,使用 hash 值计算出该元素在数组中的索引位置(即桶的位置)。

  3. 接着,检查该索引位置上是否已经存在元素。如果不存在,直接将新的键值对作为一个 Entry 对象存放在该位置。

  4. 如果索引位置上已经存在元素,则需要进行进一步的处理。

    • 如果存在的元素的 key 和要插入的 key 相等(使用 equals 方法进行比较),那么直接替换该元素的值为新的值。

    • 如果存在元素的 key 和要插入的 key 不相等,表示发生了 hash 冲突,此时需要进行链表或红黑树的操作。

      • 如果当前桶中的元素是链表,遍历链表找到最后一个节点,然后将新的键值对作为一个节点添加到链表的末尾。

      • 如果当前桶中的元素是红黑树,调用红黑树的插入操作,将新的键值对插入到树中。

  5. JDK1.7 底层采用数组+链表,插入时采用头插法。JDK1.8,底层采用数组 + 链表 / 红黑树,并且把头插法改成了尾插法

总结起来,put 方法的执行过程包括计算 hash 值、确定元素在数组中的位置,处理冲突,插入节点,以及可能的扩容操作。

当发生哈希冲突时,HashMap 会使用链表或红黑树来解决冲突,具体的数据结构选择是基于链表长度的阈值判断,当链表长度超过8时会将链表转换为红黑树。下面是一个示例代码:

import java.util.HashMap;
​
public class HashMapExample {
    public static void main(String[] args) {
        HashMap<Integer, String> hashMap = new HashMap<>();
​
        // 添加键值对
        hashMap.put(1, "Apple");
        hashMap.put(2, "Banana");
​
        // 发生哈希冲突,插入相同索引位置的元素
        hashMap.put(9, "Grape");
        hashMap.put(10, "Orange");
​
        // 查看HashMap的内部结构
        System.out.println(hashMap);
    }
}

输出结果:

{1=Apple, 2=Banana, 9=Grape, 10=Orange}

在这个例子中,我们向 HashMap 中插入了四个键值对。其中,键值对 (9, "Grape")(10, "Orange") 的哈希值发生了冲突,它们将被映射到数组中的相同索引位置。

当发生冲突时,HashMap 会将这两个键值对插入到链表中,即原来的槽位上会形成一个链表结构。因此,输出的结果中可以看到键 9 和键 10 出现在同一个数组位置,并且按照插入的顺序保留在链表中。

需要注意的是,当链表长度超过8时,HashMap 会将链表转换为红黑树以提高查找效率。这里的示例中,链表长度未超过阈值,因此仍然是链表结构。

8.HashMap 的 get 方法的执行过程?

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

HashMap 的 get 方法用于获取给定 key 对应的 value 值。具体的执行过程如下:

  1. 首先,根据 key 的哈希值计算其在数组中的索引位置。通过 (hash & (table.length - 1)) 计算元素在数组中的索引位置。其中,hash 是 key 的哈希值,& 是位与运算,table.length 是数组的长度。

  2. 通过计算出的索引位置获取对应的桶(bucket)。桶是一组链表或红黑树结构,存储着相同索引位置的键值对。如果桶为 null,则表示当前位置上没有键值对,直接返回 null。

  3. 遍历桶中的链表或红黑树,查找给定 key 对应的键值对。具体而言:

    • 如果桶中的数据结构为链表,遍历链表并比较每个节点的键和给定的 key 是否相等。若相等,则返回对应的 value 值;若链表遍历完毕仍未找到相应的键值对,则返回 null。

    • 如果桶中的数据结构为红黑树,通过红黑树的查找操作快速定位到对应的节点,并判断其键是否和给定的 key 相等。若相等,则返回对应的 value 值;否则返回 null。

需要注意的是,在进行查找时,HashMap 会使用键的 equals 方法来比较键是否相等。因此,在使用自定义类型作为键时,需要重写 equals 方法以确保正确的查找结果。

下面是一个简单的 Java 代码示例,演示了如何使用 HashMap 的 get 方法获取给定 key 对应的 value 值:

import java.util.HashMap;
​
public class HashMapExample {
    public static void main(String[] args) {
        // 创建一个新的HashMap
        HashMap<String, Integer> hashMap = new HashMap<>();
​
        // 添加键值对
        hashMap.put("apple", 1);
        hashMap.put("banana", 2);
        hashMap.put("cherry", 3);
​
        // 使用get方法获取对应的value值
        Integer appleValue = hashMap.get("apple");
        Integer orangeValue = hashMap.get("orange");
​
        // 输出结果
        System.out.println("Value of apple: " + appleValue);
        System.out.println("Value of orange: " + orangeValue);
    }
}

输出结果:

Value of apple: 1
Value of orange: null

在这个示例中,我们新建了一个 HashMap 并向其中添加了三个键值对。接着,使用 get 方法分别获取键 "apple" 和 "orange" 对应的 value 值。由于键 "apple" 存在于 HashMap 中,因此返回对应的 value 值为 1;而键 "orange" 不存在,因此返回 null。

9.HashMap 的 resize 方法的执行过程?

HashMap 的 resize 方法用于在容量不足时,对 HashMap 进行扩容。具体的执行过程如下:

  1. 创建一个新的数组,其长度为原数组长度的两倍。新数组的长度为原数组长度左移一位(即原数组长度乘以2)。

  2. 遍历原数组中的每个桶,将其中的键值对重新分配到新数组中的对应位置。

  3. 对于每个桶,遍历其中的链表或红黑树,将其中的键值对重新计算哈希值,并放置到新数组的对应位置。

  4. 重分配之后,所有的键值对已经重新分布到新数组中的合适位置。

需要注意的是,在进行 resize 操作时,旧数组中的元素并没有被直接复制到新数组中,而是通过重新计算哈希值来确定它们在新数组中的位置。这是因为新数组的长度发生了变化,直接复制可能会导致哈希冲突。

下面是一个简单的 Java 代码示例,演示了如何使用 HashMap 的 resize 方法进行扩容:

import java.lang.reflect.Field;
import java.util.HashMap;
​
public class HashMapExample {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 创建一个新的HashMap
        HashMap<Integer, String> hashMap = new HashMap<>();
​
        // 添加键值对,使其超出默认初始容量(16)
        for (int i = 0; i < 17; i++) {
            hashMap.put(i, "Value" + i);
        }
​
        // 获取HashMap的容量大小
        int capacity = getHashMapCapacity(hashMap);
​
        // 输出结果
        System.out.println("HashMap的容量:" + capacity);
    }
​
    private static int getHashMapCapacity(HashMap<?, ?> hashMap) throws NoSuchFieldException, IllegalAccessException {
        Field tableField = HashMap.class.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] table = (Object[]) tableField.get(hashMap);
        return table.length;
    }
}

输出结果:

HashMap的容量:32

在这个示例中,我们向一个初始容量为 16 的 HashMap 中添加了 17 个键值对。由于超出了默认的初始容量,HashMap 会执行扩容操作。

通过反射获取 HashMap 内部的 table 数组,并获取其长度,即为扩容后的容量大小。结果为 32,即表明 HashMap 扩容成功。

10.JDK1.8之后,HashMap头插法改为尾插法?

  • 在 JDK 1.8 中(包括之后的版本),HashMap 并没有完全改为尾插法,

  • 而是根据链表是否已经转换为红黑树来决定具体的插入方式。

  • 对于链表部分,依然使用的是头插法;

  • 对于红黑树部分,使用的是尾插法。

  • 这样的设计可以在保证插入效率的同时,兼顾了链表和红黑树的特性,提高了 HashMap 的性能和效率。

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

HashMap这样做有两点原因

  • 提升计算效率,更快算出元素的位置。对于机器而言,位运算永远比取余运算快得多。

  • 减少哈希碰撞,使得元素分布均匀

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

  • 在 HashMap 中,get 方法的返回值确实无法准确判断一个键是否存在于 Map 中。

  • 因为 HashMap 允许键和值都为 null,当 get 方法返回 null 时,并不能确定是该键不存在于 Map 中,还是该键对应的值为 null。

13.HashSet 的实现原理?

HashSet 是基于 HashMap 实现的,它使用了 HashMap 来存储元素。在 HashSet 内部,所有元素都存储在一个 HashMap 中,而 HashSet 的元素实际上就是这个 HashMap 的键值对中的键

在 HashSet 中添加元素时,HashSet 会将这些元素作为 HashMap 的键,而将一个固定的 Object 对象(称之为“虚拟值”)作为 HashMap 的值。因为 HashMap 不允许键重复,所以当我们向 HashSet 中添加重复的元素时,其实是在尝试向 HashMap 中添加已经存在的键,这个操作会被 HashMap 忽略掉。

14.LinkedHashMap 的实现原理?

LinkedHashMap 是基于 HashMap 实现的,但是多了header、before、after三个属性,有了这三个属性就能组成一个双向链表,来实现按插入顺序或访问顺序排序,其迭代顺序默认为插入顺序。

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

迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象: 

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

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

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

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

在Java中,Iterator 是用于遍历集合(Collection)的接口。通过Iterator,我们可以依次访问集合中的元素,而不需要了解集合的具体实现细节。

使用 Iterator 遍历集合的一般步骤如下:

  1. 获取集合的迭代器对象:通过调用集合的 iterator() 方法获取到一个 Iterator 对象。例如:

List<String> list = new ArrayList<>();
// 添加元素到列表
Iterator<String> iterator = list.iterator();
  1. 判断集合中是否还有下一个元素:通过调用 Iterator 的 hasNext() 方法判断集合中是否还有未遍历的元素。如果有,返回 true;否则返回 false。例如:

if (iterator.hasNext()) {
    // 有下一个元素
} else {
    // 没有下一个元素,遍历结束
}
  1. 获取集合中的下一个元素:通过调用 Iterator 的 next() 方法获取集合中的下一个元素。例如:

String element = iterator.next();
  1. 遍历集合的操作:对获取到的元素进行操作,例如输出、处理等。

下面是一个完整的示例代码,演示了如何使用 Iterator 遍历 ArrayList 集合并输出每个元素:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
​
public class IteratorExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Orange");
​
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);
        }
    }
}
  • 对于每个新创建的迭代器对象,它最开始的位置都是指向集合的第一个元素之前的位置,而不是第一个元素的位置。因此,调用 next() 方法后迭代器的位置才会移动到第一个元素位置,返回第一个元素的值。

16.Collection 和 Collections 有什么区别?

  • Collection 是一个接口,表示一组对象的集合,定义了基本的集合操作和方法。

  • Collections 是一个工具类,提供了对集合的各种操作,其中的方法都是静态方法。

Collections 是 Java 集合框架中的一个工具类,提供了许多静态方法,用于操作集合。

下面是一些常用的 Collections 方法,以及它们的作用:

  1. sort(List<T> list):对指定的列表进行排序。该方法会使用 Comparable 接口的 compareTo() 方法进行比较,因此待排序的元素类型必须实现 Comparable 接口。

List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9));
Collections.sort(list);
System.out.println(list); // 输出 [1, 1, 3, 4, 5, 9]
  1. binarySearch(List<? extends Comparable<? super T>> list, T key):在指定的有序列表中查找指定的值,并返回其下标。如果列表中不存在该元素,则返回一个负数值,表示该元素应该插入的位置。该方法同样需要使用 Comparable 接口进行比较,并且列表中的元素必须实现 Comparable 接口。

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
int index = Collections.binarySearch(list, 5);
System.out.println(index); // 输出 4
  1. reverse(List<?> list):将指定列表中的元素进行翻转。

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Collections.reverse(list);
System.out.println(list); // 输出 [5, 4, 3, 2, 1]
  1. shuffle(List<?> list):随机打乱指定列表中的元素。

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Collections.shuffle(list);
System.out.println(list); // 输出随机的顺序,例如 [4, 2, 3, 1, 5]
  1. fill(List<? super T> list, T obj):用指定的值填充指定列表。

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d", "e"));
Collections.fill(list, "x");
System.out.println(list); // 输出 [x, x, x, x, x]

还有许多其他的方法,可以根据具体需求灵活使用。

需要注意的是,Collections 类中的大多数方法都需要传入一个集合对象作为参数,并且该集合对象不能为 null,否则会抛出 NullPointerException 异常。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿人啊兴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值