Java 面试题
Java 语法基础练习题
翻转整数
给定一个 32 位有符号整数,将整数中的数字进行反转。如果反转后整数溢出那么就返回 0。
示例 1:
1 | 输入: 123 |
示例 2:
1 | 输入: -123 |
1 | private static int reverse(int num) { |
字符串转换整数
请你来实现一个 myAtoi(string s)
函数,使其能将字符串转换成一个 32 位有符号整数。
函数 myAtoi(string s)
的算法如下:
- 空格:读入字符串并丢弃无用的前导空格(
" "
) - 符号:检查下一个字符(假设还未到字符末尾)为
'-'
还是'+'
。如果两者都不存在,则假定结果为正。 - 转换:通过跳过前置零来读取该整数,直到遇到非数字字符或到达字符串的结尾。如果没有读取数字,则结果为0。
- 舍入:如果整数数超过 32 位有符号整数范围
[−231, 231 − 1]
,需要截断这个整数,使其保持在这个范围内。具体来说,小于−231
的整数应该被舍入为−231
,大于231 − 1
的整数应该被舍入为231 − 1
。
返回整数作为最终结果。
示例 1:
输入:s = “42”
输出:42
解释:加粗的字符串为已经读入的字符,插入符号是当前读取的字符。
1 | 带下划线线的字符是所读的内容,插入符号是当前读入位置。 |
示例 2:
输入:s = “ -042”
输出:-42
解释:
1 | 第 1 步:" -042"(读入前导空格,但忽视掉) |
示例 3:
输入:s = “1337c0d3”
输出:1337
解释:
1 | 第 1 步:"1337c0d3"(当前没有读入字符,因为没有前导空格) |
示例 4:
输入:s = “0-1”
输出:0
解释:
1 | 第 1 步:"0-1" (当前没有读入字符,因为没有前导空格) |
示例 5:
输入:s = “words and 987”
输出:0
解释:
读取在第一个非数字字符“w”处停止。
提示:
0 <= s.length <= 200
s
由英文字母(大写和小写)、数字(0-9
)、' '
、'+'
、'-'
和'.'
组成
1 | private static int parseInt(String str) { |
int digit = c - '0';
这行代码的作用是将字符 c
转换为对应的数字值。
字符与 ASCII 码的关系
在计算机中,字符通常以 ASCII 码的形式存储。例如:
- 字符
'0'
的 ASCII 值是 48 - 字符
'1'
的 ASCII 值是 49 - 字符
'2'
的 ASCII 值是 50 - …
- 字符
'9'
的 ASCII 值是 57
因此,字符 '0'
到 '9'
的 ASCII 值是连续递增的,差值恰好对应数字值。
代码解析
当你有一个字符变量 c
(例如 '3'
),执行 c - '0'
时:
c
被隐式转换为对应的 ASCII 值(例如'3'
的值是 51)。'0'
的 ASCII 值是 48。- 计算差值:
51 - 48 = 3
,得到的结果就是字符'3'
对应的数字 3。
示例:
1 | char c = '3'; |
数组和字符串
Java中String、StringBuffer和StringBuilder的区别是什么?
- String:不可变字符,每次拼接会创建新的对象。
- StringBuffer:可变字符,线程安全,多线程环境下性能高。
- StringBuilder:可变字符,线程不安全,单线程的环境下性能高。
String str = new String(“abc”) 创建了几个对象?
- 判断字符串常量池中是否存在字面量
"abc"
。- 若不存在:JVM 会在常量池中创建一个
String
对象,值为"abc"
。 - 若已存在:跳过此步骤,不创建新对象。
- 若不存在:JVM 会在常量池中创建一个
- 无论常量池中是否已有
"abc"
,new String("abc")
都会在堆内存中创建一个新的String
对象,该对象的值复制自常量池中的"abc"
。
1 | String str1 = new String("abc"); // 最多创建 2 个对象(常量池 + 堆) |
String str1 = new String(“abc”) 和 String str2 = “abc” 的区别?
new String("abc")
:- 无论如何都会在堆中创建新对象,且可能在常量池中创建对象(若字面量不存在)。
- 内存占用:至少 1 个对象(堆),最多 2 个对象(堆 + 常量池)。
String str2 = "abc";
:- 仅在常量池中创建对象(若不存在),否则直接引用常量池中的对象。
- 内存占用:0 或 1 个对象(仅当常量池不存在
"abc"
时创建)。
intern方法有什么作用?
intern()
是 String
类的一个 native 方法,用于手动将字符串对象添加到 字符串常量池 中,并返回常量池中的引用。其核心作用是 复用字符串对象,节省内存。以下是详细解析:
1. 基本原理
检查常量池:调用str.intern()
时,JVM 会先检查字符串常量池中是否已存在与str
内容相同(equals()
为 true)的字符串。
- 若存在:直接返回常量池中的引用,即使
str
本身在堆中。 - 若不存在:将
str
的内容添加到常量池(JDK 7+ 后直接复制引用,而非创建新对象),并返回该引用。
2. 关键区别(JDK 6 vs JDK 7+)
- JDK 6 及以前:常量池存放在永久代(方法区),
intern()
会在常量池 复制字符串对象(创建新对象)。 - **JDK 7+**:常量池移至堆内存,
intern()
会 直接引用堆中的对象,而非复制。
面向对象
抽象类和接口有什么区别
抽象类 | 接口 |
---|---|
定义与语法 | |
使用 abstract class 声明 |
使用 interface 声明 |
可以包含普通方法和抽象方法 | 只能包含抽象方法(Java 8+ 允许默认方法和静态方法) |
抽象方法使用 abstract 关键字声明 |
方法默认是 public abstract ,无需显式声明 |
可以有构造器 | 不能有构造器 |
继承与实现 | |
子类使用 extends 关键字继承 |
类使用 implements 关键字实现接口 |
一个类只能继承一个抽象类 | 一个类可以实现多个接口 |
子类必须实现所有抽象方法 | 实现类必须实现所有抽象方法(除非类是抽象类) |
成员变量 | |
可以有各种访问修饰符的成员变量 | 只能有 public static final 常量 |
示例:protected int age; |
示例:int MAX_SPEED = 100; (隐式为 public static final) |
设计目的 | |
表示 “is-a” 关系,定义类的基本特征和行为 | 表示 “can-do” 关系,定义类的额外能力 |
用于代码复用和共享公共状态 | 用于实现多态和松耦合 |
适合作为相关类的基类 | 适合为不相关类提供通用功能 |
应用场景 | |
当需要共享代码和状态时 | 当需要定义行为规范但不关心实现时 |
当类之间有共同的属性和行为时 | 当多个不相关的类需要实现同一功能时 |
例如:java.io.InputStream |
例如:java.util.Comparator |
- 使用抽象类:当需要定义类的基本行为,且有共享代码或状态时。
- 使用接口:当需要定义行为规范,且希望被多个不相关的类实现时。
Java中的参数传递是按值,还是按引用?
在Java中,参数传递只有按值传递,无论是基本类型还是引用类型。
什么是反射?
反射是 Java 在运行时动态获取类的信息(如方法、字段、构造器等),并可以动态调用对象的方法或操作字段的能力。核心类位于java.lang.reflect
包中。
反射原理:
Java 程序的执行分为编译和运行两步,编译之后会生成字节码(.class)文件,JVM 进行类加载的时候,会加载字节码文件,将类型相关的所有信息加载进方法区,反射就是去获取这些信息,然后进行各种操作。
基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 获取Class对象的三种方式
Class<?> clazz = MyClass.class; // 方式1:类名.class
Class<?> clazz = obj.getClass(); // 方式2:对象实例.getClass()
Class<?> clazz = Class.forName("com.MyClass"); // 方式3:全限定名
// 通过反射创建对象
Constructor<?> constructor = clazz.getConstructor();
MyClass obj = (MyClass) constructor.newInstance();
// 通过反射调用方法
Method method = clazz.getMethod("methodName", paramTypes);
method.invoke(obj, args);
// 通过反射访问字段
Field field = clazz.getDeclaredField("fieldName");
field.setAccessible(true); // 打破私有访问限制
field.set(obj, value);
反射在你平时写代码或者框架中的应用场景有哪些?
框架 | 反射应用场景 | 具体实现 |
---|---|---|
Spring | 依赖注入(IoC) | 通过反射创建Bean实例并注入依赖 |
AOP动态代理 | 动态生成代理类并拦截方法调用 | |
注解处理(@Autowired等) | 扫描并解析类/方法/字段上的注解 | |
Hibernate/MyBatis | ORM映射 | 通过反射将数据库结果集映射到Java对象 |
懒加载机制 | 动态生成实体类的代理对象 | |
JUnit | 测试用例发现和执行 | 通过反射查找带@Test注解的方法并执行 |
什么是Java中的动态代理?
动态代理是 Java 在运行时动态创建代理类和对象的机制,它允许在不修改原始类代码的情况下,通过代理对象控制对原始对象的访问。代理对象可以拦截对目标对象的方法调用,在方法执行前后插入自定义逻辑,增强功能(如日志、事务),实现 AOP(面向切面编程)。
核心机制
- JDK 动态代理:基于接口实现,要求目标类必须实现至少一个接口。
- CGLIB 动态代理:基于继承实现,通过生成目标类的子类拦截方法调用。
- 核心类
java.lang.reflect.Proxy
:生成代理实例。java.lang.reflect.InvocationHandler
:处理方法调用的拦截逻辑。
JDK的动态代理和CGLIB的动态代理有什么区别?
特性 | JDK 动态代理 | CGLIB 动态代理 |
---|---|---|
依赖接口 | 必须实现接口 | 无需接口,基于继承 |
代理方式 | 生成实现接口的代理类 | 生成继承目标类的子类 |
性能 | 创建快,调用慢(反射) | 创建慢,调用快(字节码) |
适用场景 | 接口导向的框架(如 Spring AOP) | 无接口的类(如 Spring 的 @Service) |
JDK 动态代理示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52// 1. 定义接口
interface UserService {
void saveUser(String username);
}
// 2. 实现接口
class UserServiceImpl implements UserService {
@Override
public void saveUser(String username) {
System.out.println("Saving user: " + username);
}
}
// 3. 实现InvocationHandler
class LoggingHandler implements InvocationHandler {
private final Object target; // 目标对象
public LoggingHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强
System.out.println("Before method: " + method.getName());
// 调用目标方法
Object result = method.invoke(target, args);
// 后置增强
System.out.println("After method: " + method.getName());
return result;
}
}
// 4. 创建代理对象并使用
public class Main {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
// 创建代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class<?>[]{UserService.class},
new LoggingHandler(target)
);
// 调用代理方法
proxy.saveUser("Alice");
}
}CGLIB 动态代理示例
1 | // 1. 定义目标类(无需实现接口) |
什么是Java 的SPI 机制?
SPI(Service Provider Interface) 是 Java 提供的一种服务发现机制,允许第三方实现接口并通过配置文件动态加载,它通过在运行时动态加载接口实现类,实现模块解耦和可插拔架构。SPI 的核心思想是”面向接口编程 + 约定优于配置“。
工作流程
graph TD A[定义服务接口] --> B[创建实现类] B --> C[配置META-INF/services文件] C --> D[ServiceLoader加载] D --> E[获取实现实例]
示例代码:
定义服务接口
1
2
3
4// 服务接口
public interface DataSource {
Connection getConnection();
}实现服务接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// MySQL实现
public class MySQLDataSource implements DataSource {
@Override
public Connection getConnection() {
return new MySQLConnection();
}
}
// PostgreSQL实现
public class PostgreSQLDataSource implements DataSource {
@Override
public Connection getConnection() {
return new PostgreSQLConnection();
}
}配置实现类
在
META-INF/services/com.example.DataSource
文件中写入:1
2com.example.MySQLDataSource
com.example.PostgreSQLDataSourceServiceLoader加载并使用
1
2
3
4
5
6
7
8
9
10
11
12
13ServiceLoader<DataSource> loader = ServiceLoader.load(DataSource.class);
// 遍历所有实现
for (DataSource ds : loader) {
Connection conn = ds.getConnection();
// 使用连接...
}
// 获取指定实现(需自行判断)
Optional<DataSource> mysqlDs = loader.stream()
.filter(p -> p.type().getName().contains("MySQL"))
.map(ServiceLoader.Provider::get)
.findFirst();
SPI 与 API 的区别
API | SPI |
---|---|
应用程序编程接口:框架或库提供的功能调用接口,用于被应用程序调用。 | 服务提供者接口:框架定义的标准接口,允许第三方提供实现,用于框架扩展。 |
调用方向:应用程序→API→框架 | 调用方向:框架→SPI→第三方实现 |
示例:Java 标准库的java.util.List 接口 |
示例:JDBC 的java.sql.Driver 接口 |
API:你调用别人提供的功能(如使用
HashMap
)SPI:别人调用你实现的扩展(如为JDBC提供新驱动)
单例模式有几种实现方式?
饿汉式(线程安全,非懒加载)
类加载时直接初始化实例,借助 JVM 类加载机制保证线程安全(类加载过程是单线程的)。
1
2
3
4
5
6
7
8
9
10
11
12
13public class HungrySingleton {
// 类加载时直接实例化
private static final HungrySingleton INSTANCE = new HungrySingleton();
//私有构造器,防止外部实例化
private HungrySingleton(){};
//全局访问点
private static HungrySingleton getInstance(){
return INSTANCE;
}
}特点:
- 优点:实现简单,线程安全(类加载时初始化)。
- 缺点:非懒加载(类加载即实例化),若实例占用资源大且长期不使用,会浪费内存。
懒汉式(线程不安全,懒加载)
延迟初始化实例,仅在第一次调用getInstance()
时创建,但多线程环境下会产生多个实例(线程不安全)。
1 | public class LazySingletonUnsafe { |
- 优点:懒加载,节省资源。
- 缺点:线程不安全,多线程环境下失效,实际开发中禁止使用。
懒汉式(线程安全,同步方法)
在
getInstance()
方法上添加synchronized
关键字,强制多线程串行执行,保证线程安全。1
2
3
4
5
6
7
8
9
10
11
12
13
14public class LazySingletonSafeSyncMethod {
private static LazySingletonSafeSyncMethod instance;
private LazySingletonSafeSyncMethod(){};
// 同步方法:多线程需排队执行,保证线程安全
private static synchronized LazySingletonSafeSyncMethod getInstance(){
if (instance == null){
instance = new LazySingletonSafeSyncMethod();
}
return instance;
}
}- 优点:线程安全,懒加载。
- 缺点:
synchronized
修饰整个方法,并发效率低(即使实例已创建,每次调用仍需排队)。
双重检查锁(DCL,线程安全,懒加载)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class DCLSingleton {
// volatile防止指令重排:避免"半初始化"实例被其他线程获取
private static volatile DCLSingleton instance;
private DCLSingleton (){}
public static DCLSingleton getInstance() {
// 第一次判空:避免已创建实例后仍进入同步块(提高效率)
if (instance == null){
synchronized (DCLSingleton.class){
// 第二次判空:防止多线程同时通过第一次判空后,重复创建实例
if (instance == null){
instance = new DCLSingleton();
// 注:new操作分3步:1.分配内存 2.初始化 3.赋值给instance
// 若不加volatile,可能发生指令重排(1→3→2),导致其他线程获取"半初始化"实例
}
}
}
return instance;
}
}特点:
- 优点:懒加载,线程安全,并发效率高(仅首次创建时同步)。
- 关键:
volatile
关键字不可省略,否则可能因指令重排导致线程安全问题。
静态内部类(线程安全,懒加载)
利用 JVM “静态内部类按需加载” 的特性实现懒加载,同时借助类加载机制保证线程安全。
1
2
3
4
5
6
7
8
9
10
11
12public class StaticInnerClassSingleton {
// 静态内部类:仅在调用getInstance()时才会加载
private static class InnerClass {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
private StaticInnerClassSingleton() {}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.INSTANCE;
}
}原理:
- 外部类加载时,静态内部类不会被加载,实现懒加载。
- 首次调用
getInstance()
时,静态内部类加载,其静态变量INSTANCE
初始化(JVM 保证类加载过程线程安全),确保唯一实例。
特点:实现简洁,线程安全,懒加载,效率高,是推荐的实现方式之一。
枚举单例(线程安全,天然防反射 / 序列化)
利用枚举的特性(JVM 保证枚举实例唯一且不可变)实现单例,是《Effective Java》推荐的最佳方式。
1
2
3
4
5
6
7
8public enum EnumSingleton {
INSTANCE; // 唯一实例
// 枚举可定义方法
public void doSomething() {
// 业务逻辑
}
}特点:
- 优点:线程安全(JVM 保证枚举实例仅被初始化一次),天然防止反射和序列化破坏单例,实现极简。
- 缺点:无法懒加载(枚举类加载时即初始化实例),若实例依赖参数初始化,灵活性较低。
如何避免反射和序列化破坏单例?
反射破坏单例及解决方案
反射破坏原理:通过
Class.getDeclaredConstructor()
获取私有构造器,调用setAccessible(true)
取消访问检查后,可多次调用newInstance()
创建实例。示例(破坏 DCL 单例):
1
2
3
4
5
6
7
8
9
10
11
12
13public class TestReflect {
public static void main(String[] args) throws Exception {
// 获取单例实例
DCLSingleton instance1 = DCLSingleton.getInstance();
// 反射获取私有构造器
Constructor<DCLSingleton> constructor = DCLSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 绕过私有访问限制
DCLSingleton instance2 = constructor.newInstance(); // 创建新实例
System.out.println(instance1 == instance2); // false(单例被破坏)
}
}解决方案:在私有构造器中添加判断,若已有实例则抛出异常,阻止重复创建。
1
2
3
4
5
6private DCLSingleton() {
// 防止反射破坏:若已有实例,抛异常
if (instance != null) {
throw new RuntimeException("禁止通过反射创建实例");
}
}序列化破坏单例及解决方案
序列化破坏原理:单例实例序列化后写入磁盘,反序列化时会通过反射创建新实例(即使构造器私有),导致单例失效。
示例(破坏静态内部类单例):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class TestSerialize {
public static void main(String[] args) throws Exception {
// 获取单例实例
StaticInnerClassSingleton instance1 = StaticInnerClassSingleton.getInstance();
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
oos.writeObject(instance1);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.obj"));
StaticInnerClassSingleton instance2 = (StaticInnerClassSingleton) ois.readObject();
System.out.println(instance1 == instance2); // false(单例被破坏)
}
}解决方案:在单例类中重写
readResolve()
方法,返回已有的单例实例,覆盖反序列化生成的新对象。1
2
3
4
5
6
7
8public class StaticInnerClassSingleton implements Serializable {
// ... 其他代码同上 ...
// 反序列化时,JVM会调用此方法,返回已有实例
private Object readResolve() {
return InnerClass.INSTANCE;
}
}枚举单例为何天然防破坏?
- 防反射:JVM 对枚举的构造器做了特殊保护,反射调用
newInstance()
时会直接抛出IllegalArgumentException
,无法创建新实例。 - 防序列化:枚举的序列化机制与普通类不同,反序列化时不会创建新实例,而是直接返回枚举常量(通过
Enum.valueOf()
获取已有实例)。
- 防反射:JVM 对枚举的构造器做了特殊保护,反射调用
泛型
Java中泛型擦除是什么?
泛型擦除是 Java 泛型的核心机制,指在编译后,泛型类型参数会被擦除,替换为其上限类型(通常是Object
),从而使代码在运行时不再保留泛型类型信息。
Java集合框架
ArrayList 和 LinkedList 有什么区别?
维度 | ArrayList | LinkedList |
---|---|---|
数据结构 | 动态数组 | 双向链表 |
随机访问 | 支持高效随机访问(O(1) ) |
不支持高效随机访问(O(n) ) |
插入 / 删除效率 | 尾部插入快(O(1) ),中间插入慢(O(n) ) |
任意位置插入 / 删除快(O(1) ,需先定位) |
内存占用 | 连续内存空间,可能有预分配冗余 | 非连续内存,每个节点包含前后引用 |
扩容机制 | 自动扩容(默认增长原来的 1.5 倍) | 无需扩容,按需分配节点 |
适用场景 | 频繁随机访问,较少插入删除 | 频繁插入删除,较少随机访问 |
ArrayList线程安全吗?把ArrayList变成线程安全有哪些方法?
不是线程安全的,ArrayList变成线程安全的方式有:
使用Collections类的synchronizedList方法将ArrayList包装成线程安全的List:
1
List<String> synchronizedList = Collections.synchronizedList(arrayList);
使用CopyOnWriteArrayList类代替ArrayList,它是一个线程安全的List实现:
1
CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>(arrayList);
使用Vector类代替ArrayList,Vector是线程安全的List实现:
1
Vector<String> vector = new Vector<>(arrayList);
说一下CopyOnWriteArrayList
CopyOnWriteArrayList
是Java为读多写少场景设计的线程安全列表,它通过写时复制机制避免了读操作的锁开销,实现了读写分离。但由于每次写操作都要复制数组,会带来内存和性能开销,因此适用于读密集、写较少且允许弱一致性的场景。在实际项目中,我们需要根据读写比例和一致性要求选择合适的容器。
- 原理
- 写操作:在执行
add()
、remove()
等修改操作时,会先复制原数组,在新数组上完成修改,最后用新数组替换原数组。 - 读操作:直接访问原数组,无需加锁,因此读操作无阻塞。
- 写操作:在执行
- 优点:读操作高效,线程安全,迭代器弱一致性避免并发修改异常。
- 缺点:写操作开销大(复制数组),占用更多内存,不适合写频繁的场景。
说一下 Collections.synchronizedList
Collections.synchronizedList
是 Java 提供的一个工具方法,它位于 java.util.Collections
类中。用于将普通列表(如 ArrayList
、LinkedList
)转换为线程安全的列表。它会对每个访问方法(如 get, set, add, remove)进行同步(加 synchronized 锁),从而保证线程安全。
CopyOnWriteArrayList和Collections.synchronizedList的区别
特性 | Collections.synchronizedList |
CopyOnWriteArrayList |
---|---|---|
实现方式 | 同步包装器,使用 synchronized |
写时复制(Copy-On-Write) |
锁粒度 | 全局锁(所有操作串行化) | 无锁读,写操作加锁 |
读性能 | 低(需获取锁) | 高(无需锁) |
写性能 | 中(仅需获取锁) | 低(需复制数组) |
迭代器安全性 | 不安全,需手动同步 | 安全(基于快照) |
适用场景 | 读写频率相近,且需要弱一致性 | 读多写少,且允许弱一致性 |
ArrayList的扩容机制?
当往 ArrayList 中添加元素时,会先检查是否需要扩容,如果当前容量+1 超过数组长度,就会进行扩容。
扩容后的新数组长度是原来的 1.5 倍,然后再把原数组的值拷贝到新数组中。
说一下HashMap的底层数据结构
HashMap 的底层是数组 + 链表 + 红黑树的复合结构(JDK1.7之前是数组 + 链表 )。
- 数组是主体,每个元素是一个桶;当发生哈希冲突时,冲突的元素会形成链表;
- 当链表长度超过 8 且数组长度 ≥64 时,链表会转换为红黑树以提升查询效率。
- 这种设计平衡了空间和时间复杂度,既避免了开放寻址法的扩容开销,又通过树化优化了链表过长的问题。
- 在 JDK 8 中,插入方式从头插法改为尾插法,避免了多线程环境下的链表成环问题,但仍不保证线程安全。
哈希冲突解决方法有哪些?
- 拉链法(链地址法):每个哈希桶中维护一个链表(或其他数据结构),冲突的元素都存入该链表。
- 链表法:
- 原理:JDK 7 及以前的
HashMap
采用此方法。当冲突发生时,将元素插入链表头部(头插法)。 - 优点:实现简单,无需扩容整个哈希表,适用于冲突频繁的场景。
- 缺点:链表过长时查询效率为 O (n)。
- 原理:JDK 7 及以前的
- 链表 + 红黑树:
- 原理:JDK 8 后的
HashMap
优化:当链表长度超过阈值(8)且哈希表容量 ≥64 时,将链表转换为红黑树(查询效率 O (log n))。 - 优点:高效处理大量冲突,避免链表过长导致的性能问题。
- 原理:JDK 8 后的
- 链表法:
- 再哈希法:当发生冲突时,使用另一个哈希函数再次计算键的哈希值,直到找到一个空槽来存储键值对。
- 开放寻址法:当发生冲突时,直接在哈希表中寻找下一个空闲位置。常见的开放寻址方法包括线性探测、二次探测和双重散列。
HashMap和HashTable的区别
- 线程安全:HashMap 非线程安全,HashTable 线程安全(但性能差,推荐用 ConcurrentHashMap)。
- 空值支持:HashMap 允许 null 键和 null 值,HashTable 不允许。
- 历史设计:HashMap 继承自 AbstractMap,HashTable 继承自 Dictionary(已过时)。
- 容量与扩容:HashMap 初始容量 16 且必须为 2 的幂,HashTable 初始 11,扩容为 2n+1。
- 性能:HashMap 单线程性能更高,HashTable 因全局锁性能较差。
在实际开发中,优先使用 HashMap,仅在需要线程安全时考虑 ConcurrentHashMap,避免使用 HashTable。
ConcurrentHashMap和HashTable的区别
- 锁机制:ConcurrentHashMap 在 JDK 8 采用 CAS + synchronized 锁定桶节点,并发度更高;HashTable 使用全局锁。
- 性能:ConcurrentHashMap 读无锁,写仅锁冲突节点,性能远优于 HashTable。
- 数据结构:JDK 8 的 ConcurrentHashMap 采用数组+链表+红黑树;HashTable与 HashMap 一致,但所有操作同步。
- 空值支持:两者均不允许 null 键值。
- 迭代器:ConcurrentHashMap 弱一致性,不抛异常;HashTable 可能抛出 ConcurrentModificationException。
在实际开发中,强烈推荐使用 ConcurrentHashMap 替代 HashTable,尤其在高并发场景下,其性能优势显著。
HashSet和HashMap的区别
- 接口与功能:HashSet 实现 Set 接口,存储唯一元素;HashMap 实现 Map 接口,存储键值对。
- 底层实现:HashSet 依赖 HashMap,将元素作为 key 存储,value 固定为 PRESENT。
- null 支持:两者均允许 1 个 null 键(HashSet 的 null 元素),但 HashMap 还允许多个 null 值。
- 应用场景:HashSet 用于去重和存在性检查,HashMap 用于键值映射。
在实际开发中,若只需存储不重复元素,选择 HashSet;若需键值关联,选择 HashMap。
HashMap的put流程
计算 key 的哈希值(高 16 位与低 16 位异或),减少哈希冲突。
1
2
3
4
5
6
7static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//确定桶位置。
index = (n - 1) & hash // n 为数组长度,必须是 2 的幂次方若数组未初始化,先进行扩容(初始容量 16)。
通过哈希值和数组长度取模确定桶位置。
处理桶内节点:
- 若桶为空,直接插入新节点。
- 若首节点 key 匹配,覆盖旧值。
- 若是树节点,调用树插入逻辑。
- 若是链表,遍历链表:
- 找到相同 key 则覆盖。
- 未找到则链尾插入,若链表长度 ≥8 且数组长度 ≥64,将链表转换为红黑树。
插入后检查元素数量是否超过阈值(容量 × 加载因子),超过则扩容。
JDK 8 对 put 方法进行了优化,采用尾插法避免多线程下的链表成环问题,并通过红黑树优化了链表过长时的性能。
HashMap的扩容机制
当元素数量超过阈值(capacity * loadFactor)即 容量(初始化16) * 负载因子(默认 0.75) 时,触发扩容。
创建一个新的数组,容量为原来的2倍,并重新计算阈值。
1
newCap = oldCap << 1(例如,16 → 32)。
数据迁移
- 通过
hash & oldCap
将节点分为两类:结果为 0 则留在原索引,否则移至原索引+oldCap。 - JDK 8 优化:将链表拆分为低位链和高位链,避免链表反转,提高效率。
- 通过
树化与反树化
- 若链表长度 ≥8 且容量 ≥64,转换为红黑树。
- 扩容后若树节点数 ≤6,红黑树退化为链表。
讲讲 LinkedHashMap
LinkedHashMap 是 HashMap 的子类,它在哈希表的基础上增加了双向链表,用于维护元素的插入顺序或访问顺序。
LinkedHashMap 在 HashMap 的基础上维护了一个双向链表,通过 before 和 after 标识前置节点和后置节点。
讲讲TreeMap
1 | TreeMap 是基于红黑树实现的有序 Map,主要特点如下: |
讲讲ConcurrentHashMap
ConcurrentHashMap
是 Java 提供的线程安全哈希表,主要特点如下:
- JDK 8 优化:
- 数据结构:数组 + 链表 + 红黑树,与 HashMap 一致。
- 锁机制:抛弃JDK1.7之前使用的分段锁,采用 CAS + synchronized 锁定桶头节点,并发度更高。
- 核心优势:
- 高效并发:读操作无锁,写操作仅锁冲突的桶,性能远优于 HashTable。
- 弱一致性:迭代器不抛出 ConcurrentModificationException,允许遍历期间修改。
- 原子操作:
- 提供 putIfAbsent、computeIfAbsent 等原子方法,避免复合操作的竞态条件。
- 适用场景:
- 高并发读写场景(如缓存、计数器)。
- 需线程安全但不希望使用全局锁的场景。
相比 HashTable,ConcurrentHashMap 的锁粒度更小,读操作无锁,性能显著提升;相比 Collections.synchronizedMap,它提供了更丰富的原子方法和更高的并发度。
ConcurrentHashMap 为什么 key 和 value 不能为 null?
- 避免歧义:
- 在并发场景下,若允许 null,当 get(key) 返回 null 时,无法区分是 key 不存在还是 value 为 null,可能导致逻辑错误。
- 原子操作的语义:
- 原子方法(如 putIfAbsent)依赖返回值判断操作结果。若允许 null,返回 null 无法明确是插入成功还是 key 已存在,破坏原子性。
- 并发复杂性:
- 弱一致性迭代器难以处理 null 值的语义,增加实现复杂度。
相比之下,HashMap 允许 null 是为了单线程下的灵活性,但在并发环境中,这种灵活性会带来安全隐患。因此,ConcurrentHashMap 选择严格设计,强制用户处理 null 的情况,避免潜在问题。






- 1. Java 面试题
- 1.1. Java 语法基础练习题
- 1.2. 数组和字符串
- 1.3. 面向对象
- 1.4. 泛型
- 1.5. Java集合框架
- 1.5.1. ArrayList 和 LinkedList 有什么区别?
- 1.5.2. ArrayList线程安全吗?把ArrayList变成线程安全有哪些方法?
- 1.5.3. 说一下CopyOnWriteArrayList
- 1.5.4. 说一下 Collections.synchronizedList
- 1.5.5. CopyOnWriteArrayList和Collections.synchronizedList的区别
- 1.5.6. ArrayList的扩容机制?
- 1.5.7. 说一下HashMap的底层数据结构
- 1.5.8. 哈希冲突解决方法有哪些?
- 1.5.9. HashMap和HashTable的区别
- 1.5.10. ConcurrentHashMap和HashTable的区别
- 1.5.11. HashSet和HashMap的区别
- 1.5.12. HashMap的put流程
- 1.5.13. HashMap的扩容机制
- 1.5.14. 讲讲 LinkedHashMap
- 1.5.15. 讲讲TreeMap
- 1.5.16. 讲讲ConcurrentHashMap
- 1.5.17. ConcurrentHashMap 为什么 key 和 value 不能为 null?