JavaEE
- Java基础概念面试题详解
- 1. Java的特点是什么?
- 2. Java和C++的区别有哪些?
- 3. 什么是JDK、JRE和JVM?它们之间有什么关系?
- 4. Java是编译型语言还是解释型语言?
- 5. Java如何实现跨平台?
- 数据类型与变量面试题详解
- 1. Java有哪些基本数据类型?
- 2. `int`和`Integer`有什么区别?
- 3. 什么是自动装箱和拆箱?
- 4. `String`、`StringBuilder`和`StringBuffer`的区别?
- 5. `==`和`equals()`的区别?
- 面向对象编程(OOP)面试题详解
- 1. 面向对象的四大特性是什么?
- 2. 什么是类和对象?
- 3. 重载(Overload)和重写(Override)的区别?
- 4. 抽象类和接口的区别?
- 5. 什么是多态?如何实现多态?
- 6. 构造方法(Constructor)的特点和作用?
- 7. this和super关键字的区别?
- 8. 什么是静态变量和静态方法?
- 9. final、finally和finalize的区别?
- 异常处理面试题详解
- 1. Java异常体系结构是怎样的?
- 2. Error和Exception的区别?
- 3. 受检异常和非受检异常的区别?
- 4. try-catch-finally的执行顺序?
- 5. throw和throws的区别?
- 集合框架面试题详解
- 1. Java集合框架的主要接口和类有哪些?
- 2. List、Set和Map的区别?
- 3. ArrayList和LinkedList的区别?
- 4. HashMap的工作原理?
- 5. HashMap和HashTable的区别?
- 6. ConcurrentHashMap如何保证线程安全?
- Java 7 及之前的 ConcurrentHashMap 实现
- Java 8 及之后的 ConcurrentHashMap 实现
- 重要内部类变化
- 并发控制机制对比
- 为什么Java 8要重新设计?
- 典型使用场景建议
- 多线程面试题详解
- 1. 线程和进程的区别?
- 2. 创建线程的几种方式?
- 3. Runnable和Callable的区别?
- 4. 线程的生命周期(状态)有哪些?
- 5. sleep()和wait()的区别?
- 6. synchronized关键字的用法?
- 7. volatile关键字的作用?
- 8. 什么是线程池?为什么要用线程池?
- IO与NIO面试题详解
- 1. Java IO流分为哪几种?
- 2. 字节流和字符流的区别?
- 3. BIO、NIO和AIO的区别?
- 4. NIO的核心组件有哪些?
- 反射与泛型面试题详解
- 1. 什么是反射?反射的应用场景?
- 2. 如何获取Class对象?
- 3. 什么是泛型?泛型擦除是什么意思?
- Java 8新特性面试题详解
- 1. Lambda表达式的作用?
- 2. Stream API的常用方法有哪些?
- 3. 什么是函数式接口?常见的函数式接口有哪些?
- 4. Optional类的作用?
- JVM基础面试题详解
- 1. JVM内存结构是怎样的?
- 2. 什么是垃圾回收(GC)?常见的垃圾回收算法有哪些?
- 3. 强引用、软引用、弱引用和虚引用的区别?
- 4. 类加载过程是怎样的?
- Java Stream 补充面试题详解
- 1. Stream 的中间操作和终止操作有什么区别?
- 2. 并行流(Parallel Stream)使用时要注意什么?
- 3. Stream 和 Collection 的区别?
- 4. 常用的 Stream 收集器(Collectors)有哪些?
- 5. Stream 中的 findFirst() 和 findAny() 有什么区别?
- 6. Stream 的 flatMap() 和 map() 有什么区别?
- 7. 如何用 Stream 处理大文件?
- 8. Stream 的 peek() 和 forEach() 有什么区别?
- 9. 如何自定义 Collector?
- 10. Stream 的 reduce() 方法如何使用?
- Java 其他高频面试题补充
- 1. Java 模块化系统(Java 9+)
- 2. 记录类型(Record, Java 14+)
- 3. 密封类(Sealed Class, Java 15+)
- 4. 模式匹配(Java 16+)
- 5. 虚拟线程(Virtual Thread, Java 19+)
- 6. 响应式编程(Reactive Streams)
- 7. Java 原生内存访问(Java 16+)
- 8. 协程库比较
- 9. 微基准测试(JMH)
- 10. 代码安全实践
- 11. JVM 性能调优
- 12. 现代Java开发实践
Java基础概念面试题详解
1. Java的特点是什么?
Java的主要特点包括:
- 面向对象:完全遵循OOP原则(封装、继承、多态)
- 平台无关性:通过"Write Once, Run Anywhere"机制实现
- 健壮性:
- 强类型检查
- 自动内存管理(垃圾回收机制)
- 异常处理机制
- 安全性:
- 字节码验证
- 无指针运算
- 安全管理器
- 多线程支持:内置Thread类
- 分布式支持:完善的网络库(java.net包)
- 动态性:反射机制支持运行时类型检查
2. Java和C++的区别有哪些?
特性 | Java | C++ |
---|---|---|
内存管理 | 自动垃圾回收 | 手动new/delete |
指针 | 无显式指针 | 支持指针操作 |
多重继承 | 通过接口实现 | 直接支持 |
平台依赖性 | 跨平台(JVM) | 平台相关 |
运行速度 | 相对较慢(JIT优化) | 直接编译为机器码 |
异常处理 | 强制处理受检异常 | 无异常类型限制 |
标准库 | 丰富的集合框架 | STL |
运算符重载 | 不支持 | 支持 |
预处理器 | 无 | 支持#define等 |
3. 什么是JDK、JRE和JVM?它们之间有什么关系?
JVM (Java Virtual Machine):
- 字节码执行引擎
- 包含类加载器、执行引擎、运行时数据区等
- 不同平台有不同实现(Windows/Linux/Mac)
JRE (Java Runtime Environment):
- = JVM + 核心类库(如java.lang, java.util等)
- 只能运行Java程序
JDK (Java Development Kit):
- = JRE + 开发工具(javac, javadoc, jdb等)
- 包含完整的Java开发环境
关系图示:
JDK ⊃ JRE ⊃ JVM
4. Java是编译型语言还是解释型语言?
Java采用混合模式:
-
编译阶段:
- 源码(.java) → 字节码(.class)
- 使用javac编译器
- 生成平台无关的中间代码
-
执行阶段:
- 解释执行:JVM逐行解释字节码
- JIT编译(Just-In-Time):
- 热点代码会被编译为本地机器码
- 显著提升性能(如HotSpot VM)
典型执行流程:
.java → (javac) → .class → (JVM) → 机器码
5. Java如何实现跨平台?
跨平台实现原理:
-
统一中间格式:
- 编译生成标准字节码(.class文件)
- 字节码与具体硬件/OS无关
-
JVM适配层:
- 各平台提供对应的JVM实现
- JVM负责将字节码转换为本地指令
-
分层抽象:
- 硬件层:x86/ARM等
- 系统层:Windows/Linux/macOS
- JVM层:统一字节码接口
- 应用层:Java程序
关键组件:
- 类加载器(加载字节码)
- 字节码验证器(安全检查)
- 解释器/JIT编译器(执行优化)
示例说明:
Windows平台:.class → windows-jvm → x86指令
Linux平台:同样的.class → linux-jvm → ARM指令
数据类型与变量面试题详解
1. Java有哪些基本数据类型?
Java的8种基本数据类型:
类型 | 大小 | 默认值 | 取值范围 | 包装类 |
---|---|---|---|---|
byte | 1字节 | 0 | -128 ~ 127 | Byte |
short | 2字节 | 0 | -32768 ~ 32767 | Short |
int | 4字节 | 0 | -2³¹ ~ 2³¹-1 | Integer |
long | 8字节 | 0L | -2⁶³ ~ 2⁶³-1 | Long |
float | 4字节 | 0.0f | ±1.4E-45 ~ ±3.4E38(6-7位有效数字) | Float |
double | 8字节 | 0.0d | ±4.9E-324 ~ ±1.7E308(15位有效数字) | Double |
char | 2字节 | ‘\u0000’ | 0 ~ 65535(Unicode字符) | Character |
boolean | 未定义 | false | true/false | Boolean |
注意:
boolean
在JVM层面用int(32位)或byte(8位)实现
2. int
和Integer
有什么区别?
对比维度 | int | Integer |
---|---|---|
类型 | 基本数据类型 | 包装类对象 |
内存分配 | 栈内存 | 堆内存 |
默认值 | 0 | null |
泛型支持 | 不能用于泛型 | 可用于泛型(如List<Integer> ) |
方法支持 | 无对象方法 | 提供parseInt() 等工具方法 |
缓存机制 | 无 | -128~127缓存(IntegerCache ) |
比较方式 | 直接比较值 | 需要用equals() 比较值 |
代码示例:
int a = 100;
Integer b = 100; // 自动装箱(实际调用Integer.valueOf(100))
Integer c = new Integer(100); // 显式创建新对象System.out.println(a == b); // true(自动拆箱比较值)
System.out.println(b == c); // false(比较对象地址)
System.out.println(b.equals(c)); // true(比较实际值)
3. 什么是自动装箱和拆箱?
自动装箱(Autoboxing):
基本类型 → 包装类型(编译器的语法糖)
int num = 42;
Integer boxedNum = num; // 等价于 Integer.valueOf(42)
自动拆箱(Unboxing):
包装类型 → 基本类型(编译器的语法糖)
Integer boxedNum = Integer.valueOf(100);
int unboxed = boxedNum; // 等价于 boxedNum.intValue()
重要特性:
- 缓存机制:-128~127的
Integer
对象会被缓存 - 性能影响:循环中频繁装箱拆箱会导致性能问题(创建多余对象)
4. String
、StringBuilder
和StringBuffer
的区别?
特性 | String | StringBuilder | StringBuffer |
---|---|---|---|
可变性 | 不可变(final char[]) | 可变 | 可变 |
线程安全 | 天生线程安全 | 非线程安全 | 线程安全(synchronized方法) |
性能 | 最低(每次修改创建新对象) | 最高 | 中等 |
使用场景 | 常量字符串操作 | 单线程字符串拼接 | 多线程字符串操作 |
代码示例:
String str = "Hello";
str += " World"; // 创建3个对象("Hello", " World", "Hello World")StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" World"); // 只操作同一个对象StringBuffer sbf = new StringBuffer();
sbf.append("Thread").append("Safe"); // 方法添加synchronized锁
5. ==
和equals()
的区别?
操作符/方法 | 比较内容 | 适用场景 |
---|---|---|
== | 内存地址比较 | 基本类型比较值,对象比较地址 |
equals() | 对象内容比较(可重写) | 对象的值比较 |
关键规则:
- 对于基本类型:
==
比较值
int x = 5, y = 5;System.out.println(x == y); // true
- 对于对象类型:
==
比较内存地址
String s1 = new String("abc");String s2 = new String("abc");System.out.println(s1 == s2); // false
equals()
默认实现与==
相同,但可被重写:
System.out.println(s1.equals(s2)); // true(String类重写了equals)
重要类别的equals实现:
String
:比较字符序列内容Integer
:比较int值- 自定义类:需重写equals()和hashCode()
面向对象编程(OOP)面试题详解
1. 面向对象的四大特性是什么?
- 封装(Encapsulation):
将数据和操作数据的方法绑定在一起,对外隐藏实现细节
示例:
public class Person {private String name; // 私有字段public String getName() { // 公开方法return this.name;}}
- 继承(Inheritance):
子类继承父类的特征和行为,实现代码复用
示例:
class Animal {}class Dog extends Animal {} // Dog继承Animal
-
多态(Polymorphism):
同一操作作用于不同对象产生不同行为
实现方式:- 方法重写(Override)
- 接口实现
- 抽象类和抽象方法
-
抽象(Abstraction):
提取关键特征,忽略非本质细节
实现方式:- 抽象类(abstract class)
- 接口(interface)
2. 什么是类和对象?
类(Class):
- 对象的蓝图/模板
- 包含:
- 属性(字段/成员变量)
- 行为(方法)
- 示例:
public class Student {String name; // 字段void study() { // 方法System.out.println("Studying...");}
}
对象(Object):
- 类的具体实例
- 特点:
- 占用独立内存空间
- 包含类定义的所有字段副本
- 示例:
Student s1 = new Student(); // s1是Student类的对象
s1.name = "张三";
s1.study();
3. 重载(Overload)和重写(Override)的区别?
对比维度 | 重载(Overload) | 重写(Override) |
---|---|---|
定义 | 同一类中同名不同参的方法 | 子类重新实现父类方法 |
参数要求 | 必须修改参数列表 | 必须保持相同参数列表 |
返回类型 | 可以不同 | 必须相同或为子类 |
访问修饰符 | 可以不同 | 不能比父类更严格 |
抛出异常 | 可以不同 | 不能抛出更宽泛的异常 |
调用时机 | 编译时确定 | 运行时确定 |
重载示例:
class Calculator {int add(int a, int b) { return a+b; }double add(double a, double b) { return a+b; } // 重载
}
重写示例:
class Animal {void makeSound() { System.out.println("Animal sound"); }
}
class Cat extends Animal {@Overridevoid makeSound() { System.out.println("Meow"); } // 重写
}
4. 抽象类和接口的区别?
对比维度 | 抽象类(abstract class) | 接口(interface) |
---|---|---|
定义关键字 | abstract | interface |
方法实现 | 可以有具体方法 | Java 8前只能有抽象方法 |
字段 | 可以有普通字段 | 默认public static final |
构造方法 | 可以有 | 不能有 |
多继承 | 单继承 | 多实现(implements多个接口) |
设计目的 | 代码复用 | 定义规范/契约 |
默认方法 | Java 8前无 | Java 8支持default方法 |
抽象类示例:
abstract class Shape {abstract void draw(); // 抽象方法void move() { // 具体方法System.out.println("Moving shape");}
}
接口示例(Java 8+):
interface Drawable {void draw(); // 抽象方法default void print() { // 默认方法System.out.println("Printing...");}static void help() { // 静态方法System.out.println("Help info");}
}
5. 什么是多态?如何实现多态?
多态类型:
- 编译时多态:方法重载
- 运行时多态:方法重写
实现方式:
- 继承 + 方法重写
- 接口实现
- 抽象类和抽象方法
典型示例:
class Animal {void sound() { System.out.println("Animal sound"); }
}
class Dog extends Animal {@Overridevoid sound() { System.out.println("Bark"); }
}
class Cat extends Animal {@Overridevoid sound() { System.out.println("Meow"); }
}// 测试多态
Animal myAnimal = new Dog();
myAnimal.sound(); // 输出"Bark"(运行时多态)myAnimal = new Cat();
myAnimal.sound(); // 输出"Meow"
6. 构造方法(Constructor)的特点和作用?
特点:
- 方法名与类名相同
- 无返回类型(连void都没有)
- 可以重载(多个不同参数的构造方法)
- 默认提供无参构造(如果没定义任何构造方法)
- 不能被static/final/abstract修饰
作用:
- 初始化对象状态
- 为对象字段赋初值
- 执行必要的启动操作
示例:
public class Person {String name;int age;// 无参构造public Person() {this.name = "Unknown";this.age = 0;}// 带参构造public Person(String name, int age) {this.name = name;this.age = age;}
}// 使用构造方法
Person p1 = new Person(); // 调用无参构造
Person p2 = new Person("Alice", 25); // 调用带参构造
7. this和super关键字的区别?
对比维度 | this | super |
---|---|---|
指向 | 当前对象实例 | 父类对象引用 |
用途 | 1. 解决局部变量与字段同名问题 2. 调用本类其他构造方法(this()) | 1. 访问父类成员 2. 调用父类构造方法(super()) |
调用限制 | 必须在构造方法第一行 | 必须在子类构造方法第一行 |
this示例:
class Student {String name;Student(String name) {this.name = name; // 区分字段和参数}Student() {this("Unknown"); // 调用其他构造方法}
}
super示例:
class Parent {String familyName = "Smith";
}
class Child extends Parent {String fullName;Child(String firstName) {super(); // 调用父类构造方法(可省略)this.fullName = firstName + " " + super.familyName;}
}
8. 什么是静态变量和静态方法?
静态变量(类变量):
- 用
static
修饰 - 属于类而非对象
- 所有实例共享同一份拷贝
- 通过
类名.变量名
访问
静态方法(类方法):
- 用
static
修饰 - 不能访问非静态成员
- 不能使用
this
/super
- 通过
类名.方法名()
调用
示例:
class Counter {static int count = 0; // 静态变量int instanceCount = 0; // 实例变量Counter() {count++;instanceCount++;}static void printCount() { // 静态方法System.out.println("Total count: " + count);// System.out.println(instanceCount); // 错误!不能访问非静态成员}
}// 使用
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter.printCount(); // 输出"Total count: 2"
System.out.println(c1.instanceCount); // 输出1
9. final、finally和finalize的区别?
关键字 | 用途 | 作用场景 |
---|---|---|
final | 修饰符 | 1. 修饰类:不可继承 2. 修饰方法:不可重写 3. 修饰变量:常量(不可修改) |
finally | 异常处理块 | try-catch-finally结构中,无论是否发生异常都会执行的代码块 |
finalize | Object类的方法 | 垃圾回收前调用的清理方法(不推荐使用) |
final示例:
final class Immutable { // 不可继承的类final int VALUE = 100; // 常量final void method() { // 不可重写的方法// ...}
}
finally示例:
try {// 可能抛出异常的代码
} catch (Exception e) {// 异常处理
} finally {// 一定会执行的代码(如释放资源)System.out.println("Cleanup");
}
finalize示例(已废弃):
@Override
protected void finalize() throws Throwable {try {// 资源清理} finally {super.finalize();}
}
异常处理面试题详解
1. Java异常体系结构是怎样的?
Java异常类的继承体系:
Throwable (所有错误和异常的基类)
├── Error (严重错误,程序无法处理)
│ ├── VirtualMachineError (JVM错误)
│ │ ├── OutOfMemoryError
│ │ └── StackOverflowError
│ └── LinkageError (类链接错误)
└── Exception (可以捕获处理的异常)
├── RuntimeException (运行时异常)
│ ├── NullPointerException
│ ├── IndexOutOfBoundsException
│ └── IllegalArgumentException
└── 其他非运行时异常
├── IOException
│ ├── FileNotFoundException
│ └── EOFException
└── SQLException
关键特点:
Error
表示系统级错误,通常不应捕获Exception
分为检查型异常和非检查型异常- 自定义异常应继承
Exception
或RuntimeException
2. Error和Exception的区别?
对比维度 | Error | Exception |
---|---|---|
严重程度 | 严重系统错误 | 程序可处理的异常情况 |
是否可恢复 | 通常不可恢复 | 多数情况下可以恢复 |
是否强制捕获 | 不需要捕获 | 检查型异常必须捕获或声明 |
来源 | JVM或底层系统资源问题 | 程序逻辑问题或外部条件 |
典型例子 | OutOfMemoryError | NullPointerException |
StackOverflowError | FileNotFoundException |
Error示例:
// 递归调用导致栈溢出
public static void infiniteRecursion() {infiniteRecursion();
}
// 调用后会抛出StackOverflowError
Exception示例:
try {FileInputStream fis = new FileInputStream("nonexistent.txt");
} catch (FileNotFoundException e) {System.out.println("文件未找到");
}
3. 受检异常和非受检异常的区别?
对比维度 | 受检异常(Checked Exception) | 非受检异常(Unchecked Exception) |
---|---|---|
继承关系 | 继承自Exception但不包括RuntimeException | 继承自RuntimeException或Error |
处理要求 | 必须捕获或声明抛出 | 不强制要求处理 |
编译检查 | 编译器会检查 | 编译器不检查 |
设计目的 | 可预期的异常情况 | 程序错误或不可控情况 |
典型例子 | IOException | NullPointerException |
SQLException | ArrayIndexOutOfBoundsException |
受检异常示例:
// 必须处理(捕获或声明throws)
try {Class.forName("com.example.NonExistClass");
} catch (ClassNotFoundException e) {e.printStackTrace();
}
非受检异常示例:
// 可以不处理,但会导致程序终止
String str = null;
System.out.println(str.length()); // 抛出NullPointerException
4. try-catch-finally的执行顺序?
标准执行流程:
- 执行try块中的代码
- 如果发生异常:
a. 跳转到匹配的catch块
b. 执行catch块中的异常处理 - 无论是否发生异常:
a. 都会执行finally块中的代码 - 继续执行后续代码
特殊场景:
- try或catch中有return:先执行finally再return
- finally中有return:会覆盖try/catch中的return值
- System.exit():会立即终止程序,不执行finally
执行顺序示例:
public static int testFinally() {try {System.out.println("try block");return 1;} catch (Exception e) {System.out.println("catch block");return 2;} finally {System.out.println("finally block");// return 3; // 如果取消注释,将返回3而不是1}
}
// 输出顺序:
// try block
// finally block
// 返回值为1
5. throw和throws的区别?
对比维度 | throw | throws |
---|---|---|
语法位置 | 方法体内使用 | 方法声明处使用 |
作用 | 主动抛出异常对象 | 声明方法可能抛出的异常类型 |
抛出数量 | 每次只能抛出一个异常对象 | 可以声明多个异常类型 |
处理要求 | 必须显式创建异常对象 | 只是声明,不创建异常对象 |
使用场景 | 在方法内部抛出具体异常 | 在方法签名中声明可能抛出的异常 |
throw示例:
public void validateAge(int age) {if (age < 0) {throw new IllegalArgumentException("年龄不能为负数");}
}
throws示例:
public void readFile() throws FileNotFoundException, IOException {FileInputStream fis = new FileInputStream("test.txt");// 其他IO操作
}
组合使用示例:
public void transferMoney(double amount) throws InsufficientBalanceException {if (balance < amount) {throw new InsufficientBalanceException("余额不足");}// 转账逻辑...
}
最佳实践:
- 受检异常必须在方法签名中声明
- 不要滥用throws声明所有异常
- 自定义异常应提供有意义的错误信息
- 在catch块中通常应该记录异常日志
集合框架面试题详解
1. Java集合框架的主要接口和类有哪些?
核心接口:
Collection
(所有集合的根接口)List
(有序可重复集合)Set
(无序不可重复集合)Queue
(队列)
Map
(键值对集合)
常用实现类:
-
List实现类:
ArrayList
(基于动态数组)LinkedList
(基于双向链表)Vector
(线程安全版ArrayList)Stack
(继承自Vector)
-
Set实现类:
HashSet
(基于HashMap)LinkedHashSet
(保持插入顺序)TreeSet
(基于红黑树,有序)
-
Map实现类:
HashMap
(基于哈希表)LinkedHashMap
(保持插入顺序)TreeMap
(基于红黑树,按键排序)Hashtable
(线程安全,已过时)ConcurrentHashMap
(线程安全优化版)
-
Queue实现类:
PriorityQueue
(优先级队列)ArrayDeque
(双端队列)
2. List、Set和Map的区别?
特性 | List | Set | Map |
---|---|---|---|
元素重复性 | 允许重复元素 | 不允许重复元素 | Key不允许重复,Value允许重复 |
有序性 | 保持插入顺序 | 通常无序(HashSet) | 通常无序(HashMap) |
实现基础 | 动态数组/链表 | 基于Map实现 | 哈希表/红黑树 |
访问方式 | 通过索引访问 | 只能迭代访问 | 通过Key访问Value |
典型实现 | ArrayList, LinkedList | HashSet, TreeSet | HashMap, TreeMap |
null支持 | 允许null元素 | 最多一个null(HashSet) | HashMap允许一个null key |
3. ArrayList和LinkedList的区别?
对比维度 | ArrayList | LinkedList |
---|---|---|
底层结构 | 动态数组 | 双向链表 |
内存占用 | 连续内存,占用较少 | 节点存储,占用更多 |
访问效率 | O(1)随机访问 | O(n)顺序访问 |
插入删除 | 尾部操作O(1),中间O(n) | 头尾操作O(1),中间O(n) |
扩容机制 | 默认扩容50% | 无需扩容 |
适用场景 | 频繁随机访问 | 频繁插入删除 |
代码示例:
// ArrayList访问示例
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("A");
arrayList.get(0); // O(1)快速访问// LinkedList插入示例
LinkedList<String> linkedList = new LinkedList<>();
linkedList.addFirst("B"); // O(1)头部插入
4. HashMap的工作原理?
核心机制:
-
哈希计算:
- 调用key的
hashCode()
计算哈希值 - 通过扰动函数减少哈希冲突
- 公式:
index = (n - 1) & hash
(n为数组长度)
- 调用key的
-
存储结构:
- Java 8前:数组+链表
- Java 8+:数组+链表/红黑树(链表长度>8时转换)
-
put流程:
- 计算key的hash值
- 如果数组为空,初始化(默认16)
- 如果对应位置为空,直接插入
- 如果存在冲突:
- 链表:遍历查找,存在则覆盖,否则尾插
- 红黑树:树形查找插入
- 超过阈值(默认0.75)则扩容2倍
-
get流程:
- 计算key的hash值
- 定位数组下标
- 遍历链表/红黑树查找
关键参数:
- 默认容量:16
- 负载因子:0.75
- 树化阈值:8
- 解树化阈值:6
5. HashMap和HashTable的区别?
对比维度 | HashMap | HashTable |
---|---|---|
线程安全 | 非线程安全 | 线程安全(synchronized方法) |
性能 | 更高 | 较低 |
null支持 | 允许一个null key和多个null值 | 不允许null键值 |
继承关系 | 继承AbstractMap | 继承Dictionary |
迭代器 | Fail-Fast | Fail-Safe |
初始容量 | 16 | 11 |
扩容机制 | 2倍扩容 | 2倍+1扩容 |
HashMap示例:
HashMap<String, Integer> map = new HashMap<>();
map.put(null, 1); // 允许
map.put("key", null); // 允许
HashTable示例:
Hashtable<String, Integer> table = new Hashtable<>();
// table.put(null, 1); // 抛出NullPointerException
// table.put("key", null); // 抛出NullPointerException
6. ConcurrentHashMap如何保证线程安全?
Java 7 及之前的 ConcurrentHashMap 实现
核心设计:
- 分段锁(Segment)机制:
- 将整个Map分成多个Segment(默认16个)
- 每个Segment继承ReentrantLock
- 不同Segment可并发操作
数据结构:
static final class Segment<K,V> extends ReentrantLock {transient volatile HashEntry<K,V>[] table;// 其他字段...
}static final class HashEntry<K,V> {final K key;volatile V value;volatile HashEntry<K,V> next;
}
主要特点:
-
并发度(Concurrency Level):
- 可指定Segment数量(默认16)
- 实际并发量受限于Segment数量
-
锁粒度:
- 只锁定单个Segment
- 不同Segment的操作可并行
-
读操作:
- 完全无锁(volatile变量保证可见性)
- 弱一致性迭代器
-
扩容:
- 单个Segment独立扩容
- 不影响其他Segment访问
代码示例:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1); // 只锁定对应Segment
缺点:
- 内存占用较大(每个Segment都有独立数据结构)
- 并发度固定,无法动态扩展
- 某些场景下仍存在竞争
Java 8 及之后的 ConcurrentHashMap 实现
核心改进:
- 废弃分段锁,改用:
- CAS + synchronized 锁单个Node
- 更细粒度的锁控制
数据结构:
- 数组 + 链表/红黑树(类似HashMap)
- Node定义:
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;volatile V val;volatile Node<K,V> next;}
主要优化:
-
锁粒度更细:
- 只锁定链表头节点/树根节点
- 并发度理论上等于桶数组大小
-
CAS操作:
- 无竞争时使用CAS更新
- 减少锁的使用
-
扩容机制:
- 多线程协同扩容
- 使用ForwardingNode标记迁移状态
-
计数机制:
- 采用LongAdder思想
- 分散计数热点
关键方法实现:
-
putVal()流程:
- 计算hash值
- 如果表为空则初始化
- 如果桶为空则CAS插入
- 如果存在hash冲突:
- 链表:synchronized锁定头节点
- 红黑树:锁定树根节点
- 超过阈值则扩容
-
get()流程:
- 根据hash定位到桶
- 遍历链表/红黑树查找
- 完全无锁(依赖volatile读)
代码示例:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.computeIfAbsent("key", k -> 1); // 原子操作
性能对比:
操作 | Java 7 | Java 8+ |
---|---|---|
低并发put | 中等(分段锁开销) | 高(CAS优化) |
高并发put | 中等(受限于并发度) | 高(更细粒度锁) |
get | 高(无锁) | 高(无锁) |
内存占用 | 较高(分段结构) | 较低(统一结构) |
扩容效率 | 分段独立扩容 | 多线程协同扩容 |
重要内部类变化
Java 7:
- Segment:继承ReentrantLock
- HashEntry:链表节点
Java 8:
- Node:基础链表节点
- TreeNode:红黑树节点
- TreeBin:红黑树容器
- ForwardingNode:扩容转发节点
- ReservationNode:占位节点
并发控制机制对比
Java 7:
- 依赖Segment锁
- put操作必须获取锁
- size()方法较复杂(需统计各Segment)
Java 8:
- CAS + synchronized
- put操作可能无锁(CAS成功时)
- size()使用CounterCell减少竞争
为什么Java 8要重新设计?
-
锁粒度问题:
- Segment数量固定导致并发度受限
- 实际场景中不同Segment的负载不均衡
-
内存占用:
- Segment结构带来额外内存开销
-
现代CPU架构:
- CAS操作在多数场景下比锁更高效
- 减少线程上下文切换
-
技术演进:
- JVM对synchronized的优化(偏向锁、轻量级锁)
- 借鉴了LongAdder的分段计数思想
典型使用场景建议
Java 7版本:
- 读多写少的并发场景
- 键的hash分布均匀的情况
Java 8+版本:
- 高并发写入场景
- 数据量大的情况
- 需要原子复合操作(computeIfAbsent等)
通用最佳实践:
- 合理设置初始容量(避免频繁扩容)
- 避免在遍历时修改Map
- 使用并发安全的方法(如computeIfAbsent)
- 监控并发性能指标
多线程面试题详解
1. 线程和进程的区别?
对比维度 | 进程 | 线程 |
---|---|---|
资源分配 | 操作系统分配资源的基本单位 | CPU调度的基本单位 |
内存空间 | 每个进程有独立内存空间 | 同一进程的线程共享内存 |
创建开销 | 大(需要分配独立资源) | 小(共享进程资源) |
通信方式 | 管道、消息队列、共享内存等 | 直接读写共享变量 |
稳定性 | 进程崩溃不影响其他进程 | 线程崩溃可能导致整个进程退出 |
并发性 | 进程间并发 | 线程间并发 |
数量限制 | 通常几十个 | 通常数百到数千个 |
关键点:
- 进程是程序的一次执行,线程是进程内的执行单元
- Java中无法直接操作进程,只能通过
Runtime.exec()
等有限方式
2. 创建线程的几种方式?
1. 继承Thread类
class MyThread extends Thread {@Overridepublic void run() {System.out.println("Thread running");}
}
// 使用
new MyThread().start();
2. 实现Runnable接口
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("Runnable running");}
}
// 使用
new Thread(new MyRunnable()).start();
3. 实现Callable接口(带返回值)
class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {return "Callable result";}
}
// 使用
FutureTask<String> task = new FutureTask<>(new MyCallable());
new Thread(task).start();
String result = task.get(); // 获取返回值
4. 使用线程池(推荐)
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.execute(() -> {System.out.println("Pool thread running");
});
executor.shutdown();
3. Runnable和Callable的区别?
对比维度 | Runnable | Callable |
---|---|---|
返回值 | 无 | 有 |
异常处理 | 不能抛出受检异常 | 可以抛出异常 |
使用场景 | 简单异步任务 | 需要返回结果的任务 |
引入版本 | Java 1.0 | Java 5.0 |
函数式接口 | 是 | 是 |
执行方式 | Thread或Executor.execute() | ExecutorService.submit() |
4. 线程的生命周期(状态)有哪些?
Java线程的6种状态(Thread.State枚举):
-
NEW(新建)
- 线程被创建但未调用start()
-
RUNNABLE(可运行)
- 包括操作系统中的Ready和Running状态
- 可能正在执行或等待CPU时间片
-
BLOCKED(阻塞)
- 等待获取监视器锁(synchronized)
- 只有这种状态会竞争锁
-
WAITING(等待)
- 无限期等待其他线程显式唤醒
- 通过以下方法进入:
- Object.wait()
- Thread.join()
- LockSupport.park()
-
TIMED_WAITING(超时等待)
- 带时间限制的等待
- 通过以下方法进入:
- Thread.sleep(long)
- Object.wait(long)
- Thread.join(long)
-
TERMINATED(终止)
- 线程执行完毕
状态转换图:
NEW → start() → RUNNABLE
RUNNABLE → 获取锁 → BLOCKED
RUNNABLE → wait()/join() → WAITING
RUNNABLE → sleep()/wait(n) → TIMED_WAITING
各种等待状态 → 条件满足 → RUNNABLE
RUNNABLE → 执行完成 → TERMINATED
5. sleep()和wait()的区别?
对比维度 | sleep() | wait() |
---|---|---|
所属类 | Thread的静态方法 | Object的实例方法 |
锁释放 | 不释放锁 | 释放锁 |
唤醒条件 | 时间到期 | notify()/notifyAll()或超时 |
使用位置 | 任意位置 | 必须在同步块内(持有锁时) |
异常处理 | 需要处理InterruptedException | 需要处理InterruptedException |
精度控制 | 支持纳秒参数 | 支持纳秒参数 |
6. synchronized关键字的用法?
三种使用方式:
- 实例方法同步
public synchronized void method() {// 锁定当前实例
}
- 静态方法同步
public static synchronized void staticMethod() {// 锁定当前类的Class对象
}
- 同步代码块
public void block() {synchronized(obj) { // 锁定指定对象// 临界区代码}
}
锁的特性:
- 可重入性:同一线程可重复获取同一把锁
- 非公平性:不保证等待线程的获取顺序
- 独占性:同一时间只有一个线程能持有锁
- 不可中断性:等待锁的线程不能被中断
锁升级过程(Java 6+优化):
无锁 → 偏向锁 → 轻量级锁(CAS) → 重量级锁
7. volatile关键字的作用?
两大核心作用:
-
可见性保证
- 写操作立即刷新到主内存
- 读操作直接从主内存读取
-
禁止指令重排序
- 通过内存屏障实现
- 保证happens-before关系
适用场景:
- 状态标志位(如while(!stop))
- 单例模式的双重检查锁定
- 一次性安全发布
不适用场景:
- 非原子操作的复合操作(如i++)
- 需要互斥访问的临界区
示例:
class VolatileExample {private volatile boolean flag = false;public void writer() {flag = true; // 写操作对其他线程立即可见}public void reader() {while(!flag) {} // 能及时看到flag变化System.out.println("Flag changed");}
}
8. 什么是线程池?为什么要用线程池?
线程池核心组件:
- 工作线程(Worker):实际执行任务的线程
- 任务队列(BlockingQueue):存放待执行任务
- 线程工厂(ThreadFactory):创建新线程
- 拒绝策略(RejectedExecutionHandler):处理任务过多情况
线程池优势:
- 降低资源消耗:复用已创建的线程
- 提高响应速度:任务到达可直接执行
- 提高线程管理性:统一分配、调优和监控
- 防止资源耗尽:通过队列和拒绝策略
ThreadPoolExecutor核心参数:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数int maximumPoolSize, // 最大线程数long keepAliveTime, // 空闲线程存活时间TimeUnit unit, // 时间单位BlockingQueue<Runnable> workQueue, // 任务队列ThreadFactory threadFactory, // 线程工厂RejectedExecutionHandler handler // 拒绝策略
)
四种常用线程池:
1. newFixedThreadPool:固定大小线程池
2. newCachedThreadPool:可扩容线程池
3. newSingleThreadExecutor:单线程池
4. newScheduledThreadPool:定时任务线程池
执行流程:
- 提交任务
- 如果核心线程未满,创建新线程执行
- 如果核心线程已满,加入任务队列
- 如果队列已满且线程数未达最大值,创建新线程
- 如果队列已满且线程数已达最大值,执行拒绝策略
拒绝策略:
- AbortPolicy(默认):抛出RejectedExecutionException
- CallerRunsPolicy:由调用线程直接执行
- DiscardPolicy:直接丢弃任务
- DiscardOldestPolicy:丢弃队列最老的任务
示例代码:
ExecutorService pool = Executors.newFixedThreadPool(5);
pool.execute(() -> {System.out.println(Thread.currentThread().getName() + " executing task");
});
pool.shutdown();
IO与NIO面试题详解
1. Java IO流分为哪几种?
按数据流向分类:
- 输入流(
InputStream
/Reader
) - 输出流(
OutputStream
/Writer
)
按数据类型分类:
-
字节流(8位字节):
InputStream
(抽象基类)FileInputStream
ByteArrayInputStream
BufferedInputStream
ObjectInputStream
OutputStream
(抽象基类)FileOutputStream
ByteArrayOutputStream
BufferedOutputStream
ObjectOutputStream
-
字符流(16位Unicode):
Reader
(抽象基类)FileReader
CharArrayReader
BufferedReader
InputStreamReader
Writer
(抽象基类)FileWriter
CharArrayWriter
BufferedWriter
OutputStreamWriter
按功能分类:
- 节点流(直接操作数据源)
- 处理流(包装其他流,提供增强功能)
典型类结构:
InputStream
├── FileInputStream
├── FilterInputStream
│ ├── BufferedInputStream
│ └── DataInputStream
└── ObjectInputStream
Reader
├── FileReader
├── BufferedReader
└── InputStreamReader
2. 字节流和字符流的区别?
对比维度 | 字节流 | 字符流 |
---|---|---|
基本单位 | 8位字节 | 16位Unicode字符 |
处理类型 | 二进制数据(如图片、音频) | 文本数据 |
基类 | InputStream /OutputStream | Reader /Writer |
缓冲区 | 通常无内置缓冲 | 通常内置缓冲 |
编码转换 | 不处理编码 | 自动处理字符编码转换 |
典型场景 | 文件复制、网络数据传输 | 文本文件读写 |
性能 | 处理二进制更快 | 处理文本更方便 |
转换流示例:
// 字节流转字符流(指定编码)
InputStreamReader isr = new InputStreamReader(new FileInputStream("file.txt"), "UTF-8");
// 字符流转字节流
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("file.txt"), "GBK");
3. BIO、NIO和AIO的区别?
对比维度 | BIO (Blocking IO) | NIO (Non-blocking IO) | AIO (Asynchronous IO) |
---|---|---|---|
IO模型 | 同步阻塞 | 同步非阻塞 | 异步非阻塞 |
数据准备 | 线程阻塞等待数据就绪 | 轮询检查数据就绪状态 | OS通知数据就绪 |
处理方式 | 面向流(Stream) | 面向缓冲区(Buffer) | 面向事件和回调 |
线程开销 | 每个连接需要独立线程 | 单线程处理多连接 | 回调机制减少线程占用 |
吞吐量 | 低(线程上下文切换开销) | 高(减少线程数量) | 最高(无等待时间) |
适用场景 | 连接数少的固定架构 | 连接数多且连接时间短 | 连接数多且连接时间长 |
JDK支持 | Java 1.0 | Java 1.4 | Java 7 |
BIO示例:
ServerSocket server = new ServerSocket(8080);
while(true) {Socket client = server.accept(); // 阻塞new Thread(() -> handleClient(client)).start();
}
NIO示例:
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
while(true) {int ready = selector.select(); // 非阻塞Set<SelectionKey> keys = selector.selectedKeys();// 处理就绪的通道
}
4. NIO的核心组件有哪些?
1. Buffer(缓冲区)
- 数据容器,提供结构化访问
- 核心实现类:
ByteBuffer
(最常用)CharBuffer
IntBuffer
- 等基本类型Buffer
- 重要属性:
- capacity(容量)
- position(当前位置)
- limit(读写限制)
- mark(标记位置)
2. Channel(通道)
- 双向数据传输管道
- 主要实现:
FileChannel
(文件IO)SocketChannel
(TCP)ServerSocketChannel
(TCP服务端)DatagramChannel
(UDP)
- 特点:
- 可异步读写
- 必须配合Buffer使用
3. Selector(选择器)
- 多路复用器,单线程管理多个Channel
- 核心方法:
open()
:创建Selectorselect()
:监控注册的ChannelselectedKeys()
:获取就绪的SelectionKey集合
- 事件类型:
OP_ACCEPT
:连接接受OP_CONNECT
:连接就绪OP_READ
:读就绪OP_WRITE
:写就绪
4. Charset(字符集)
- 处理字符编码/解码
- 常用字符集:
StandardCharsets.UTF_8
StandardCharsets.ISO_8859_1
StandardCharsets.US_ASCII
NIO文件复制示例:
FileChannel inChannel = new FileInputStream("source.txt").getChannel();
FileChannel outChannel = new FileOutputStream("target.txt").getChannel();ByteBuffer buffer = ByteBuffer.allocate(1024);
while(inChannel.read(buffer) != -1) {buffer.flip(); // 切换为读模式outChannel.write(buffer);buffer.clear(); // 清空缓冲区
}
NIO网络编程示例:
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);while(true) {selector.select();Set<SelectionKey> keys = selector.selectedKeys();Iterator<SelectionKey> iter = keys.iterator();while(iter.hasNext()) {SelectionKey key = iter.next();if(key.isAcceptable()) {// 处理新连接SocketChannel client = server.accept();client.configureBlocking(false);client.register(selector, SelectionKey.OP_READ);} else if(key.isReadable()) {// 处理读事件SocketChannel client = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);client.read(buffer);// 处理数据...}iter.remove();}
}
反射与泛型面试题详解
1. 什么是反射?反射的应用场景?
反射(Reflection)定义:
- 在运行时动态获取类的完整结构信息
- 动态操作类属性、方法和构造器
- 核心类:
Class
、Field
、Method
、Constructor
主要功能:
- 运行时获取类的修饰符、父类、接口、注解等信息
- 运行时构造任意类的对象
- 运行时调用任意对象的方法
- 运行时访问/修改字段值(包括private字段)
- 动态创建和操作数组
应用场景:
- 框架开发(Spring的IoC容器)
- 动态代理(AOP实现)
- 注解处理器
- 通用工具类(如BeanUtils)
- 序列化/反序列化
- IDE的代码提示
示例代码:
// 获取类信息
Class<?> clazz = Class.forName(“java.lang.String”);
Field[] fields = clazz.getDeclaredFields();
// 创建实例
Constructor<?> constructor = clazz.getConstructor(String.class);
Object str = constructor.newInstance(“Hello”);
// 调用方法
Method method = clazz.getMethod(“length”);
int len = (int) method.invoke(str);
2. 如何获取Class对象?
4种获取方式:
-
类名.class(最安全高效)
Class stringClass = String.class; -
对象.getClass()
String s = “”;
Class<?> clazz = s.getClass(); -
Class.forName()(最灵活)
Class<?> clazz = Class.forName(“java.lang.String”); -
类加载器.loadClass()
Class<?> clazz = MyClass.class.getClassLoader().loadClass(“com.example.MyClass”);
区别对比:
方式 | 是否初始化类 | 适用场景 |
---|---|---|
类名.class | 否 | 编译时已知类名 |
getClass() | 是 | 已有对象实例 |
Class.forName() | 是 | 动态加载类(如JDBC驱动) |
ClassLoader.loadClass() | 否 | 需要控制类加载过程 |
3. 什么是泛型?泛型擦除是什么意思?
泛型(Generics)定义:
- 参数化类型,将类型作为参数
- 主要作用:
- 类型安全(编译时检查)
- 消除强制类型转换
- 代码复用(通用算法)
泛型擦除(Type Erasure):
- Java编译器在编译时去除泛型类型信息
- 运行时只保留原始类型(Raw Type)
- 所有泛型类型最终都会被替换为Object或上界类型
擦除规则:
- 无界泛型:
List<T>
→List
- 有界泛型:
List<T extends Number>
→List<Number>
- 泛型方法:类型参数会被擦除到第一个边界
示例说明:
// 编译前
List list = new ArrayList<>();
list.add(“hello”);
String s = list.get(0);
// 编译后(擦除后)
List list = new ArrayList();
list.add(“hello”);
String s = (String) list.get(0); // 强制转换
Java 8新特性面试题详解
1. Lambda表达式的作用?
核心作用:
- 简化匿名内部类的写法
- 实现函数式编程风格
- 使代码更简洁、可读性更强
语法结构:
(parameters) -> expression
或
(parameters) -> { statements; }
特点:
- 可选类型声明:编译器可推断参数类型
- 可选参数圆括号:单参数时可省略
- 可选大括号:单条语句时可省略
- 可选return关键字:单条表达式时自动返回
使用示例:
// 替代匿名类
Runnable r1 = new Runnable() {@Overridepublic void run() {System.out.println("Old way");}
};Runnable r2 = () -> System.out.println("Lambda way");// 集合操作
list.forEach(item -> System.out.println(item));
2. Stream API的常用方法有哪些?
常用中间操作:
filter(Predicate)
:过滤元素map(Function)
:元素转换flatMap(Function)
:扁平化流distinct()
:去重sorted()
:排序peek(Consumer)
:查看元素但不修改
常用终端操作:
forEach(Consumer)
:遍历collect(Collector)
:转换为集合toArray()
:转换为数组reduce()
:归约操作count()
:计数anyMatch()
/allMatch()
/noneMatch()
:匹配检查findFirst()
/findAny()
:查找元素
示例代码:
List<String> result = list.stream().filter(s -> s.length() > 3).map(String::toUpperCase).sorted().collect(Collectors.toList());long count = list.stream().distinct().count();
3. 什么是函数式接口?常见的函数式接口有哪些?
函数式接口定义:
- 只有一个抽象方法的接口
- 可用
@FunctionalInterface
注解标记 - Lambda表达式的目标类型
四大核心函数式接口:
-
Consumer
- 消费型:接受输入无返回
- 方法:
void accept(T t)
- 示例:
list.forEach(System.out::println)
-
Supplier
- 供给型:无输入有返回
- 方法:
T get()
- 示例:
() -> new Random().nextInt()
-
Function<T,R>
- 函数型:接受T返回R
- 方法:
R apply(T t)
- 示例:
String::length
-
Predicate
- 断言型:接受T返回boolean
- 方法:
boolean test(T t)
- 示例:
s -> s.startsWith("A")
其他常用接口:
UnaryOperator<T>
:一元操作(继承Function)BinaryOperator<T>
:二元操作BiFunction<T,U,R>
:双参数函数BiConsumer<T,U>
:双参数消费者
4. Optional类的作用?
设计目的:
- 优雅处理
null
值问题 - 避免
NullPointerException
- 明确表示"值可能不存在"的语义
核心方法:
-
创建:
Optional.of(value)
(非null值)Optional.ofNullable(value)
(可null值)Optional.empty()
(空实例)
-
值处理:
get()
:获取值(不安全)orElse(default)
:获取值或默认值orElseGet(supplier)
:获取值或计算默认值orElseThrow(exceptionSupplier)
:获取值或抛异常
-
条件处理:
isPresent()
:判断值是否存在ifPresent(consumer)
:值存在时执行操作filter(predicate)
:过滤值map(function)
:值转换
使用示例:
// 传统null检查
if(user != null) {Address address = user.getAddress();if(address != null) {return address.getStreet();}
}
return "unknown";// Optional方式
return Optional.ofNullable(user).map(User::getAddress).map(Address::getStreet).orElse("unknown");
最佳实践:
- 不要用Optional作为方法参数
- 不要用Optional.get()而不检查isPresent()
- 优先使用orElse/orElseGet替代get()
- 集合返回空集合而非Optional
JVM基础面试题详解
1. JVM内存结构是怎样的?
JVM内存主要分区:
-
程序计数器(PC Register)
- 线程私有
- 记录当前线程执行的字节码行号
- 唯一不会OOM的区域
-
Java虚拟机栈(Java Stack)
- 线程私有
- 存储栈帧(方法调用的局部变量、操作数栈等)
- 可能抛出StackOverflowError/OutOfMemoryError
-
本地方法栈(Native Stack)
- 线程私有
- 为Native方法服务
-
堆(Heap)
- 线程共享
- 存储对象实例和数组
- GC主要工作区域
- 分代设计(新生代/老年代)
-
方法区(Method Area)
- 线程共享
- 存储类信息、常量、静态变量等
- JDK8后由元空间(Metaspace)实现
-
运行时常量池(Runtime Constant Pool)
- 方法区的一部分
- 存储编译期生成的字面量和符号引用
内存结构图示:
[JVM内存结构]
线程私有区:
- 程序计数器
- Java栈
- 本地方法栈
线程共享区:
- 堆
- 方法区
- 运行时常量池
2. 什么是垃圾回收(GC)?常见的垃圾回收算法有哪些?
GC核心概念:
- 自动管理堆内存
- 识别并回收不再使用的对象
- 防止内存泄漏
判断对象可回收的算法:
-
引用计数法:
- 对象被引用时计数器+1
- 为0时回收
- 问题:循环引用无法处理
-
可达性分析(主流):
- 从GC Roots对象开始遍历引用链
- 不可达对象判定为可回收
- GC Roots包括:
- 虚拟机栈引用的对象
- 方法区静态属性引用的对象
- 方法区常量引用的对象
- Native方法引用的对象
垃圾回收算法:
-
标记-清除(Mark-Sweep)
- 步骤:标记可回收对象 → 清除
- 问题:内存碎片
-
复制算法(Copying)
- 将存活对象复制到另一半空间
- 适用场景:新生代(Edem:Survivor=8:1)
- 优点:无碎片
- 缺点:空间利用率低
-
标记-整理(Mark-Compact)
- 步骤:标记 → 存活对象向一端移动 → 清理边界
- 适用场景:老年代
- 优点:无碎片
- 缺点:移动成本高
-
分代收集(Generational Collection)
- 新生代:复制算法
- 老年代:标记-清除/标记-整理
- 现代JVM主流策略
GC触发条件:
- Minor GC:Eden区满
- Full GC:
- 老年代空间不足
- 方法区空间不足
- System.gc()调用
3. 强引用、软引用、弱引用和虚引用的区别?
引用类型 | 回收时机 | 用途 | 实现类 |
---|---|---|---|
强引用 | 永不回收(即使OOM) | 普通对象引用 | 默认 |
软引用 | 内存不足时回收 | 内存敏感缓存 | SoftReference |
弱引用 | 下次GC时回收 | 规范映射/临时缓存 | WeakReference |
虚引用 | 随时可能回收 | 对象回收跟踪 | PhantomReference |
代码示例:
// 强引用
Object obj = new Object();// 软引用
SoftReference<Object> softRef = new SoftReference<>(new Object());// 弱引用
WeakReference<Object> weakRef = new WeakReference<>(new Object());// 虚引用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
引用队列(ReferenceQueue):
- 配合软/弱/虚引用使用
- 被引用对象回收后,引用对象入队
- 用于执行后续清理操作
4. 类加载过程是怎样的?
类加载的生命周期:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
详细过程:
-
加载:
- 通过全限定名获取二进制字节流
- 将字节流转化为方法区的运行时数据结构
- 在堆中生成Class对象作为访问入口
-
验证:
- 文件格式验证(魔数等)
- 元数据验证(语义分析)
- 字节码验证(StackMapTable)
- 符号引用验证
-
准备:
- 为类变量(static)分配内存
- 设置初始值(零值)
- final static变量直接赋实际值
-
解析:
- 将符号引用转为直接引用
- 类/接口解析
- 字段解析
- 方法解析
-
初始化:
- 执行类构造器()
- 父类先初始化
- 线程安全执行
类加载器:
-
启动类加载器(Bootstrap):
- 加载JAVA_HOME/lib下的核心类
- 由C++实现,无Java对应类
-
扩展类加载器(Extension):
- 加载JAVA_HOME/lib/ext下的类
- Java实现,sun.misc.Launcher$ExtClassLoader
-
应用类加载器(Application):
- 加载classpath下的类
- Java实现,sun.misc.Launcher$AppClassLoader
-
自定义类加载器:
- 继承ClassLoader实现
双亲委派模型:
- 子加载器先委托父加载器加载
- 父加载器无法完成时子加载器尝试
- 防止核心类被篡改
- 避免重复加载
打破双亲委派的场景:
- SPI服务加载(如JDBC)
- OSGi模块化
- 热部署实现
类初始化触发条件:
- new/getstatic/putstatic/invokestatic指令
- 反射调用
- 初始化子类时父类未初始化
- 主类(包含main()的类)
- MethodHandle调用
Java Stream 补充面试题详解
1. Stream 的中间操作和终止操作有什么区别?
特性 | 中间操作 | 终止操作 |
---|---|---|
返回值 | 返回新Stream | 返回非Stream结果或void |
执行时机 | 延迟执行(lazy) | 触发实际计算 |
可链式调用 | 是 | 否 |
示例 | filter() , map() | collect() , forEach() |
重要特点:
- 一个Stream只能有一个终止操作
- 中间操作可以多个连续调用
- 没有终止操作时,中间操作不会执行
2. 并行流(Parallel Stream)使用时要注意什么?
使用方式:
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.parallelStream().forEach(System.out::println);
注意事项:
-
线程安全:
- 确保共享变量是线程安全的
- 避免在lambda中修改外部状态
-
性能考量:
- 数据量小时可能比串行流更慢
- 适合CPU密集型操作
- 不适合IO密集型操作
-
顺序问题:
- forEachOrdered()保证顺序
- 某些操作(如findAny)在并行流中行为不同
-
资源控制:
- 默认使用ForkJoinPool.commonPool()
- 可自定义线程池:
ForkJoinPool customPool = new ForkJoinPool(4);
customPool.submit(() -> list.parallelStream().forEach(…));
3. Stream 和 Collection 的区别?
维度 | Stream | Collection |
---|---|---|
数据存储 | 不存储数据 | 存储所有元素 |
数据处理 | 函数式操作 | 命令式操作 |
遍历次数 | 一次性(用完即闭) | 可多次遍历 |
延迟执行 | 支持 | 不支持 |
并行处理 | 原生支持 | 需手动实现 |
内存占用 | 通常更低 | 存储全部数据 |
4. 常用的 Stream 收集器(Collectors)有哪些?
常用静态方法:
-
toList():收集为List
List list = stream.collect(Collectors.toList()); -
toSet():收集为Set
Set set = stream.collect(Collectors.toSet()); -
toMap():收集为Map
Map<String, Integer> map = stream.collect(
Collectors.toMap(Function.identity(), String::length)); -
groupingBy():分组
Map<Integer, List> group = stream.collect(
Collectors.groupingBy(String::length)); -
partitioningBy():分区
Map<Boolean, List> partition = stream.collect(
Collectors.partitioningBy(s -> s.length() > 3)); -
joining():字符串连接
String joined = stream.collect(Collectors.joining(", ")); -
summarizingInt():统计汇总
IntSummaryStatistics stats = stream.collect(
Collectors.summarizingInt(String::length));
5. Stream 中的 findFirst() 和 findAny() 有什么区别?
方法 | 行为 | 并行流表现 |
---|---|---|
findFirst() | 返回第一个元素(按相遇顺序) | 保证返回流中第一个元素 |
findAny() | 返回任意元素 | 可能返回任意匹配元素 |
使用场景:
- 需要确定性的结果:用findFirst()
- 只需要任意匹配项且不关心顺序:用findAny()(并行流性能更好)
6. Stream 的 flatMap() 和 map() 有什么区别?
map():
- 一对一的元素转换
- 不会改变流中元素数量
- 示例:
List<String> words = Arrays.asList("Hello", "World");List<Integer> lengths = words.stream().map(String::length) // ["Hello", "World"] → [5, 5].collect(Collectors.toList());
flatMap():
- 一对多的元素转换
- 将嵌套流"扁平化"为一个流
- 示例:
List<List<String>> listOfLists = Arrays.asList(Arrays.asList("a", "b"),Arrays.asList("c", "d"));List<String> flatList = listOfLists.stream().flatMap(List::stream) // [["a","b"],["c","d"]] → ["a","b","c","d"].collect(Collectors.toList());
7. 如何用 Stream 处理大文件?
高效读取大文件示例:
try (Stream<String> lines = Files.lines(Paths.get("large.txt"))) {List<String> result = lines.filter(line -> line.contains("keyword")).limit(1000).collect(Collectors.toList());
}
优势:
- Files.lines()不会一次性加载整个文件
- 流式处理保持低内存占用
- 可配合limit()控制处理量
8. Stream 的 peek() 和 forEach() 有什么区别?
方法 | 返回类型 | 用途 | 是否触发计算 |
---|---|---|---|
peek() | Stream | 调试/观察元素(中间操作) | 否 |
forEach() | void | 执行副作用(终止操作) | 是 |
peek()示例:
List<String> collected = Stream.of("a", "b", "c").peek(System.out::println) // 调试用.map(String::toUpperCase).collect(Collectors.toList());
forEach()示例:
Stream.of("a", "b", "c").forEach(System.out::println); // 实际消费流
9. 如何自定义 Collector?
实现 Collector 接口:
Collector<String, List<String>, List<String>> toListIgnoreCase = Collector.of(ArrayList::new, // supplier(list, str) -> list.add(str.toLowerCase()), // accumulator(list1, list2) -> { // combinerlist1.addAll(list2);return list1;},Collector.Characteristics.IDENTITY_FINISH);List<String> result = stream.collect(toListIgnoreCase);
核心参数:
- Supplier:创建结果容器
- Accumulator:将元素添加到容器
- Combiner:合并两个容器(并行流用)
- Finisher:最终转换(可选)
- Characteristics:收集器特性
10. Stream 的 reduce() 方法如何使用?
三种形式:
- 基本形式:
Optional<T> reduce(BinaryOperator<T> accumulator)Optional<Integer> sum = Stream.of(1, 2, 3).reduce((a, b) -> a + b);
- 带初始值:
T reduce(T identity, BinaryOperator<T> accumulator)int sum = Stream.of(1, 2, 3).reduce(0, (a, b) -> a + b);
- 复杂形式(并行流用):
<U> U reduce(U identity,BiFunction<U,? super T,U> accumulator,BinaryOperator<U> combiner)int sum = Stream.of(1, 2, 3).reduce(0,(partialSum, num) -> partialSum + num,Integer::sum);
使用场景:
- 聚合计算(求和、求积等)
- 复杂对象构建
- 替代某些collect()操作
Java 其他高频面试题补充
1. Java 模块化系统(Java 9+)
关键概念:
module-info.java
:模块描述文件- 模块路径 vs 类路径
- 访问控制规则:
requires
:声明依赖exports
:公开包opens
:反射开放包
面试问题:
- 模块化解决了什么问题?
- 如何迁移传统项目到模块化系统?
requires static
和requires transitive
的区别?
2. 记录类型(Record, Java 14+)
特点:
- 不可变数据载体
- 自动生成:构造器、访问器、equals()、hashCode()、toString()
示例:
public record Point(int x, int y) {}
面试问题:
- Record 和传统类的区别?
- 如何自定义 Record 的方法?
- Record 能否继承其他类?
3. 密封类(Sealed Class, Java 15+)
特点:
- 限制哪些类可以继承自己
- 通过
permits
关键字指定子类
示例:
public sealed class Shape
permits Circle, Square, Rectangle {…}
面试问题:
- 密封类的设计目的是什么?
- 密封类与final类的区别?
- 如何与Record结合使用?
4. 模式匹配(Java 16+)
instanceof 模式匹配:
if (obj instanceof String str) {
System.out.println(str.length());
}
switch 表达式增强:
return switch (day) {
case MONDAY, FRIDAY -> “工作日”;
case SATURDAY, SUNDAY -> “周末”;
default -> throw new IllegalArgumentException();
};
面试问题:
- 模式匹配如何简化代码?
- switch表达式与传统switch的区别?
- 什么情况下会抛出MatchError?
5. 虚拟线程(Virtual Thread, Java 19+)
特点:
- 轻量级线程(协程)
- 由JVM调度,非OS线程
- 高吞吐量(支持百万级线程)
使用方式:
Thread.startVirtualThread(() -> {
System.out.println(“Virtual thread running”);
});
面试问题:
- 虚拟线程与传统线程的区别?
- 什么场景适合使用虚拟线程?
- 如何避免虚拟线程的pin操作?
6. 响应式编程(Reactive Streams)
核心接口:
- Publisher
- Subscriber
- Subscription
- Processor<T,R>
面试问题:
- 背压(Backpressure)是什么?
- 如何实现冷热数据流?
- Reactor 和 RxJava 的主要区别?
7. Java 原生内存访问(Java 16+)
Foreign Function & Memory API:
- 替代JNI的更安全方式
- 直接操作堆外内存
示例:
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
segment.set(ValueLayout.JAVA_INT, 0, 42);
}
面试问题:
- 为什么需要新的内存访问API?
- MemorySegment 和 ByteBuffer 的区别?
- 如何防止使用原生API导致的内存泄漏?
8. 协程库比较
库名 | 特点 | 适用场景 |
---|---|---|
Quasar | 字节码增强 | 传统Java项目 |
Kilim | 轻量级 | 简单协程需求 |
Project Loom | JVM原生支持(虚拟线程) | Java 19+项目 |
面试问题:
- 协程与线程的本质区别?
- 如何在Java 8项目中使用协程?
- 协程的栈结构如何实现?
9. 微基准测试(JMH)
典型问题:
- 如何正确测量Java方法性能?
- JMH 如何避免JIT优化干扰?
- @BenchmarkMode 的几种模式区别?
- 什么是死代码消除(Dead Code Elimination)?
10. 代码安全实践
重点领域:
-
反序列化漏洞防护
- 使用白名单控制可反序列化类
- 替代方案:JSON/Protocol Buffers
-
密码学安全
- 避免使用弱算法(MD5/SHA1)
- 正确使用初始化向量(IV)
-
内存安全
- 敏感数据及时清零(char[]处理密码)
- 防止时序攻击
面试问题:
- 如何安全存储用户密码?
- 为什么推荐使用BCrypt而不是SHA系列?
- Java 的 SecureRandom 实现原理?
11. JVM 性能调优
常用工具:
- 命令行工具:
- jps, jstat, jstack, jmap
- 图形化工具:
- VisualVM, JConsole
- 高级工具:
- Async Profiler, JFR(Java Flight Recorder)
调优参数:
- 堆内存:-Xms, -Xmx
- 元空间:-XX:MetaspaceSize
- GC选择:-XX:+UseG1GC
- 日志输出:-Xlog:gc*
面试问题:
- 如何分析OOM问题?
- G1 和 ZGC 的适用场景区别?
- 如何诊断线程死锁?
12. 现代Java开发实践
热点话题:
-
不可变设计:
- Record 的使用
- 防御性拷贝
-
空值处理:
- Optional 最佳实践
- 静态分析工具(@NonNull)
-
并发模式:
- CompletableFuture 组合
- 反应式编程
面试问题:
- 为什么提倡不可变对象?
- Optional.flatMap() 和 map() 的区别?
- 如何设计无锁数据结构?