java基础面试题七数据结构与集合源码
目录
1. 链表和数组有什么区别?
2. 栈是如何运行的?
3. ArrayList的默认大小是多少,以及扩容机制
4. ArrayList的底层是怎么实现的?
5. 数组和 ArrayList 的区别
6. 什么是线程安全的List?
7. 说说HahMap底层实现
8. HashMap初始值16,临界值12是怎么算的
9.HashMap长度为什么是2的幂次方
10. HashMap怎么计算哈希值和索引?扩容机制?怎么解决hash冲突?
11. HashMap底层是数组+链表,有数组很快了,为什么加链表?
12. HashMap为什么长度达到一定的长度要转化为红黑树
13. HashMap什么时候扩充为红黑树,什么时候又返回到链表?
14. 在 JDK1.8中,HashMap的数据结构与1.7相比有什么变化,这些变化的好处在哪里?
15. HashMap的get()方法的原理?
16. hashcode和equals区别?
17. hashCode() 与 equals() 生成算法、方法怎么重写?
18. 说一下equals和==的区别,然后问equals相等hash值一定相等吗?hash值相等equals一定相等吗?
19. HashSet存放数据的方式?
20. Set是如何实现元素的唯一性?
21. 用哪两种方式来实现集合的排序
1. 链表和数组有什么区别?
- 选择数组:当数据量固定且需要高效的随机访问时。
- 选择链表:当数据量动态变化且需要频繁插入或删除操作时。
2. 栈是如何运行的?
先进后出。属于ADT(abstract data type),可以使用数组、链表实现栈结构
3. ArrayList的默认大小是多少,以及扩容机制
ArrayList的扩容机制
ArrayList
的扩容机制是通过增加底层数组的大小来实现的。当元素数量超过当前容量时,ArrayList
会自动扩容。扩容的过程如下:
- 触发扩容:当向
ArrayList
中添加元素时,如果当前容量不足以容纳新元素,就会触发扩容操作。 - 新容量计算:
- 扩容后的新容量为 原容量的1.5倍(即:
newCapacity = oldCapacity + (oldCapacity >> 1)
)。 - 例如:
- 如果当前容量为10,扩容后容量将变为15。
- 如果当前容量为15,扩容后容量将变为22。
- 扩容后的新容量为 原容量的1.5倍(即:
- 数组复制:
- 扩容后,会创建一个新的数组,并将旧数组中的元素复制到新数组中。
4. ArrayList的底层是怎么实现的?
ArrayList
是 Java 中的一个动态数组实现,属于 java.util
包。其底层依赖于一个可变大小的数组来存储元素。以下是 ArrayList
的核心实现原理:
1. 底层数据结构
-
数组:
ArrayList
内部使用一个 Object[] 数组 来存储元素。它的声明如下:
transient Object[] elementData; // 用于存储元素的数组
transient
关键字:表示该字段不会被序列化,这是一种优化手段。
2. 添加元素(add 操作)
当调用 add(E e)
方法时:
- 检查容量:
- 确保底层数组有足够的空间容纳新元素。如果当前容量不足,会调用
ensureCapacityInternal
方法进行扩容。
- 确保底层数组有足够的空间容纳新元素。如果当前容量不足,会调用
- 存储元素:
- 将元素添加到数组的下一个空位。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保有足够的容量
elementData[size++] = e; // 将元素存入数组并增加size
return true;
}
3. 扩容机制
-
如果当前数组满了,会触发扩容。扩容的步骤包括:
- 计算新容量:通常是原容量的 1.5 倍。
- 创建新数组:分配一个新的、更大的数组。
- 复制元素:将旧数组中的元素复制到新数组中。
4. 删除元素(remove 操作)
-
删除元素时,
ArrayList
会将所有后续元素向前移动一位,以填补被删除元素的位置,并将最后一个元素置空。
5. 数组和 ArrayList 的区别
ArrayList看做是对数组的常见操作的封装。
总结
特性 | 数组 | ArrayList |
---|---|---|
大小 | 固定 | 动态调整 |
存储类型 | 基本类型和对象 | 仅存储对象(使用包装类) |
访问速度 | O(1) | O(1) |
插入/删除效率 | 低,需要手动移动元素 | 高,内置方法处理 |
内存使用 | 高效,无额外开销 | 可能有预留空间,开销较大 |
功能支持 | 无内置方法 | 提供丰富的方法 |
多维支持 | 直接支持 | 通过嵌套实现 |
适用场景 | 固定大小、性能优先 | 大小变化、不确定元素数量 |
6. 什么是线程安全的List?
线程安全的 List 是指在多线程环境中,多个线程可以安全地访问和修改同一个 List 实例,而不会导致数据不一致或抛出异常。线程安全的 List 提供了同步机制,确保每个线程在操作 List 时,不会与其他线程发生冲突。
Vector:线程安全的。
ArrayList:线程不安全。----> 使用同步机制处理。
7. 说说HahMap底层实现
1. HashMap 的基本结构
- 数组 + 链表 + 红黑树:
HashMap
由一个 Node 数组 组成,每个元素(桶)都是一个 链表 或 红黑树。- 在 Java 8 之前,
HashMap
只使用 数组 + 链表。Java 8 引入了 红黑树,解决链表过长导致的性能问题。
2. 核心数据结构
-
Node 类(存储键值对):
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 键的哈希值
final K key; // 键
V value; // 值
Node<K,V> next; // 下一个节点引用(用于链表)
}
重要字段:
hash
:通过键计算出的哈希值。key
和value
:键值对数据。next
:指向下一个节点(用于解决哈希冲突)。
总结
- 核心结构:
HashMap
由 数组 + 链表 + 红黑树 组成。 - 存储逻辑:根据键的哈希值计算索引,并存储在对应的桶中。
- 哈希冲突解决:使用链表解决冲突,链表过长时转换为红黑树。
- 性能:平均时间复杂度为 O(1);在最坏情况下(大量冲突时),链表查找是 O(n),而红黑树是 O(log n)。
通过引入红黑树,Java 8 优化了 HashMap
在高冲突场景下的性能,确保了更高的查询效率。
8. HashMap初始值16,临界值12是怎么算的
HashMap
的初始容量(默认值)是 16,默认负载因子是 0.75。临界值(threshold)是指触发 扩容 的元素数量阈值。临界值的计算公式如下:
临界值=初始容量×负载因子
9.HashMap长度为什么是2的幂次方
1. 高效的哈希计算
在 HashMap
中,确定元素存储位置(索引)是通过以下公式计算的:索引=(hash&(length−1))
其中:
hash
:键的哈希值。length
:数组长度(必须是 2 的幂次方)。
为什么用 2 的幂次方能简化计算?
-
位运算的优势:
- 位运算(
&
)比取模运算(%
)更快。 - 当
length
是 2 的幂次方时,hash & (length - 1)
等价于hash % length
,但效率更高。
- 位运算(
-
避免负数问题:
- 位运算直接操作二进制位,避免了负数取模的复杂处理。
-
均匀分布:
length - 1
的二进制形式是全 1(如 16 - 1 = 15 的二进制为1111
)。hash & (length - 1)
操作能确保索引均匀分布在[0, length - 1]
之间,减少哈希冲突。
2. 减少哈希冲突
-
哈希冲突:当不同键的哈希值计算出相同的数组索引时,就会发生哈希冲突。
-
2 的幂次方长度的作用:
- 确保在计算索引时,只取哈希值的低位参与计算,而低位的变化更容易分散。
- 如果长度是 2 的幂次方,哈希值中的所有位都能有效参与索引计算,从而提高分散性,减少冲突。
3. 负载因子与扩容效率
-
扩容机制:
当HashMap
扩容时,容量会变为原来的 2 倍。这样扩容后,原索引的元素要么保持不变,要么移动到新数组中的新索引(原索引 + 原容量)。- 例如,当前容量为 16,扩容到 32:
- 原索引
index = hash & 15
- 新索引有两种情况:
index
(保留原位置)index + 16
(原位置 + 旧容量)
- 原索引
这种扩展方式避免了重新计算所有哈希值,提升了扩容效率。
- 例如,当前容量为 16,扩容到 32:
10. HashMap怎么计算哈希值和索引?扩容机制?怎么解决hash冲突?
-
哈希值计算:
- 获取键的
hashCode()
。 - 通过扰动函数混合高低位:
(h = key.hashCode()) ^ (h >>> 16)
。
- 获取键的
-
索引计算:
索引=(hash&(length−1)) -
扩容机制:
- 触发条件:元素数量达到 容量 × 负载因子。
- 扩容后,容量翻倍,并重新分配元素。
-
哈希冲突解决:
- 使用 链表 存储冲突元素。
- 链表长度超过阈值时,转换为 红黑树,提高性能。
通过这些设计,HashMap
实现了高效的存储、查找和扩容,保证了在多种场景下的性能表现。
11. HashMap底层是数组+链表,有数组很快了,为什么加链表?
总结:为什么数组需要链表?
- 解决哈希冲突:数组只能存储一个元素,链表使得同一个桶可以存储多个元素。
- 数据完整性:确保即使哈希冲突发生,也不会覆盖已有数据。
- 平衡时间与空间复杂度:
- 正常情况:数组提供快速访问。
- 冲突情况:链表保证数据存储,红黑树提升性能。
- 扩展性:链表结构灵活,支持动态增加元素;结合红黑树后,可以应对大规模数据和严重冲突。
12. HashMap为什么长度达到一定的长度要转化为红黑树
- 链表转换为红黑树的原因:为了优化哈希冲突严重时的查找性能。
- 触发条件:
- 链表长度 ≥ 8 且数组长度 ≥ 64。
- 删除元素后,链表长度 ≤ 6 时转换回链表。
- 红黑树的优势:查找、插入、删除操作的时间复杂度为 O(log n),远优于链表的 O(n)。
通过这种设计,HashMap
在处理大量数据或严重哈希冲突时依然能保持高效的性能。
13. HashMap什么时候扩充为红黑树,什么时候又返回到链表?
转换方向 | 触发条件 | 原因 |
---|---|---|
链表 → 红黑树 | 链表长度 ≥ 8 且数组长度 ≥ 64 | 提高查找性能,避免线性时间复杂度。 |
红黑树 → 链表 | 链表长度 ≤ 6 | 节省内存开销,链表在小规模冲突下高效。 |
14. 在 JDK1.8中,HashMap的数据结构与1.7相比有什么变化,这些变化的好处在哪里?
① 在jdk8中,当我们创建了HashMap实例以后,底层并没有初始化table数组。当首次添加(key,value)时,进行判断,
如果发现table尚未初始化,则对数组进行初始化。
② 在jdk8中,HashMap底层定义了Node内部类,替换jdk7中的Entry内部类。意味着,我们创建的数组是Node[]
③ 在jdk8中,如果当前的(key,value)经过一系列判断之后,可以添加到当前的数组角标i中。如果此时角标i位置上有
元素。在jdk7中是将新的(key,value)指向已有的旧的元素(头插法),而在jdk8中是旧的元素指向新的
(key,value)元素(尾插法)。 "七上八下"
④ jdk7:数组+单向链表
jk8:数组+单向链表 + 红黑树
什么时候会使用单向链表变为红黑树:如果数组索引i位置上的元素的个数达到8,并且数组的长度达到64时,我们就将此索引i位置上
的多个元素改为使用红黑树的结构进行存储。(为什么修改呢?红黑树进行put()/get()/remove()
操作的时间复杂度为O(logn),比单向链表的时间复杂度O(n)的好。性能更高。
什么时候会使用红黑树变为单向链表:当使用红黑树的索引i位置上的元素的个数低于6的时候,就会将红黑树结构退化为单向链表。
15. HashMap的get()方法的原理?
-
时间复杂度:
- 最佳情况:O(1)(键分布均匀,无冲突)。
- 最坏情况:O(n)(大量冲突,链表较长)。
- 红黑树优化:在冲突严重时,复杂度降低为 O(log n)。
-
关键点:
- 通过哈希值和数组索引定位桶。
- 使用
hashCode()
和equals()
保证键的准确匹配。 - 通过链表或红黑树解决哈希冲突,提高查找效率。
16. hashcode和equals区别?
属性 | hashCode() | equals() |
---|---|---|
返回值 | 返回一个整数(哈希值) | 返回一个布尔值(true 或 false ) |
作用 | 用于确定对象在哈希集合中的存储位置 | 用于判断两个对象的内容是否相等 |
比较逻辑 | 计算哈希码(可能不同对象有相同哈希值) | 比较对象的实际内容 |
默认实现 | 基于对象内存地址生成哈希值 | 基于对象引用比较(== ) |
重写要求 | 如果重写了 equals() ,通常也要重写 hashCode() | 可以根据需要自定义比较逻辑 |
17. hashCode() 与 equals() 生成算法、方法怎么重写?
1. 重写 equals()
方法
基本原则:
- 自反性:
x.equals(x)
必须返回true
。 - 对称性:
x.equals(y)
为true
时,y.equals(x)
也必须为true
。 - 传递性:
x.equals(y)
和y.equals(z)
都为true
时,x.equals(z)
必须为true
。 - 一致性:在对象状态未改变的情况下,多次调用
equals()
应返回相同的结果。 - 与
null
比较:任何对象与null
比较都应返回false
。
2. 重写 hashCode()
方法
基本原则:
- 如果两个对象通过
equals()
方法比较返回true
,那么它们的hashCode()
值必须相等。 - 如果两个对象
equals()
返回false
,它们的hashCode()
值不必不同,但不同的哈希值有助于提高哈希集合的性能。 hashCode()
生成的哈希值应尽量分布均匀,以减少哈希冲突。
18. 说一下equals和==的区别,然后问equals相等hash值一定相等吗?hash值相等equals一定相等吗?
问题 1:equals()
相等,hashCode()
一定相等吗?
答案:是的。
根据 Java 规范:
- 如果两个对象通过
equals()
比较返回true
,那么它们的hashCode()
必须相等。 - 这是为了确保在哈希集合(如
HashMap
、HashSet
)中能正确地存储和检索对象。
问题 2:hashCode()
相等,equals()
一定相等吗?
答案:不一定。
- 不同对象可能具有相同的哈希值(这种情况称为 哈希冲突)。
- 因此,
hashCode()
相等并不意味着equals()
也一定返回true
。
19. HashSet存放数据的方式?
- 底层数据结构:
HashSet
基于HashMap
。 - 存储元素:将元素作为
HashMap
的键,固定常量PRESENT
作为值。 - 元素唯一性:依赖于
HashMap
的hashCode()
和equals()
方法来判断重复。
20. Set是如何实现元素的唯一性?
实现唯一性的原理
不同类型的 Set
实现唯一性的方式略有不同,但核心依赖于元素的 hashCode()
和 equals()
方法:
hashCode()
方法:用于计算对象的哈希值。equals()
方法:用于比较对象的内容是否相等。
1. HashSet
的唯一性实现
- 底层结构:
HashSet
使用HashMap
存储元素。 - 存储逻辑:
- 当向
HashSet
添加元素时,实际上是将元素作为HashMap
的键(key
)。 HashMap
的键不能重复,因此HashSet
也不会存储重复元素。
- 当向
- 流程:
- 调用元素的
hashCode()
计算哈希值,定位到哈希桶。 - 如果哈希桶中已有元素,使用
equals()
比较新旧元素。 - 若
equals()
返回true
,认为元素重复,不插入。 - 若返回
false
,插入元素。
- 调用元素的
2. LinkedHashSet
的唯一性实现
- 底层结构:基于
LinkedHashMap
。 - 与
HashSet
的区别:- 同样使用
HashMap
存储元素,但维护一个双向链表来记录元素的插入顺序。
- 同样使用
- 唯一性判断逻辑:
- 与
HashSet
相同,依赖于hashCode()
和equals()
。
- 与
3. TreeSet
的唯一性实现
- 底层结构:基于 红黑树。
- 存储逻辑:
- 使用自然顺序或自定义比较器(
Comparator
)对元素进行排序。 - 在插入新元素时,会根据比较器判断新元素与已有元素是否相等:
- 如果比较结果为 0,表示元素重复,不插入。
- 使用自然顺序或自定义比较器(
- 依赖方法:
- 自然排序:对象必须实现
Comparable
接口,重写compareTo()
方法。 - 自定义排序:使用
Comparator
对象比较。
- 自然排序:对象必须实现
21. 用哪两种方式来实现集合的排序
1. 使用 Comparable
接口
- 定义:
Comparable
是一个用于定义对象自然排序顺序的接口。 - 实现方式:类实现
Comparable
接口,并重写compareTo()
方法。 - 适用场景:当对象具有自然顺序(例如,按数字大小、字母顺序)时使用。
2. 使用 Comparator
接口
- 定义:
Comparator
是一个函数式接口,可以用来定义自定义排序规则。 - 实现方式:
- 创建
Comparator
实例,通常使用匿名类或 Lambda 表达式。 - 适用于无法修改类本身,或需要多个排序规则的情况。
- 创建
- 适用场景:当需要对对象进行多种不同的排序或不方便修改对象类时使用。