1.Java 中常用的容器有哪些?
Collection
-
Set
-
TreeSet:基于红黑树实现,支持有序性操作,例如:根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
-
HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
-
LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
-
List
-
ArrayList:基于动态数组实现,支持随机访问。
-
LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
Map
-
TreeMap:基于红黑树实现。
-
HashMap:基于哈希表实现。
-
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 的扩容机制是在元素数量超过当前容量时进行自动扩容,以保证数组的容量足够存储更多的元素。具体的扩容机制如下:
-
初始化容量:在创建 ArrayList 对象时,会分配一个初始容量(默认为 10)的数组用于存储元素。
-
添加元素时的扩容:
-
当添加元素时,如果当前元素数量已经达到数组的容量上限,则需要进行扩容。
-
扩容操作会创建一个新的数组,其大小为当前容量的 1.5 倍(即将当前容量乘以 1.5),然后将原数组中的元素复制到新数组中。
-
然后将新元素添加到新数组的末尾。
-
最后,将 ArrayList 的底层数组引用指向新数组,并更新当前容量。
-
-
System.arraycopy() 方法:在进行数组复制时,ArrayList 使用了 System.arraycopy() 方法来提高性能。该方法可以直接将源数组中的元素复制到目标数组中,比使用循环逐个复制元素的效率更高。
通过使用自动扩容,ArrayList 在添加元素时能够灵活地动态增加存储空间,避免频繁的数组重新分配和复制操作。但在实际使用中,如果预先知道 ArrayList 的大致容量,也可以使用带初始容量参数的构造方法来避免多次扩容,以提高性能。例如,可以使用 ArrayList(int initialCapacity)
构造方法指定初始容量。
5.Array 和 ArrayList 有何区别?什么时候更适合用 Array?
-
Array 可以容纳基本类型和对象,而 ArrayList 只能容纳对象
-
Array 是指定大小固定的,而 ArrayList 大小是动态增长和缩小
-
如果需要固定大小的集合,且元素类型是已知的且相同的,可以使用 Array。
-
如果需要动态大小的集合,或者元素类型可能变化,或者需要频繁地插入和删除元素,可以使用 ArrayList。
Array(数组)和 ArrayList(动态数组)是 Java 中用于存储多个元素的数据结构,它们有以下区别:
-
固定大小 vs 动态大小:
-
Array 的大小在创建时就确定了,并且无法改变。一旦数组被创建,它的大小就是固定的。
-
ArrayList 的大小可以根据需要动态增长和缩小。它会自动进行扩容和收缩,以适应元素的添加和移除操作。
-
-
存储类型:
-
Array 可以存储基本数据类型(如 int、char 等)和引用类型(如对象的引用)。
-
ArrayList 只能存储引用类型(对象的引用),不能存储基本数据类型。如果需要存储基本数据类型,需要使用其对应的包装类(如 Integer、Character)或者使用 Java 8 引入的自动装箱和拆箱功能。
-
-
编译时类型检查:
-
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 方法的执行过程?
-
首先,根据要插入的 key 计算出它的 hash 值。
-
然后,使用 hash 值计算出该元素在数组中的索引位置(即桶的位置)。
-
接着,检查该索引位置上是否已经存在元素。如果不存在,直接将新的键值对作为一个 Entry 对象存放在该位置。
-
如果索引位置上已经存在元素,则需要进行进一步的处理。
-
如果存在的元素的 key 和要插入的 key 相等(使用 equals 方法进行比较),那么直接替换该元素的值为新的值。
-
如果存在元素的 key 和要插入的 key 不相等,表示发生了 hash 冲突,此时需要进行链表或红黑树的操作。
-
如果当前桶中的元素是链表,遍历链表找到最后一个节点,然后将新的键值对作为一个节点添加到链表的末尾。
-
如果当前桶中的元素是红黑树,调用红黑树的插入操作,将新的键值对插入到树中。
-
-
-
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 值。具体的执行过程如下:
-
首先,根据 key 的哈希值计算其在数组中的索引位置。通过
(hash & (table.length - 1))
计算元素在数组中的索引位置。其中,hash
是 key 的哈希值,&
是位与运算,table.length
是数组的长度。 -
通过计算出的索引位置获取对应的桶(bucket)。桶是一组链表或红黑树结构,存储着相同索引位置的键值对。如果桶为 null,则表示当前位置上没有键值对,直接返回 null。
-
遍历桶中的链表或红黑树,查找给定 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 进行扩容。具体的执行过程如下:
-
创建一个新的数组,其长度为原数组长度的两倍。新数组的长度为原数组长度左移一位(即原数组长度乘以2)。
-
遍历原数组中的每个桶,将其中的键值对重新分配到新数组中的对应位置。
-
对于每个桶,遍历其中的链表或红黑树,将其中的键值对重新计算哈希值,并放置到新数组的对应位置。
-
重分配之后,所有的键值对已经重新分布到新数组中的合适位置。
需要注意的是,在进行 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 怎么使用?有什么特点?
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象:
-
使用方法 iterator() 要求容器返回一个 Iterator。第一次调用 Iterator 的 next() 方法时,它返回序列的第一个元素。注意:iterator() 方法是 java.lang.Iterable 接口,被 Collection 继承。
-
使用 next() 获得序列中的下一个元素。
-
使用 hasNext() 检查序列中是否还有元素。
-
使用 remove() 将迭代器新返回的元素删除。
在Java中,Iterator 是用于遍历集合(Collection)的接口。通过Iterator,我们可以依次访问集合中的元素,而不需要了解集合的具体实现细节。
使用 Iterator 遍历集合的一般步骤如下:
-
获取集合的迭代器对象:通过调用集合的 iterator() 方法获取到一个 Iterator 对象。例如:
List<String> list = new ArrayList<>(); // 添加元素到列表 Iterator<String> iterator = list.iterator();
-
判断集合中是否还有下一个元素:通过调用 Iterator 的 hasNext() 方法判断集合中是否还有未遍历的元素。如果有,返回 true;否则返回 false。例如:
if (iterator.hasNext()) { // 有下一个元素 } else { // 没有下一个元素,遍历结束 }
-
获取集合中的下一个元素:通过调用 Iterator 的 next() 方法获取集合中的下一个元素。例如:
String element = iterator.next();
-
遍历集合的操作:对获取到的元素进行操作,例如输出、处理等。
下面是一个完整的示例代码,演示了如何使用 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
方法,以及它们的作用:
-
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]
-
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
-
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]
-
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]
-
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 异常。