Java常见问题记录与思考
1.1 集合
在 Java 中,集合(Collection) 是用于存储、操作和处理一组对象的框架。Java中常见的集合有List、Queue、Set、Map等。
线程安全、是否重复,底层实现、
1.1.1 List:有序、可重复
1. ArrayList
底层:基于动态数组实现,数组的大小动态扩展,当数组达到最大容量时,会通过扩容机制(通常是当前容量的 1.5 倍或 2 倍)来增加容量。
效率:
插入操作:在末尾添加元素时,插入速度非常快,时间复杂度为 O(1);但如果在中间插入或删除元素,时间复杂度为 O(n),因为需要移动其他元素。
删除操作:删除元素时,需要将元素后面的所有元素向前移动,时间复杂度为 O(n)。
查询速度:由于是基于数组实现的,可以通过索引直接访问元素,查询速度非常快,时间复杂度为 O(1)。
线程安全性:不安全。
适用场景:需要快速随机访问元素的场景。插入和删除操作较少的情况。
2. LinkedList
底层:基于双向链表实现。每个元素(节点)包含指向前一个元素和下一个元素的引用。
效率:
插入操作:在列表头部或尾部插入元素时,速度非常快,时间复杂度为 O(1);如果在中间插入元素,需要先遍历到指定位置,时间复杂度为 O(n)。
删除操作:在列表头部或尾部删除元素时,速度也很快,时间复杂度为 O(1);删除中间元素时,时间复杂度为 O(n)。
查询速度:由于是链表结构,访问元素时需要从头或尾部开始逐个遍历,查询速度较慢,时间复杂度为 O(n)。
线程安全性:不安全。
使用场景:需要频繁插入和删除操作(特别是在列表头部或尾部),随机访问较少的场景。
3. Vector
底层:Vector 与 ArrayList 类似,也是基于动态数组实现,差别在于 Vector 在实现上做了线程同步的处理。
效率:
插入操作:与 ArrayList 类似,插入元素时如果是在末尾,时间复杂度为 O(1);但插入中间时,时间复杂度为 O(n)。
删除操作:删除元素时,其他元素需要向前移动,时间复杂度为 O(n)。
查询速度:与 ArrayList 类似,通过索引访问元素时查询速度非常快,时间复杂度为 O(1)。
线程安全性:线程安全,通过同步方法保证多线程访问时的数据一致性。但由于同步开销,性能较差,因此通常推荐使用 ArrayList 替代 Vector。
适用场景:需要线程安全、且访问频率较低的场景。
4. Stack
底层:继承自 Vector,也基于动态数组实现。
效率:
插入操作:push 操作(入栈)在末尾进行,时间复杂度为 O(1)。
删除操作:pop 操作(出栈)也在末尾进行,时间复杂度为 O(1)。
查询速度:与 Vector 相同,查询速度非常快,时间复杂度为 O(1)。
线程安全性:Stack 是线程安全的,因为它继承了 Vector,并且同步了所有的方法,但由于同步开销,性能不如 ArrayList 和 LinkedList。
适用场景:需要栈(LIFO)操作的场景,如深度优先搜索(DFS)、回溯算法等。
5. CopyOnWriteArrayList
底层:基于数组实现的,每次修改(如添加、删除元素)时,会复制整个底层数组。
效率:
插入操作:插入操作会创建一个新的数组,因此插入操作的时间复杂度是 O(n),由于需要复制数组,性能相对较差。
删除操作:删除操作同样需要复制数组,时间复杂度是 O(n)。
查询速度:查询速度非常快,因为底层是一个常规的数组,时间复杂度为 O(1)。
线程安全性:CopyOnWriteArrayList 是线程安全的。它通过每次写操作时复制整个底层数组来避免多个线程之间的竞争条件,非常适合读多写少的场景。
适用场景:读操作远多于写操作的场景,尤其是在需要多线程环境下的读写操作时。
集合类 | 底层实现 | 增删速度 | 查询速度 | 线程安全性 |
---|---|---|---|---|
ArrayList | 动态数组 | 插入末尾快,中间慢 | 通过索引访问快 | 非线程安全 |
LinkedList | 双向链表 | 头尾插入删除快,中间慢 | 查询慢(需遍历) | 非线程安全 |
Vector | 动态数组 | 插入末尾快,中间慢 | 通过索引访问快 | 线程安全(但效率差) |
Stack | 动态数组(继承自 Vector ) | 入栈出栈快 | 通过索引访问快 | 线程安全(但效率差) |
CopyOnWriteArrayList | 动态数组(复制数组) | 插入和删除慢(复制数组) | 查询快 | 线程安全(读多写少时优) |
常见问题:
1 ArrayList和LinkedList有什么区别?
特性 | ArrayList | LinkedList |
---|---|---|
底层实现 | 动态数组 | 双向链表 |
插入/删除(尾部) | O(1) | O(1) |
插入/删除(中间或头部) | O(n) | O(n) |
查询(通过索引) | O(1) | O(n) |
内存占用 | 相对紧凑,只有数据部分 | 较大,每个节点包含数据和两个指针 |
线程安全性 | 非线程安全 | 非线程安全 |
适用场景 | 需要频繁访问元素和尾部插入/删除的场景 | 需要频繁在头部或尾部插入删除元素的场景 |
2 对List集合去重都有哪些操作?
1 将 List
转换为 Set
,然后再将其转换回 List
。
```java
import java.util.*;
public class ListRemoveDuplicates {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "apple", "orange", "banana"));
// 使用 HashSet 去重
Set<String> set = new HashSet<>(list);
// 如果需要保持原有顺序,可以使用 LinkedHashSet
Set<String> orderedSet = new LinkedHashSet<>(list);
// 将 Set 转换回 List
List<String> uniqueList = new ArrayList<>(orderedSet);
System.out.println("去重后的列表:" + uniqueList);
}
2 Stream API,可以通过 distinct() 方法去重。distinct() 会返回一个不包含重复元素的流。
import java.util.*;
import java.util.stream.*;
public class ListRemoveDuplicates {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "apple", "orange", "banana"));
// 使用 Stream 的 distinct 方法进行去重
List<String> uniqueList = list.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("去重后的列表:" + uniqueList);
}
}
3 使用 List 的 contains() 方法手动去重
import java.util.*;
public class ListRemoveDuplicates {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "apple", "orange", "banana"));
List<String> uniqueList = new ArrayList<>();
for (String item : list) {
if (!uniqueList.contains(item)) {
uniqueList.add(item);
}
}
System.out.println("去重后的列表:" + uniqueList);
}
}
4 使用 ListIterator 和 contains() 去重(在原地去重)
import java.util.*;
public class ListRemoveDuplicates {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "apple", "orange", "banana"));
ListIterator<String> iterator = list.listIterator();
List<String> seen = new ArrayList<>();
while (iterator.hasNext()) {
String item = iterator.next();
if (seen.contains(item)) {
iterator.remove(); // 如果已经出现过,移除当前元素
} else {
seen.add(item); // 否则加入已见元素列表
}
}
System.out.println("去重后的列表:" + list);
}
}
5 使用 for 循环和 HashSet 去重(原地去重)
import java.util.*;
public class ListRemoveDuplicates {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "apple", "orange", "banana"));
Set<String> seen = new HashSet<>();
ListIterator<String> iterator = list.listIterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (!seen.add(item)) { // 如果元素已存在,add 返回 false,说明是重复元素
iterator.remove();
}
}
System.out.println("去重后的列表:" + list);
}
}
3 数组和链表分别适用于什么场景?
数组:需要 快速随机访问(通过索引获取元素)。数据量 较固定 或不需要频繁变化。对内存密度要求高,避免额外的指针开销。
链表:需要频繁进行 插入和删除,尤其是在中间位置。需要一个 动态大小 的数据结构,数据项的数量未知或经常变化。不需要随机访问,而是按顺序遍历数据。
4 ArrayList和LinkedList的底层数据结构是什么?
ArrayList:底层是动态数组,适合用于频繁查询和不太频繁修改的场景。
LinkedList:底层是双向链表,适合用于频繁进行插入和删除操作的场景,尤其是在头部或尾部插入时效率很高。
1.1.2 Queue
1 LinkedList(无界队列)
LinkedList 实现了 Queue 接口,是一个无界队列,它既能用作队列,也能用作双端队列(Deque)。
支持插入、删除操作,并且具有较高的灵活性。
插入、删除操作的时间复杂度是 O(1),因为它是基于链表实现的。
示例代码:
import java.util.LinkedList;
import java.util.Queue;
public class QueueExample {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
// 添加元素
queue.offer("A");
queue.offer("B");
queue.offer("C");
// 移除元素
System.out.println(queue.poll()); // 输出 A
// 查看队列头元素
System.out.println(queue.peek()); // 输出 B
}
}
2 PriorityQueue(优先队列)
PriorityQueue 是一个基于优先级的队列,元素会按照其优先级进行排序(默认是升序排序)。它是无界队列,不保证元素的顺序,只保证高优先级元素先被移除。
元素顺序:可以根据元素的自然顺序或自定义的比较器(Comparator)来排序。
适用于需要按照某种规则(如优先级)处理任务的场景。
示例代码:
import java.util.PriorityQueue;
import java.util.Queue;
public class PriorityQueueExample {
public static void main(String[] args) {
Queue<Integer> queue = new PriorityQueue<>();
// 添加元素
queue.offer(5);
queue.offer(1);
queue.offer(3);
// 移除元素
System.out.println(queue.poll()); // 输出 1 (优先级最高的元素)
}
}
3 ArrayBlockingQueue(有界队列)
ArrayBlockingQueue 是一个 有界队列,底层是基于数组实现的。它的大小在创建时指定,并且具有容量限制。当队列已满时,调用 offer() 或 put() 方法会被阻塞或返回 false。
常用于多线程编程中的生产者-消费者模式。
线程安全,多个线程可以安全地同时操作队列。
示例代码:
import java.util.concurrent.ArrayBlockingQueue;
public class ArrayBlockingQueueExample {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
// 添加元素
queue.put("A");
queue.put("B");
queue.put("C");
// 队列已满,调用put会阻塞
// queue.put("D"); // 将会阻塞,直到有空间
// 移除元素
System.out.println(queue.take()); // 输出 A
}
}
4 LinkedBlockingQueue(有界队列)
LinkedBlockingQueue 是一个基于链表实现的 有界队列。它是线程安全的,通常用于多线程的生产者-消费者模式。
容量可以在创建时指定,也可以使用默认容量。适用于需要线程安全并且可以支持较大容量的队列操作。
常见问题
1 在开发中常用哪些队列,在什么场景下使用的?
1. LinkedList(实现 Queue 接口)
用于一般的队列应用,存储和处理元素的顺序不需要严格的优先级控制。
广度优先搜索(BFS):如图算法中的广度优先搜索(BFS)需要按层级顺序访问节点时用到。
示例应用:
Web 请求的排队处理。
图遍历中的节点访问。
2. PriorityQueue
当不同任务有不同优先级时,PriorityQueue 可以确保高优先级任务优先执行。例如,操作系统中的进程调度。
事件处理系统:按时间或优先级顺序处理事件。
Dijkstra 算法:在图的最短路径算法中,优先队列用于按权重排序节点。
示例应用:
高优先级的任务(如急需处理的任务)优先执行。
实现贪心算法(如最小堆)。
1.1.3 Set不可重复
1 HashSet
底层结构:HashSet 是基于哈希表(HashMap)实现的,元素的存储依赖于元素的哈希值。
特点:
不保证元素的顺序:HashSet 不维护元素的插入顺序,因此元素的顺序可能与插入顺序不同。
查询效率高:由于哈希表的特性,HashSet 在元素查找、插入、删除等操作上具有 O(1) 的平均时间复杂度。
线程不安全:HashSet 不是线程安全的,在多线程环境下需要手动同步。
适用场景:
用于快速查找、插入、删除元素的场景。
在要求不关心元素顺序的情况下去重集合。
示例代码:
import java.util.HashSet;
import java.util.Set;
public class HashSetExample {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("C");
set.add("A"); // 重复元素,插入失败
System.out.println(set); // 输出 [A, B, C],元素不重复
}
}
2 LinkedHashSet
底层结构:LinkedHashSet 也是基于 哈希表 实现的,但它同时维护一个 链表,用来保持元素的插入顺序。
特点:
有序性:与 HashSet 不同,LinkedHashSet 保证元素按照插入的顺序排列。
插入顺序保持:即使删除元素,LinkedHashSet 也会保持剩余元素的插入顺序。
查询效率较高:相较于 HashSet,LinkedHashSet 的查询效率稍低,插入顺序的保证带来了一定的额外开销。
适用场景:
当你需要去重的同时保持元素的插入顺序时,可以使用 LinkedHashSet。
示例代码:
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetExample {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<>();
set.add("A");
set.add("B");
set.add("C");
set.add("A"); // 重复元素,插入失败
System.out.println(set); // 输出 [A, B, C],保持插入顺序
}
}
3 TreeSet
底层结构:TreeSet 是基于 红黑树(自平衡二叉查找树)实现的。
特点:
有序性:TreeSet 会自动按照元素的 自然顺序(元素实现 Comparable 接口)或根据提供的 Comparator 进行排序。
不允许重复元素:TreeSet 不允许重复元素,如果插入重复元素,则会被忽略。
较低的性能:由于是基于红黑树实现的,插入、删除、查找操作的时间复杂度为 O(log n),相比 HashSet 的 O(1),性能较低。
适用场景:
需要排序的场景,尤其是需要按升序、降序排列元素的场合。
如果需要对元素进行范围查询,TreeSet 提供了更好的支持。
示例代码:
import java.util.Set;
import java.util.TreeSet;
public class TreeSetExample {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
set.add(3);
set.add(1);
set.add(2);
set.add(2); // 重复元素,插入失败
System.out.println(set); // 输出 [1, 2, 3],自动按升序排序
}
}
常见问题
1 Set如何保证元素不重复?
通过 元素的 hashCode() 和 equals() 方法来进行比较和判断。如果两个对象的 hashCode() 不同,它们必定不重复,直接插入。
如果两个对象的 hashCode() 相同,Set 会进一步使用 equals() 方法来比较它们,如果 equals() 返回 true,则认为它们重复,不插入;如果返回 false,则认为它们不同,插入新的元素。
2 HashSet的原理是是什么?
基于哈希表实现元素的存储与查找。HashSet
的底层实现使用了 HashMap
,每个元素都作为 HashMap
的 键 存储,值 总是一个固定的常量。
- 当向
HashSet
中添加一个元素时,首先会计算该元素的 哈希值,使用hashCode()
方法。 - 根据哈希值,元素会被映射到哈希表中的一个桶(bucket)。每个桶的索引由哈希值决定。
- 对于正常情况下的插入、删除、查找等操作,
HashSet
的时间复杂度为 O(1),因为哈希表支持常数时间复杂度的查找。 - 但在极端情况下(如哈希冲突过多),时间复杂度可能退化为 O(n),尤其是当哈希表需要扩容时。
- 当哈希表的负载因子(即当前元素数量与表容量的比率)超过一定阈值时,
HashSet
会进行 rehash,即扩展哈希表的大小,并重新计算每个元素的位置。
3 TreeSet在排序是时如何比较元素的?
在 Java 中,TreeSet 是基于红黑树(自平衡二叉查找树)实现的,它能够自动对元素进行排序。TreeSet 在排序时是通过以下两种方式来比较和排序元素的:
- 使用元素的 compareTo() 方法(如果元素实现了 Comparable 接口)
如果集合中的元素实现了 Comparable 接口(即重写了 compareTo() 方法),TreeSet 会使用该方法来比较元素的大小,并根据返回值来进行排序。
compareTo() 方法:返回一个整数值,表示当前对象与另一个对象的比较结果:
如果返回值为负数,表示当前元素小于比较对象。
如果返回值为零,表示当前元素与比较对象相等。
如果返回值为正数,表示当前元素大于比较对象。
例如,TreeSet 会使用 compareTo() 方法来按升序排列元素。如果返回负值,则当前元素排在前面;如果返回正值,则当前元素排在后面。
示例代码:
import java.util.TreeSet;
class Person implements Comparable<Person> {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
// 实现 Comparable 接口,按年龄升序排序
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
public class TreeSetExample {
public static void main(String[] args) {
TreeSet<Person> set = new TreeSet<>();
set.add(new Person("Alice", 25));
set.add(new Person("Bob", 30));
set.add(new Person("Charlie", 20));
System.out.println(set); // 输出按年龄排序的结果: [Charlie (20), Alice (25), Bob (30)]
}
}
在上述代码中,Person 类实现了 Comparable 接口,并重写了 compareTo() 方法,根据年龄进行升序排序。
- 使用自定义的 Comparator(如果元素没有实现 Comparable)
如果集合中的元素没有实现 Comparable 接口,或者你希望使用不同的排序方式,可以在创建 TreeSet 时提供一个 自定义的 Comparator 对象来定义排序规则。
Comparator 接口:Comparator 接口定义了一个 compare(T o1, T o2) 方法,用于比较两个对象的大小,并根据返回值来确定排序顺序。
如果返回负数,o1 排在 o2 前面。
如果返回零,o1 和 o2 被认为是相等的。
如果返回正数,o1 排在 o2 后面。
示例代码:
import java.util.TreeSet;
import java.util.Comparator;
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
public class TreeSetExample {
public static void main(String[] args) {
// 使用自定义的 Comparator 来按姓名的字母顺序排序
TreeSet<Person> set = new TreeSet<>(new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return p1.name.compareTo(p2.name); // 按名字的字母顺序排序
}
});
set.add(new Person("Alice", 25));
set.add(new Person("Bob", 30));
set.add(new Person("Charlie", 20));
System.out.println(set); // 输出按姓名排序的结果: [Alice (25), Bob (30), Charlie (20)]
}
}
在这个示例中,TreeSet 使用自定义的 Comparator 来根据名字的字母顺序对 Person 对象进行排序。
- 总结
compareTo() 方法:如果元素实现了 Comparable 接口,TreeSet 会通过元素的 compareTo() 方法进行排序。
Comparator 接口:如果元素没有实现 Comparable 接口,或者你希望使用自定义的排序规则,TreeSet 会使用提供的 Comparator 对象来进行排序。
无论是使用 compareTo() 还是 Comparator,TreeSet 都会根据返回值来决定元素的排序顺序,从而保持元素的有序性。在默认情况下,TreeSet 会按升序排列元素,但你可以通过 Comparator 提供不同的排序规则(如降序等)。
1.1.4 Map
1 HashMap
底层结构:HashMap 是基于哈希表(数组 + 链表 / 红黑树)实现的。
特点:
键值对无序:HashMap 不保证键值对的顺序,键值对的顺序与插入顺序无关。
查询效率高:由于哈希表的性质,HashMap 的查询、插入和删除操作的时间复杂度通常为 O(1),但在哈希冲突较多的情况下,性能可能退化为 O(n)。
非线程安全:HashMap 是非线程安全的,如果多个线程并发访问,可能需要手动同步。
适用场景:
需要高效的查找、插入和删除操作,且不关心元素的顺序。
2 TreeMap
底层结构:TreeMap 是基于 红黑树(自平衡的二叉查找树)实现的。
特点:
按键的自然顺序排序:TreeMap 会根据键的 自然顺序(即键的 compareTo() 方法返回值)或者根据提供的 Comparator 进行排序。
不允许 null 键:TreeMap 不允许键为 null,因为无法比较 null 和其他对象的大小。
有序性:TreeMap 会保证键值对的顺序(按键的升序排列),因此查找、插入、删除操作的时间复杂度为 O(log n)。
适用场景:
需要保证键按一定顺序排列的场景,比如排序操作、范围查询等。
3 LinkedHashMap
底层结构:LinkedHashMap 基于 哈希表(HashMap)和 链表(双向链表)实现。
特点:
保持插入顺序:LinkedHashMap 会保持键值对的插入顺序,即迭代时会按照元素被插入的顺序返回键值对。
允许 null 键和值:与 HashMap 类似,LinkedHashMap 也允许 null 键和值。
比 HashMap 稍慢:由于需要维护一个链表来保持元素的插入顺序,LinkedHashMap 的性能相对于 HashMap 会稍微差一些,但差距通常不大。
适用场景:
需要保证键值对插入顺序的场景,如缓存实现。
4 ConcurrentHashMap
底层结构:ConcurrentHashMap 是为多线程环境设计的线程安全的 Map 实现,底层使用分段锁技术(Segment)来提供并发访问的支持。
特点:
线程安全:ConcurrentHashMap 是线程安全的,可以支持高并发的插入、删除和查询操作。
分段锁机制:通过对哈希表进行分段,每次只有一个线程可以访问一个段,减少了锁的竞争,提高了并发性能。
不允许 null 键和值:与 HashMap 不同,ConcurrentHashMap 不允许使用 null 作为键和值。
适用场景:
在多线程环境下,需要线程安全的 Map,如缓存、并发数据结构等。
常见问题
1 HashMap是如何快速定位到数据的?
HashMap 使用 哈希函数 将每个键映射到一个哈希值,然后根据哈希值计算出该键应存储的桶的位置(桶实际上是数组的索引)。一旦确定了桶的位置,HashMap 就可以快速地进行查找、插入和删除操作。
1.1 哈希值的计算
每个键(key)都有一个对应的 哈希值。在 HashMap 中,哈希值是通过键的 hashCode() 方法生成的:
int hashCode = key.hashCode();
该哈希值是一个整数,然后 HashMap 会对这个整数进行进一步的处理,以确保哈希值能够均匀地分布在哈希表的各个桶中。
1.2 哈希值映射到桶(bucket)
HashMap 中的哈希表实际上是一个数组,每个元素是一个桶(bucket)。每个桶对应一个链表或树形结构,用于存储多个具有相同哈希值(发生哈希冲突的情况)的元素。为了根据哈希值找到桶,HashMap 会通过 哈希值与数组长度的模运算(取余)来确定桶的位置:
int index = (hashCode) & (capacity - 1);
capacity 是当前哈希表的大小,通常是 2 的幂(如 16、32、64 等)。
通过 & (capacity - 1) 操作,相当于对 hashCode 进行模运算,使得哈希值映射到哈希表的索引范围。
1.3 处理哈希冲突
哈希冲突是指不同的键在经过哈希函数计算后,得到相同的哈希值。HashMap 通过以下两种方式来处理哈希冲突:
链式法(Separate Chaining):在同一个桶中,多个哈希值相同的元素会以链表的形式存储。这样,哈希冲突的元素会形成一个链表,链表中的每个元素都包含了键值对。
红黑树法(在 Java 8 引入):当某个桶中的链表长度过长(通常超过 8 个元素时),HashMap 会将链表转换成一个 红黑树,以提高查找效率。红黑树是自平衡的二叉查找树,查询时间复杂度为 O(log n)。
2. 查找操作的过程
假设你在 HashMap 中使用一个键来查找对应的值,查找过程如下:
计算哈希值:首先,调用键的 hashCode() 方法,计算出该键的哈希值。
映射到桶:然后,使用哈希值与数组大小进行模运算,找到该键应该存储的桶的位置。
查找元素:
如果该桶为空,则说明没有找到该键,直接返回 null。
如果该桶不为空,HashMap 会遍历桶中的元素:
如果桶中的元素较少,使用 链表 查找;如果桶中元素较多(即链表长度较长),则使用 红黑树 进行查找。
在链表或红黑树中,HashMap 会通过比较键的 equals() 方法来判断是否找到匹配的键。如果找到相同的键,则返回对应的值;如果没有找到,则继续查找下一个元素。
3. 插入操作的过程
在 HashMap 中插入一个新的键值对的过程如下:
计算哈希值:首先计算键的哈希值。
映射到桶:通过哈希值计算出该键应该插入的桶的位置。
处理哈希冲突:
如果该桶为空,直接将新的键值对插入该桶。
如果该桶已经有元素,HashMap 会遍历桶中的链表(或红黑树),检查是否有相同的键。如果没有相同的键,就将新键值对插入该桶;如果有相同的键,则替换原来的值。
扩容:当哈希表的负载因子超过阈值时(通常是 0.75),HashMap 会自动 扩容。扩容时,哈希表的大小会翻倍,并且重新计算所有元素的桶位置。
4. 删除操作的过程
删除操作的过程也类似:
计算哈希值:计算键的哈希值。
映射到桶:通过哈希值找到桶的位置。
遍历桶中的元素:
如果桶为空,返回 null,表示没有找到该键。
如果桶中有元素,HashMap 会遍历该桶(链表或红黑树),并使用 equals() 方法来查找目标键。
删除元素:一旦找到匹配的键,删除该键值对。如果桶中元素是链表,删除相应的链表节点;如果是红黑树,删除树中的节点。
5. 总结
HashMap 通过哈希表和哈希函数实现快速的数据查找,具体过程如下:
哈希值计算:通过 key.hashCode() 获取键的哈希值。
桶定位:通过模运算(& (capacity - 1))找到键的存储位置。
哈希冲突处理:采用链表或红黑树来解决哈希冲突。
常数时间复杂度:由于哈希表的特性,HashMap 的查询、插入和删除操作在平均情况下都是 O(1) 时间复杂度,哈希冲突的处理方式会影响性能,但通常情况下可以保证高效。
这种通过哈希值快速定位数据的方式,使得 HashMap 成为一个非常高效的键值对存储结构,适用于需要快速查找、插入和删除操作的场景。
2 ConcurrentHashMap是如何保证线程安全的?
分段锁,一个线程只能访问某一段内容
3 常用的Map集合有哪些?
HashMap、TreeMap、LinkedHashMap