Java集合
Java 集合
1、说说常见的集合有哪些?
常见的集合主要包括以下几种:
- List(列表): 允许重复元素,有序集合。常见的实现类有 ArrayList、LinkedList、Vector。
- Set(集合): 不允许重复元素,无序集合。常见的实现类有 HashSet、TreeSet、LinkedHashSet。
- Map(映射): 键值对的集合,每个键对应一个值,键不允许重复。常见的实现类有 HashMap、TreeMap、LinkedHashMap。
- Queue(队列): 先进先出(FIFO)的数据结构。常见的实现类有 PriorityQueue、LinkedList(作为队列使用)。
- Deque(双端队列): 可以在两端进行插入和删除操作的队列。常见的实现类有 ArrayDeque、LinkedList(作为双端队列使用)。
- Stack(栈): 后进先出(LIFO)的数据结构。常见的实现类有 Stack。
2、哪些集合类可对元素的随机访问?
在 Java 中,可以对元素进行随机访问的集合类主要是实现了 RandomAccess 接口的类。这个接口并没有定义任何方法,只是一个标记接口,用于表示该集合支持高效的随机访问操作。
以下是一些实现了 RandomAccess 接口的集合类:
ArrayList:基于数组实现,支持通过索引直接访问元素。Vector:与ArrayList类似,但是是线程安全的,通过同步方法实现线程安全。CopyOnWriteArrayList:也是线程安全的,但是对写操作进行了优化,适合读多写少的场景。Arrays.asList()返回的List:如果底层数组实现了RandomAccess接口,则返回的List也会支持随机访问。
需要注意的是,并非所有的集合类都支持随机访问。比如,LinkedList 是基于链表实现的,访问元素时需要遍历链表,效率比较低。
3、Comparable 和 Comparator 接口的区别?
Comparable 和 Comparator 接口都用于比较对象,但它们之间有几个重要的区别:
- Comparable 接口:
Comparable接口是在要进行比较的类内部实现的,通常用于对类的自然顺序进行排序。- 类实现
Comparable接口后,需要重写compareTo()方法来定义对象之间的比较规则。 - 当调用排序方法(如
Collections.sort()或Arrays.sort())时,会自动调用对象的compareTo()方法进行排序。
示例代码:
public class Person implements Comparable<Person> {
private String name;
private int age;
// 构造方法和其他代码省略...
@Override
public int compareTo(Person other) {
return this.age - other.age; // 按年龄排序
}
}
- Comparator 接口:
Comparator接口是一个单独的比较器,可以用于对任意类的对象进行比较,不需要修改被比较的类本身。- 类实现
Comparator接口后,需要重写compare()方法来定义对象之间的比较规则。 - 当需要对某个类的对象进行多种不同的排序时,可以使用不同的
Comparator实现来实现不同的比较规则。
示例代码:
public class PersonComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().compareTo(p2.getName()); // 按姓名排序
}
}
使用示例:
List<Person> people = new ArrayList<>();
// 添加对象到列表...
// 使用 Comparable 接口进行排序(按年龄)
Collections.sort(people);
// 使用 Comparator 接口进行排序(按姓名)
Collections.sort(people, new PersonComparator());
总的来说,Comparable 适用于对类的自然顺序进行排序,而 Comparator 则适用于对类的不同属性或多种排序规则进行比较。
4、Collection 和 Collections 的区别?
Collection 和 Collections 是 Java 中的两个不同的概念:
- Collection:
Collection是 Java 中表示一组对象的接口,它是集合框架的基础接口之一。Collection表示的是一组对象的集合,它可以包含不同类型的元素,如列表(List)、集(Set)、队列(Queue)等。Collection接口提供了对集合元素的基本操作,如添加、删除、遍历等。
示例代码:
Collection<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
System.out.println(names); // 输出 [Alice, Bob, Charlie]
- Collections:
Collections是 Java 中的一个工具类,位于java.util包中。Collections类提供了一系列静态方法,用于对集合进行操作,如排序、查找、替换等。- 这些静态方法通常用于对集合进行常见的操作,简化了集合操作的代码实现。
示例代码:
List<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(3);
numbers.add(7);
Collections.sort(numbers); // 对集合进行排序
System.out.println(numbers); // 输出 [3, 5, 7]
总的来说,Collection 是表示集合的接口,而 Collections 是一个工具类,提供了对集合进行操作的静态方法。使用 Collection 可以操作集合对象本身,而使用 Collections 可以对集合进行各种操作,如排序、查找等。
5、Enumeration 和 Iterator 接口的区别?
Enumeration 和 Iterator 接口都是用于遍历集合(或其他数据结构)中的元素,但它们有几个重要的区别:
- Enumeration 接口:
Enumeration是较早期的 Java 接口,位于java.util包中。Enumeration接口只有两个方法:hasMoreElements()用于检查是否还有元素,nextElement()用于获取下一个元素。Enumeration是只读的,只能从集合的开头向后遍历,不能对集合进行修改操作。
示例代码:
Vector<Integer> numbers = new Vector<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
Enumeration<Integer> enumeration = numbers.elements();
while (enumeration.hasMoreElements()) {
Integer number = enumeration.nextElement();
System.out.println(number);
}
- Iterator 接口:
Iterator接口是在 Java 1.2 中引入的,也位于java.util包中。Iterator接口提供了更强大的遍历功能,除了可以判断是否还有元素和获取下一个元素外,还可以删除元素(使用remove()方法)。Iterator是双向的,可以向前或向后遍历,并且支持对集合进行修改操作。
示例代码:
List<String> colors = new ArrayList<>();
colors.add("Red");
colors.add("Blue");
colors.add("Green");
Iterator<String> iterator = colors.iterator();
while (iterator.hasNext()) {
String color = iterator.next();
System.out.println(color);
if (color.equals("Blue")) {
iterator.remove(); // 删除元素
}
}
System.out.println(colors); // 输出 [Red, Green]
总的来说,Enumeration 是较早的遍历接口,只能从集合开头向后遍历且只读,而 Iterator 是更灵活、功能更强大的遍历接口,支持双向遍历和对集合的修改操作。在现代的 Java 开发中,推荐使用 Iterator 接口进行集合的遍历操作。
6、集合使用泛型有什么优点?
集合使用泛型有以下几个优点:
-
类型安全:使用泛型可以在编译时期检测集合中存储的元素类型是否与指定的类型相符,避免了在运行时出现类型转换错误的可能性。这提高了代码的可靠性和健壮性。
-
代码简洁:使用泛型可以减少代码中的类型转换操作,使代码更加简洁清晰。不需要在每次操作集合元素时都进行显式的类型转换,提高了代码的可读性。
-
提高性能:由于避免了类型转换,泛型集合在运行时的性能通常比非泛型集合更好。因为类型转换会引入额外的开销,而泛型消除了这种开销。
-
更好的错误检测和调试:泛型可以在编译时期检测到类型不匹配的错误,使得问题更早地暴露出来并且更容易调试和修复。
示例代码:
List<String> names = new ArrayList<>(); // 使用泛型指定集合存储的元素类型为String
names.add("Alice");
names.add("Bob");
// 编译时会进行类型检查,只能添加String类型的元素
// names.add(123); // 这行代码会在编译时报错
for (String name : names) {
System.out.println(name); // 不需要进行类型转换
}
总的来说,使用泛型可以提高代码的类型安全性、可读性和性能,是现代 Java 开发中推荐的做法。
7、List、Set、Map 之间的区别是什么?
List、Set 和 Map 是 Java 中常用的集合接口,它们之间的主要区别如下:
- List(列表):
- List 是有序的集合,可以包含重复元素。
- List 中的元素是按照插入顺序排列的,可以通过索引(位置)来访问和操作元素。
- List 接口的常用实现类有 ArrayList、LinkedList 和 Vector。
示例代码:
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Apple"); // 可以包含重复元素
System.out.println(list); // 输出 [Apple, Banana, Apple]
- Set(集合):
- Set 是无序的集合,不允许包含重复元素。
- Set 中的元素没有固定的顺序,插入顺序和访问顺序可能不同。
- Set 接口的常用实现类有 HashSet、TreeSet 和 LinkedHashSet。
示例代码:
Set<String> set = new HashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Apple"); // 不允许重复元素,这个元素不会被加入
System.out.println(set); // 输出 [Apple, Banana]
- Map(映射):
- Map 是键值对的集合,每个键对应一个值,键是唯一的而值可以重复。
- Map 中的元素是无序的,但是键是唯一的,可以根据键来查找对应的值。
- Map 接口的常用实现类有 HashMap、TreeMap 和 LinkedHashMap。
示例代码:
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 25);
map.put("Bob", 30);
map.put("Alice", 26); // 允许键重复,会更新原来的值
System.out.println(map); // 输出 {Alice=26, Bob=30}
总的来说,List 是有序的、允许重复元素的集合;Set 是无序的、不允许重复元素的集合;Map 是键值对的集合,键唯一但值可以重复。根据实际需求选择合适的集合类型可以提高代码的效率和可读性。
8、为什么 Map 接口不继承 Collection 接口?
Map 接口和 Collection 接口之间没有继承关系,主要是因为它们表示的数据结构和用途有很大的不同。
-
数据结构的不同:
Collection接口表示一组对象的集合,这些对象可以重复且顺序可能是有序的(比如List),也可以不重复且顺序可能是无序的(比如Set)。Map接口表示键值对的映射关系,每个键对应一个值,键是唯一的而值可以重复,键值对之间没有固定的顺序。
-
用途的不同:
Collection接口主要用于存储和操作一组对象,例如列表、集合等,常用于迭代、搜索、过滤等操作。Map接口主要用于存储键值对的映射关系,例如字典、映射表等,常用于根据键查找值、添加、删除键值对等操作。
由于数据结构和用途的不同,Map 接口和 Collection 接口并没有共同的父接口。如果 Map 接口继承自 Collection 接口,会导致以下问题:
- 需要在
Map中添加与Collection不相关的方法,比如get(key)、put(key, value)等,这样会导致接口的臃肿和不符合接口单一职责原则。 - 需要在
Map的实现类中同时实现Collection和Map的方法,增加了复杂性和代码量。 Map和Collection的用途不同,混淆它们的继承关系可能会给开发者造成困惑。
因此,为了保持接口的简洁性、单一职责原则和明确的用途,Java 设计时决定让 Map 接口和 Collection 接口独立存在,而不是继承关系。
9、常用的线程安全的 Map 有哪些?
常用的线程安全的 Map 主要有以下几种:
- ConcurrentHashMap:
ConcurrentHashMap是 Java 中线程安全的哈希表实现,提供了对并发访问的支持。- 它使用分段锁(Segment)来实现并发访问,不同的段可以同时进行读操作,从而提高了并发性能。
ConcurrentHashMap的性能优于Hashtable,尤其在多线程并发访问的情况下更明显。
示例代码:
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("Alice", 25);
concurrentMap.put("Bob", 30);
- Hashtable:
Hashtable是 Java 中最早的线程安全的哈希表实现,通过对整个数据结构进行同步来实现线程安全。- 它的所有方法都是同步的,可以保证线程安全,但在高并发情况下性能相对较低。
- 在现代 Java 开发中,一般推荐使用
ConcurrentHashMap来替代Hashtable。
示例代码:
Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("Alice", 25);
hashtable.put("Bob", 30);
除了以上两种,还可以使用 Collections.synchronizedMap() 方法来创建线程安全的 Map。这个方法返回一个同步的包装器(Synchronized Map),将普通的非线程安全的 Map 转换为线程安全的 Map。但需要注意的是,这种方式在并发访问时可能会存在性能问题,因为所有方法都是同步的,会造成竞争和阻塞。
示例代码:
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
synchronizedMap.put("Alice", 25);
synchronizedMap.put("Bob", 30);
总的来说,如果需要在多线程环境下使用 Map,并且需要较好的性能和并发支持,推荐使用 ConcurrentHashMap。如果需要传统的线程安全 Map,可以使用 Hashtable 或通过 Collections.synchronizedMap() 方法创建同步的 Map。
10、HashMap 与 Hashtable 的区别?
HashMap 和 Hashtable 是 Java 中常用的哈希表实现,它们之间的主要区别如下:
- 线程安全性:
HashMap是非线程安全的,不保证在多线程环境下的安全性,需要自行处理线程同步问题。Hashtable是线程安全的,所有方法都是同步的,可以在多线程环境下安全使用。
- 性能:
- 由于
Hashtable的所有方法都是同步的,因此在单线程环境下性能相对较低。 HashMap在单线程环境下性能更好,不需要同步操作。
- 由于
- 空值(null):
HashMap允许键和值都为 null。Hashtable不允许键或值为 null,否则会抛出 NullPointerException。
- 迭代器:
HashMap的迭代器(Iterator)是快速失败的,即在迭代过程中,如果其他线程对HashMap进行结构性修改,会抛出 ConcurrentModificationException 异常。Hashtable的迭代器不是快速失败的,因此可以在迭代过程中进行修改操作,不会抛出异常。
- 继承关系:
HashMap继承自 AbstractMap 类,实现了 Map 接口。Hashtable继承自 Dictionary 类,实现了 Map 接口。
HashMap实现原理:HashMap使用了数组和链表(或红黑树)的结合来实现哈希表。- 当向
HashMap中添加元素时,会根据键的哈希值确定存储位置,如果存储位置已经有元素存在,则会以链表或红黑树的形式存储在同一位置上。 - 当元素数量较少时,使用链表进行存储;当链表长度达到一定阈值时,会将链表转换为红黑树,以提高查找效率。
HashMap中有一个加载因子(load factor)的概念,当元素数量达到加载因子与数组大小的乘积时,会进行扩容操作,以保持哈希表的性能。
- Hashtable 的实现原理:
Hashtable使用了数组和链表的结合来实现哈希表,与HashMap类似。- 当向
Hashtable中添加元素时,也是根据键的哈希值确定存储位置,如果存储位置已经有元素存在,则会以链表的形式存储在同一位置上。 Hashtable在设计时考虑了线程安全性,所有方法都使用了同步关键字 synchronized,因此可以保证在多线程环境下的安全访问。
示例代码:
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("Alice", 25);
hashMap.put("Bob", 30);
Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("Alice", 25);
hashtable.put("Bob", 30);
总的来说,如果在单线程环境下使用,并且不需要考虑线程安全问题,推荐使用 HashMap。如果在多线程环境下使用,或者需要传统的线程安全 Map,可以使用 Hashtable。另外,现代 Java 开发中更推荐使用线程安全性更好的 ConcurrentHashMap 来替代 Hashtable。
11、HashMap 和 TreeMap 怎么选?
选择 HashMap 还是 TreeMap 取决于你的具体需求和场景:
-
需要快速的查找、插入和删除操作:
- 如果对于插入、删除、查找等操作的性能要求比较高,并且不需要对键进行排序,推荐使用
HashMap。 HashMap的查找、插入和删除操作的时间复杂度为 O(1),性能较好。
- 如果对于插入、删除、查找等操作的性能要求比较高,并且不需要对键进行排序,推荐使用
-
需要对键进行排序:
- 如果需要对键进行排序,并且不介意牺牲一些性能以换取排序功能,可以考虑使用
TreeMap。 TreeMap会根据键的自然顺序或者自定义的比较器进行排序,因此键会按照顺序排列。
- 如果需要对键进行排序,并且不介意牺牲一些性能以换取排序功能,可以考虑使用
-
对内存占用和性能要求比较高:
- 如果对内存占用和性能要求都比较高,且不需要排序功能,推荐使用
HashMap。 HashMap在大部分情况下对内存占用和性能都有较好的表现。
- 如果对内存占用和性能要求都比较高,且不需要排序功能,推荐使用
-
需要使用自定义的比较器或自然顺序进行排序:
- 如果需要使用自定义的比较器或者键的自然顺序进行排序,并且对性能要求不是非常苛刻,可以考虑使用
TreeMap。
- 如果需要使用自定义的比较器或者键的自然顺序进行排序,并且对性能要求不是非常苛刻,可以考虑使用
示例场景:
- 如果需要在大量数据中快速查找、插入和删除,并且不需要对键进行排序,则使用
HashMap更合适。 - 如果需要对键进行排序,并且对性能要求不是特别高,则使用
TreeMap可以方便实现排序功能。
综上所述,需要根据具体的需求来选择合适的集合类型,HashMap 适用于快速查找、插入和删除,而 TreeMap 则适用于需要排序功能的场景。
12、HashMap 的数据结构是什么?
HashMap 的数据结构是哈希表(Hash Table)。哈希表是一种基于键值对(Key-Value Pair)存储的数据结构,它通过将键的哈希值映射到数组的索引上来实现快速的查找、插入和删除操作。
下面是 HashMap 数据结构的基本原理:
-
数组:
HashMap内部维护一个数组(也称为桶或存储桶),用于存储键值对元素。- 数组的长度是根据
HashMap的容量和加载因子(load factor)动态调整的。
-
哈希函数:
- 当向
HashMap中添加键值对时,会先计算键的哈希值(通过调用键的hashCode()方法)。 - 哈希函数将键的哈希值映射到数组的索引上,确定键值对在数组中的存储位置。
- 当向
-
链表或红黑树:
- 在存储位置上可能会有多个键值对,当多个键的哈希值映射到同一个数组索引时,它们会以链表或红黑树的形式存储在同一位置上。
- 在 JDK 8 及之后的版本中,当链表长度达到一定阈值时(8),链表会转换为红黑树,以提高查找性能。
-
加载因子和扩容:
- 加载因子是
HashMap中一个重要的概念,它表示当前哈希表的负载程度,即已存储元素数量与数组长度的比值。 - 当加载因子超过一定阈值(默认为 0.75)时,会触发哈希表的扩容操作,即重新计算哈希值并重新分配存储位置,以保持哈希表的性能。
- 加载因子是
综上所述,HashMap 的数据结构是基于数组和链表(或红黑树)的组合来实现的。哈希表通过哈希函数将键的哈希值映射到数组索引上,并以链表或红黑树的形式存储键值对,以实现快速的查找、插入和删除操作。
13、HashMap 在 JDK 8 中有哪些改变?
在 JDK 8 中,HashMap 发生了一些重要的改变,主要包括以下几点:
-
红黑树优化:
- JDK 8 中对
HashMap进行了性能优化,特别是对于哈希碰撞较多的情况。 - 当链表长度达到一定阈值(默认为 8)时,
HashMap会将链表转换为红黑树,以提高查找性能,使得查找的时间复杂度从 O(n) 降低到 O(log n)。
- JDK 8 中对
-
扩容机制优化:
- JDK 8 中对
HashMap的扩容机制进行了优化,采用了新的扩容算法。 - 在旧的扩容算法中,每次扩容都是将原数组的元素重新分配到新数组中,这可能会导致性能下降。新的扩容算法在一定程度上避免了这个问题,减少了元素移动的次数,提高了扩容的效率。
- JDK 8 中对
-
数组存储节点的优化:
- JDK 8 中对
HashMap内部数组存储节点的结构进行了优化。 - 在旧的版本中,每个节点(Node)对象包含了键、值、哈希值和指向下一个节点的引用,这会占用额外的内存空间。新的版本中,对节点的结构进行了精简,减少了内存占用。
- JDK 8 中对
-
并发性改进:
- JDK 8 中对
HashMap的并发性能进行了改进。 - 引入了新的并发类
ConcurrentHashMap,采用了更加精细的分段锁机制,提高了并发环境下的性能和并发访问的吞吐量。
- JDK 8 中对
总的来说,JDK 8 中的 HashMap 在性能、扩容机制、内存占用和并发性等方面都进行了优化和改进,使得其在实际应用中具有更好的性能和稳定性。
14、HashMap 的 put 方法逻辑?
HashMap 的 put 方法是用来添加键值对的,其逻辑可以简单概括为以下几个步骤:
-
计算键的哈希值:
- 首先,根据键的
hashCode()方法计算键的哈希值。 - 如果键为 null,则哈希值为 0。
- 首先,根据键的
-
计算存储位置:
- 根据哈希值和数组长度计算键值对在数组中的存储位置(索引)。
- 使用哈希值的高位和数组长度取模来确定存储位置。
-
检查存储位置是否为空:
- 如果存储位置为空(即没有碰撞),直接将键值对存储在该位置。
- 如果存储位置已经有元素存在,则可能存在哈希碰撞,需要处理冲突。
-
处理哈希碰撞:
- 如果存储位置已经有元素存在,则可能存在哈希碰撞,需要处理冲突。
- 如果键已经存在于哈希表中,则更新对应键的值。
- 如果哈希碰撞发生,且存储位置上是链表,则将新的键值对以链表的形式添加到存储位置上。
- 如果链表长度达到一定阈值(默认为 8),则将链表转换为红黑树,以提高查找性能。
-
检查是否需要扩容:
- 在添加键值对后,会检查当前哈希表中元素数量是否超过了加载因子与当前容量的乘积(即负载因子阈值)。
- 如果超过了阈值,则进行扩容操作,重新计算每个键值对的存储位置。
示例代码(简化版):
public V put(K key, V value) {
// 计算键的哈希值
int hash = hash(key);
// 计算存储位置
int index = indexFor(hash, table.length);
// 检查存储位置是否为空
Node<K, V> existing = table[index];
if (existing == null) {
// 存储位置为空,直接添加键值对
table[index] = new Node<>(hash, key, value, null);
size++;
} else {
// 处理哈希碰撞
while (existing != null) {
if (existing.hash == hash && (existing.key == key || existing.key.equals(key))) {
// 键已经存在,更新值
existing.value = value;
return value;
}
existing = existing.next;
}
// 键不存在,以链表形式添加到存储位置
table[index] = new Node<>(hash, key, value, table[index]);
size++;
}
// 检查是否需要扩容
if (size > threshold) {
resize();
}
return value;
}
这是一个简化版的 put 方法逻辑,实际上 HashMap 的 put 方法还包括了更多细节,比如扩容时重新计算每个键值对的存储位置等。
15、HashMap 的 get 方法逻辑?
HashMap 的 get 方法用于根据键获取对应的值,其逻辑可以简单概括为以下几个步骤:
-
计算键的哈希值:
- 首先,根据键的
hashCode()方法计算键的哈希值。 - 如果键为 null,则哈希值为 0。
- 首先,根据键的
-
计算存储位置:
- 根据哈希值和数组长度计算键值对在数组中的存储位置(索引)。
- 使用哈希值的高位和数组长度取模来确定存储位置。
-
查找存储位置上的元素:
- 在确定了存储位置后,查找存储位置上的元素。
- 如果存储位置上没有元素(即没有哈希碰撞),则返回 null,表示没有找到对应的值。
- 如果存储位置上有元素,则可能存在哈希碰撞,需要处理冲突。
-
处理哈希碰撞:
- 如果存储位置上有元素,可能存在哈希碰撞,需要处理冲突。
- 遍历存储位置上的元素(链表或红黑树),根据键的哈希值和键值对的键进行比较,找到对应的键值对。
- 如果找到对应的键值对,则返回对应的值;如果没有找到,则返回 null。
示例代码(简化版):
public V get(Object key) {
// 计算键的哈希值
int hash = hash(key);
// 计算存储位置
int index = indexFor(hash, table.length);
// 查找存储位置上的元素
Node<K, V> node = table[index];
while (node != null) {
if (node.hash == hash && (node.key == key || node.key.equals(key))) {
// 找到对应的键值对,返回值
return node.value;
}
node = node.next;
}
// 没有找到对应的键值对,返回 null
return null;
}
这是一个简化版的 get 方法逻辑,实际上 HashMap 的 get 方法还包括了更多细节,比如处理红黑树结构、处理哈希碰撞等。
16、HashMap 是线程安全的吗?
HashMap 是非线程安全的,也就是说,在多线程环境下使用 HashMap 是不安全的。这是因为 HashMap 的内部结构并没有考虑线程安全性,多个线程同时对 HashMap 进行操作可能会导致数据不一致或者出现异常。
如果需要在多线程环境下安全地使用类似 HashMap 的功能,可以考虑以下几种方式:
- 使用线程安全的集合类:
- Java 提供了一些线程安全的集合类,例如
Hashtable、ConcurrentHashMap、Collections.synchronizedMap()等。 Hashtable是最早的线程安全的哈希表实现,所有方法都是同步的,但性能相对较低。ConcurrentHashMap是在多线程环境下性能较好的线程安全的哈希表实现,采用了分段锁机制。Collections.synchronizedMap()方法可以将普通的非线程安全的HashMap转换为线程安全的Map。
- Java 提供了一些线程安全的集合类,例如
示例代码:
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
- 使用同步控制手段:
- 如果需要自己实现线程安全的
HashMap,可以使用同步控制手段,比如使用synchronized关键字或者Lock接口来保护对HashMap的操作。 - 这种方式需要注意同步的粒度,以避免因为同步导致的性能问题和死锁等情况。
- 如果需要自己实现线程安全的
示例代码(使用 synchronized 关键字):
Map<String, Integer> map = new HashMap<>();
public synchronized void safePut(String key, Integer value) {
map.put(key, value);
}
public synchronized Integer safeGet(String key) {
return map.get(key);
}
总的来说,如果需要在多线程环境下使用类似 HashMap 的功能,并且需要考虑线程安全性,推荐使用线程安全的集合类或者自行实现线程安全控制。
17、HashMap 是怎么解决 hash 冲突的?
HashMap 在解决哈希冲突(Hash Collision)时,主要采用了两种策略:链地址法(Separate Chaining)和开放地址法(Open Addressing)。
-
链地址法(Separate Chaining):
- 这是
HashMap最常用的解决哈希冲突的方法。 - 当发生哈希冲突时,即多个键的哈希值映射到同一个数组索引位置上,
HashMap会将这些键值对以链表的形式存储在同一个位置上。 - 如果链表长度达到一定阈值(默认为 8),则会将链表转换为红黑树,以提高查找性能。
- 链地址法的优点是实现简单,并且能够有效解决哈希冲突,适用于大多数情况下的
HashMap。
- 这是
-
开放地址法(Open Addressing):
- 开放地址法是一种更加简单的解决哈希冲突的方法,它尝试在哈希表中找到另一个空闲位置来存储冲突的键值对。
- 当发生哈希冲突时,
HashMap会根据一定的探测序列(如线性探测、二次探测等)来查找下一个空闲位置。 - 开放地址法的优点是节省了存储空间,不需要额外的链表结构;缺点是可能会产生聚集现象,导致性能下降。
在 JDK 8 之前的版本中,HashMap 使用的是链地址法来解决哈希冲突;而在 JDK 8 及之后的版本中,当链表长度达到一定阈值时(默认为 8),会将链表转换为红黑树,以提高查找性能。这样既保留了链地址法的简单性,又提高了性能。
总的来说,HashMap 通过链地址法(以链表和红黑树形式存储冲突的键值对)和开放地址法(探测空闲位置存储冲突的键值对)来解决哈希冲突,保证了在哈希表中存储键值对的高效性和正确性。
18、HashMap 是怎么扩容的?
HashMap 在扩容时会进行以下步骤:
-
计算新的容量:
- 当
HashMap中的元素数量达到加载因子与当前容量的乘积时,即负载因子阈值,会触发扩容操作。 - 扩容时,会计算新的容量,新容量通常是当前容量的两倍。
- 当
-
创建新的数组:
- 根据新的容量创建一个新的数组,用于存储扩容后的键值对。
-
重新分配元素:
- 遍历原数组中的每个元素,将每个键值对重新计算存储位置,并存储到新的数组中。
- 计算存储位置的方法是根据键的哈希值、新的数组长度进行取模计算,确定元素在新数组中的存储位置。
-
设置新的数组:
- 将新的数组设为
HashMap的数组,替换原来的数组。 - 扩容完成后,原来的数组会被垃圾回收。
- 将新的数组设为
-
调整加载因子:
- 扩容完成后,会调整加载因子为新的加载因子阈值。
- 默认情况下,加载因子阈值为 0.75,即当元素数量达到容量的 75% 时触发扩容。
示例代码(简化版):
public V put(K key, V value) {
// ... 先计算哈希值并确定存储位置 ...
// 检查是否需要扩容
if (size > threshold) {
// 扩容操作
resize();
}
// ... 添加键值对到数组 ...
}
void resize() {
int oldCapacity = table.length;
int newCapacity = oldCapacity * 2; // 新容量为原容量的两倍
// 创建新的数组
Node<K, V>[] newTable = (Node<K, V>[]) new Node[newCapacity];
// 遍历原数组,重新分配元素到新数组
for (int i = 0; i < oldCapacity; i++) {
Node<K, V> node = table[i];
while (node != null) {
// 计算新的存储位置
int index = node.hash & (newCapacity - 1);
// 将键值对存储到新数组中
Node<K, V> next = node.next;
node.next = newTable[index];
newTable[index] = node;
node = next;
}
}
// 将新数组设为 HashMap 的数组
table = newTable;
// 调整加载因子为新的加载因子阈值
threshold = (int) (newCapacity * loadFactor);
}
以上是 HashMap 扩容的简化逻辑,实际上还包括了一些细节,比如在扩容过程中可能需要将链表转换为红黑树,以及调整存储位置等。通过扩容操作,HashMap 可以保持在元素数量增加时的高效性和性能。
19、HashMap 如何实现同步?
HashMap 并不是线程安全的,因为它的内部结构并没有考虑线程安全性。然而,可以通过一些方式来实现对 HashMap 的同步操作,使其在多线程环境中安全使用。
- 使用
Collections.synchronizedMap()方法:- 可以通过
Collections.synchronizedMap()方法将普通的非线程安全的HashMap转换为线程安全的Map。 - 这种方式会返回一个同步的
Map对象,对该对象的操作会进行同步处理,从而保证线程安全。
- 可以通过
示例代码:
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
- 使用
Hashtable:Hashtable是最早的线程安全的哈希表实现,所有方法都是同步的。- 可以直接使用
Hashtable来代替HashMap,以保证线程安全。
示例代码:
Hashtable<String, Integer> hashtable = new Hashtable<>();
- 使用
ConcurrentHashMap:ConcurrentHashMap是专门为并发环境设计的线程安全的哈希表实现,采用了分段锁机制。- 在多线程环境中,推荐使用
ConcurrentHashMap来代替HashMap,以获得更好的性能和线程安全性。
示例代码:
ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
这些方法都可以在多线程环境中安全地使用 HashMap 或类似的哈希表功能,保证了对键值对的操作是线程安全的。
20、HashMap 中的负载因子是什么?
HashMap 中的负载因子(Load Factor)是一个用来衡量哈希表负载程度的参数,它表示在哈希表中允许存储的键值对数量与哈希表容量的比值。负载因子的默认值是 0.75,这也是 Java 标准库中 HashMap 的默认负载因子。
负载因子的作用是在哈希表中平衡存储空间利用率和查找效率。具体来说,当哈希表中的键值对数量达到负载因子与当前容量的乘积时,即 size > threshold = capacity * loadFactor,哈希表会触发扩容操作,重新计算存储位置,以保持查找、插入和删除操作的高效性。
较低的负载因子可以减少哈希碰撞的概率,提高查找性能,但会增加存储空间的浪费;较高的负载因子可以减少存储空间的浪费,但可能导致哈希表的性能下降。
在实际应用中,可以根据具体场景和需求来调整负载因子的值。通常情况下,0.75 是一个比较合理的默认值,可以在大多数情况下保持哈希表的性能和存储空间的平衡。
21、Hashtable 为什么不叫 HashTable?
Hashtable 之所以不叫 HashTable,是因为 Java 中的命名规范和类名不同于英语中的一般命名规则。在 Java 中,类名通常采用驼峰命名法(Camel Case),即每个单词的首字母大写,而单词之间不使用下划线。
因此,Hashtable 是按照 Java 的命名规范命名的,而不是直接使用英语中的 HashTable。这样的命名风格更符合 Java 的编码风格,也方便开发者阅读和理解。
22、ConcurrentHashMap 的数据结构?
ConcurrentHashMap 是 Java 中专门为多线程环境设计的线程安全的哈希表实现,它的数据结构包括以下几个重要的部分:
-
Segment:
ConcurrentHashMap内部使用了分段锁(Segment)的机制来保证并发安全性。Segment是ConcurrentHashMap的核心部分,每个Segment类似于一个小的HashMap,维护着一部分键值对的存储。- 分段锁的设计可以有效减小锁的粒度,提高并发性能。
-
Hash Entry 数组:
- 每个
Segment内部都有一个HashEntry数组,用于存储键值对。 HashEntry是ConcurrentHashMap内部的节点结构,包含键、值、哈希值和指向下一个节点的引用等信息。
- 每个
-
并发控制:
ConcurrentHashMap使用了一些并发控制的技术,比如 volatile 关键字和 CAS(Compare and Swap)操作来保证并发更新的原子性和可见性。- 分段锁的设计使得在并发环境中只有特定的
Segment被锁定,可以实现更好的并发性能。
-
扩容机制:
- 与普通的
HashMap类似,ConcurrentHashMap也会在负载因子达到一定阈值时触发扩容操作。 - 扩容时会对每个
Segment进行扩容,重新计算存储位置,并将键值对迁移到新的HashEntry数组中。
- 与普通的
总的来说,ConcurrentHashMap 的数据结构是基于分段锁的哈希表,通过细粒度的锁控制和并发安全的技术实现了高效的并发访问和更新,适合在多线程环境下安全地使用。
23、ArrayList 是线程安全的么?
ArrayList 不是线程安全的,它是非线程安全的集合类。这意味着在多线程环境下同时对 ArrayList 进行读写操作可能会导致数据不一致或者出现异常。
如果需要在多线程环境下安全地使用类似 ArrayList 的功能,可以考虑以下几种方式:
- 使用
Collections.synchronizedList()方法:- 可以通过
Collections.synchronizedList()方法将普通的非线程安全的ArrayList转换为线程安全的List。 - 这种方式会返回一个同步的
List对象,对该对象的操作会进行同步处理,从而保证线程安全。
- 可以通过
示例代码:
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
- 使用线程安全的并发集合类:
- Java 提供了一些线程安全的并发集合类,例如
CopyOnWriteArrayList、ConcurrentLinkedQueue等。 CopyOnWriteArrayList是一个线程安全的动态数组,适用于读多写少的场景,因为写操作会导致复制整个数组。ConcurrentLinkedQueue是一个线程安全的无界队列,适用于高并发的生产者消费者模式。
- Java 提供了一些线程安全的并发集合类,例如
示例代码(使用 CopyOnWriteArrayList):
List<String> copyOnWriteList = new CopyOnWriteArrayList<>();
总的来说,在多线程环境下如果需要安全地使用类似 ArrayList 的功能,可以选择使用线程安全的集合类或者通过同步控制手段来保证操作的线程安全性。
24、常用的线程安全的 List 集合有哪些?
常用的线程安全的 List 集合包括:
-
CopyOnWriteArrayList:它通过在修改操作时复制整个内部数组来实现线程安全。适用于读操作远远多于写操作的场景。
-
ConcurrentLinkedDeque:基于链表的双端队列,支持并发访问,适用于队列操作。
-
Collections.synchronizedList:通过
Collections工具类可以创建线程安全的 List 集合,通过在每个方法上加锁来实现线程安全。 -
CopyOnWriteArraySet:类似于
CopyOnWriteArrayList,但是它是基于CopyOnWriteArrayList实现的,保证了元素不重复且线程安全。 -
ConcurrentSkipListSet:基于跳表的并发集合,支持高并发访问和有序性。
这些集合都可以在多线程环境下安全地进行操作,选择使用哪个取决于具体的业务需求和性能要求。
25、循环删除 List 集合可能会发生什么异常?
循环删除 List 集合中的元素可能会导致 ConcurrentModificationException 异常。这个异常表示在迭代过程中,尝试修改集合结构(例如添加或删除元素)而不是使用迭代器的方法进行修改。
这个异常通常在以下情况下发生:
- 使用普通的
for循环或者foreach循环遍历集合,并在遍历过程中删除或添加元素。 - 使用迭代器遍历集合时,在遍历过程中直接调用集合的
add、remove、clear等方法修改集合结构。
为了避免这个异常,可以采用以下方法:
- 使用迭代器进行遍历,并使用迭代器的
remove方法来删除元素。 - 使用
CopyOnWriteArrayList这样的线程安全集合,在遍历和修改操作之间不会抛出异常。 - 在遍历过程中记录需要删除的元素的索引或者使用另一个集合来保存需要删除的元素,遍历结束后再统一进行删除操作。
26、ArrayList 和 LinkedList 的区别?
ArrayList 和 LinkedList 是 Java 中常见的两种 List 集合实现,它们在内部结构和特性上有一些明显的区别:
-
内部结构:
- ArrayList 使用数组实现,可以随机访问元素,通过索引快速定位元素。
- LinkedList 使用双向链表实现,每个元素都包含对前一个和后一个元素的引用,插入和删除操作效率较高。
-
随机访问:
- ArrayList 可以通过索引直接访问任何位置的元素,时间复杂度为 O(1)。
- LinkedList 访问元素需要从头部或尾部开始遍历链表,时间复杂度为 O(n)。
-
插入和删除操作:
- ArrayList 在末尾插入或删除元素效率较高,时间复杂度为 O(1),但在中间插入或删除元素时需要移动后续元素,时间复杂度为 O(n)。
- LinkedList 在任意位置插入或删除元素效率都较高,时间复杂度为 O(1),因为只需要调整相邻元素的引用即可。
-
空间复杂度:
- ArrayList 需要预分配一定大小的数组空间,当元素数量超过数组大小时需要进行扩容,会消耗额外的内存。
- LinkedList 每个元素都需要额外的空间存储前后引用,可能会占用更多的内存空间。
综上所述,如果需要频繁进行随机访问操作或者末尾插入删除操作,可以选择 ArrayList;如果需要频繁进行中间插入删除操作或者不确定操作位置,可以选择 LinkedList。
27、ArrayList 和 Vector 的区别?
ArrayList 和 Vector 都是 Java 中用于存储对象的动态数组实现,它们有一些区别,主要体现在线程安全性和性能上:
-
线程安全性:
- ArrayList 是非线程安全的,不支持多线程并发操作,如果多个线程同时访问 ArrayList 并进行修改操作,可能会导致数据不一致或者抛出异常。
- Vector 是线程安全的,内部的方法都使用了 synchronized 关键字进行同步,可以保证多线程环境下的安全性。
-
性能:
- ArrayList 在单线程环境下性能通常比 Vector 更好,因为不需要进行额外的同步操作。
- Vector 在多线程环境下由于需要进行同步操作,可能会导致性能下降,而且在一些操作上可能会比 ArrayList 慢一些。
-
增长策略:
- ArrayList 和 Vector 在元素增长时的策略略有不同。ArrayList 的增长策略是当前容量不足时,增加 50% 的容量;而 Vector 的增长策略是当前容量不足时,增加原容量的一倍。
-
遗留性质:
- Vector 是 Java 中较早的集合类,许多方法都是遗留的,例如使用 Enumeration 进行迭代,而 ArrayList 则使用了更现代的迭代方式,如 Iterator。
综上所述,如果不需要考虑线程安全性,并且在单线程环境下需要更好的性能,可以选择 ArrayList;如果需要线程安全性或者在多线程环境下使用,可以选择 Vector。不过需要注意的是,Java 1.2 版本以后引入了更强大且高效的线程安全集合类,如 Collections.synchronizedList() 和 CopyOnWriteArrayList,因此在大多数情况下推荐使用这些更现代的线程安全集合类。
28、什么是 CopyOnWriteArrayList?
CopyOnWriteArrayList 是 Java 中的一个线程安全的 List 实现类,它的特点是在写入(增加、修改、删除)操作时会创建一个新的复制副本,而不是直接在原始数据上进行修改。这种机制可以保证在读取操作和写入操作同时进行时不会发生并发修改异常(ConcurrentModificationException)。
CopyOnWriteArrayList 的工作原理可以简单描述为:
- 当进行写入操作(增加、修改、删除)时,首先会复制一份原始数据的副本。
- 在副本上执行写入操作,并将副本替换原始数据。
- 读取操作仍然访问原始数据,不受写入操作的影响。
由于 CopyOnWriteArrayList 在写入操作时会复制一份数据,因此写入操作的性能相对较低,但在读取操作频繁、写入操作相对较少的场景下,它可以提供较好的线程安全性和性能表现。
CopyOnWriteArrayList 的主要特点包括:
- 线程安全:适用于多线程环境,不需要额外的同步措施。
- 读写分离:读取操作不会受到写入操作的影响。
- 写入操作的性能较低:由于需要复制数据,因此写入操作相对较慢。
这种数据结构适用于读多写少的场景,例如配置信息的读取和更新、事件监听器列表等。
29、什么是 fail-safe?
"fail-safe"(安全失败) 是另一种迭代器(Iterator)行为的策略,与 "fail-fast" 相对应。"fail-safe" 指的是在迭代过程中,允许对集合进行结构性修改,而不会抛出 ConcurrentModificationException 异常。这种策略的目的是保证在迭代过程中对集合进行修改时不会影响到正在进行的迭代操作。
Java 中的 Concurrent 包下的一些集合类(如 ConcurrentHashMap、CopyOnWriteArrayList)采用了 "fail-safe" 的策略。这些集合类在迭代过程中允许并发的修改操作,但并不会抛出 ConcurrentModificationException 异常,因为它们内部使用了一些机制来确保迭代过程的安全性。
可以通过以下示例理解 "fail-safe" 的行为:
List<String> list = new CopyOnWriteArrayList<>();
list.add("apple");
list.add("banana");
list.add("cherry");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
if (fruit.equals("banana")) {
list.remove(fruit); // 使用 CopyOnWriteArrayList,不会抛出异常
}
}
在上面的例子中,使用了 CopyOnWriteArrayList 类,它是一个 "fail-safe" 的集合类。在迭代过程中调用 list.remove() 方法并不会抛出异常,因为 CopyOnWriteArrayList 内部会创建一个新的副本来进行修改,保证了原始集合的不变性,并且不会影响到正在进行的迭代操作。
30、什么是 fail-fast?
在 Java 中,"fail-fast"(快速失败) 是一种迭代器(Iterator)行为的策略。它指的是在迭代过程中,如果集合的内容发生了结构性修改(比如在迭代过程中添加或删除元素),则会立即抛出 ConcurrentModificationException 异常,以避免在并发环境下出现不确定的行为。
这种策略的优点是能够及时检测到在迭代过程中对集合结构的修改,避免出现潜在的并发安全问题。但同时也需要注意,在迭代过程中不要修改集合的结构,如果需要修改集合,建议使用迭代器的 remove() 方法来安全地删除元素。
可以通过以下示例理解 fail-fast 的行为:
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("cherry");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
if (fruit.equals("banana")) {
list.remove(fruit); // 在迭代过程中修改集合结构,会抛出 ConcurrentModificationException 异常
}
}
在上面的例子中,当迭代器发现在迭代过程中集合结构发生了变化(比如调用了 list.remove() 方法),就会立即抛出异常,避免继续遍历导致不确定的行为。
31、fail-fast 与 fail-safe 有什么区别?
"Fail-fast"(快速失败)和 "fail-safe"(安全失败)是两种处理并发修改的策略,它们在多线程环境下对集合的操作行为有所不同。
-
Fail-fast(快速失败):
- 特点:在集合被并发修改时,快速失败策略会立即抛出 ConcurrentModificationException 异常,防止程序继续执行可能会产生不确定行为的情况。
- 适用场景:适用于那些无法接受并发修改的情况,例如对于非并发安全的集合类(如 ArrayList、HashMap 等)在多线程环境下的使用。
-
Fail-safe(安全失败):
- 特点:在集合被并发修改时,安全失败策略允许对集合进行结构性修改,并且不会抛出 ConcurrentModificationException 异常。它会使用一些机制来确保迭代过程的安全性。
- 适用场景:适用于需要支持并发修改的场景,例如对于并发安全的集合类(如 ConcurrentHashMap、CopyOnWriteArrayList 等)在多线程环境下的使用。
区别总结:
- 反应速度:Fail-fast 策略在发现并发修改时会立即抛出异常,反应速度较快;而 Fail-safe 策略则允许并发修改,不会立即抛出异常,反应速度相对较慢。
- 适用场景:Fail-fast 适用于对并发修改敏感的场景,需要快速发现问题并停止执行;Fail-safe 则适用于需要支持并发修改的场景,允许在迭代过程中进行修改操作。
总体来说,选择使用 Fail-fast 还是 Fail-safe 取决于具体的应用场景和对并发修改的要求。
32、HashSet 的底层实现原理是什么?
HashSet 的底层实现原理主要依赖于哈希表(Hash Table)。以下是 HashSet 的底层实现原理的简要描述:
-
哈希表: HashSet 内部使用一个哈希表来存储元素,哈希表是一种通过哈希函数将键映射到索引的数据结构。Java 中的哈希表通常是基于数组实现的,每个数组元素对应一个桶(Bucket),桶中可以存放多个元素。
-
哈希函数: HashSet 使用哈希函数来确定元素在哈希表中的存储位置。哈希函数将元素的值映射为一个整数,这个整数作为元素在哈希表中的索引。
-
存储过程:
- 当向 HashSet 中添加元素时,首先会计算元素的哈希码(通过调用元素的 hashCode() 方法)。
- 根据哈希码计算出元素在哈希表中的索引位置。
- 如果该索引位置上没有元素,则直接将元素存储在该位置。
- 如果该索引位置上已经有元素(发生了哈希冲突),则采用开放寻址法或者链表法解决冲突。
- 开放寻址法:根据某种规则(如线性探测、二次探测等)向后查找空闲位置,直到找到合适的位置插入元素。
- 链表法:在哈希表的每个桶上维护一个链表,将哈希冲突的元素存储在链表中。
-
扩容: 当 HashSet 中元素数量超过负载因子(load factor)时,会进行扩容操作,即重新计算哈希表的大小并重新哈希元素。这样可以保持哈希表的装填因子在一定范围内,提高哈希表的效率。
总体来说,HashSet 的底层实现利用哈希表来存储元素,通过哈希函数确定元素的存储位置,采用开放寻址法或者链表法解决哈希冲突,同时具有动
33、怎么确保一个集合不能被修改?
要确保一个集合不能被修改,可以采取以下几种方法:
-
使用不可变集合:
- Java 提供了一些不可变集合类,如 Collections.unmodifiableXXX() 方法可以创建不可变版本的集合,如不可变的 List、Set 和 Map。
- 不可变集合的特点是一旦创建后,就不能再修改其内容。如果尝试修改不可变集合,会抛出 UnsupportedOperationException 异常。
List<String> immutableList = Collections.unmodifiableList(new ArrayList<>(Arrays.asList("apple", "banana", "cherry"))); immutableList.add("date"); // 会抛出 UnsupportedOperationException 异常 -
使用特定的集合实现类:
- 选择适合不可修改场景的集合实现类,如使用 Collections.unmodifiableSet() 或 Collections.unmodifiableMap() 方法创建不可变的 Set 和 Map。
- 这些不可变集合类会在尝试修改时抛出 UnsupportedOperationException 异常,从而确保集合不会被修改。
Set<String> immutableSet = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("apple", "banana", "cherry"))); immutableSet.add("date"); // 会抛出 UnsupportedOperationException 异常 -
使用复制或克隆:
- 在需要保护集合不被修改的场景下,可以考虑在操作时先创建集合的副本或克隆,然后对副本进行操作,原始集合保持不变。
List<String> originalList = new ArrayList<>(Arrays.asList("apple", "banana", "cherry")); List<String> readOnlyList = new ArrayList<>(originalList); readOnlyList.add("date"); // 只会修改 readOnlyList,不会影响 originalList
总体来说,要确保一个集合不能被修改,可以选择使用不可变集合类、特定的不可变集合实现类或者在操作时使用集合的副本或克隆。这样可以有效地保护集合不受意外修改,提高代码的可靠性和稳定性。

浙公网安备 33010602011771号