Java 基础面试题集
总题数: 66道 | 重点领域: 面向对象、基本语法、JVM | 难度分布: 基础到高级
本文档整理了 Java 基础的完整66道面试题目,涵盖面向对象、基本语法、常用API、设计模式等各个方面。
面试题目列表
1. Java 中的序列化和反序列化是什么?
序列化(Serialization) 是将Java对象转换为字节序列的过程,便于在网络上传输或保存到磁盘。
反序列化(Deserialization) 是从字节序列重构Java对象的过程。
实现方式:
- 实现
java.io.Serializable接口(标记接口,无需实现方法) - 使用
ObjectOutputStream进行序列化 - 使用
ObjectInputStream进行反序列化
关键点:
- 使用
transient关键字标记不需要序列化的字段 serialVersionUID用于版本控制,确保序列化与反序列化的类版本一致- 序列化会存储对象的完整对象图(包括引用的对象)
- 反序列化不会调用构造器
示例:
1// 序列化2ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"));3oos.writeObject(userObject);45// 反序列化6ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"));7User user = (User)ois.readObject();2. Java 中 Exception 和 Error 有什么区别?
Exception(异常):
- 表示程序可以处理的异常情况
- 分为受检异常(Checked Exception)和非受检异常(Unchecked Exception)
- 受检异常必须被try-catch或throws声明,如IOException、SQLException
- 非受检异常是RuntimeException的子类,如NullPointerException、ArrayIndexOutOfBoundsException
Error(错误):
- 表示严重问题,通常是不可恢复的
- 程序通常无法处理
- 例如OutOfMemoryError、StackOverflowError、VirtualMachineError
主要区别:
- 可处理性:Exception通常是可以被程序处理的,Error通常是不可恢复的
- 来源:Exception通常是程序问题,Error通常是系统或JVM问题
- 处理责任:Exception通常需要开发者处理,Error通常是JVM处理
异常体系:
1Throwable2├── Error3│ ├── OutOfMemoryError4│ ├── StackOverflowError5│ └── ...6└── Exception7 ├── IOException (checked)8 ├── SQLException (checked)9 └── RuntimeException (unchecked)10 ├── NullPointerException11 ├── ArrayIndexOutOfBoundsException12 └── ...3. 你认为 Java 的优势是什么?
Java的主要优势包括:
- 跨平台性:遵循"一次编写,到处运行"(WORA)的原则,通过JVM实现
- 面向对象:完全支持面向对象编程范式,包括封装、继承、多态
- 安全性:自带安全管理机制,包括类加载器、字节码验证器和安全管理器
- 自动内存管理:垃圾回收机制自动处理内存分配和回收
- 丰富的API和生态系统:标准库全面,开源框架丰富
- 并发支持:内置线程支持,并发库(如java.util.concurrent)强大
- 可靠性和稳定性:成熟语言,大量生产环境验证
- 向后兼容性:新版本通常能兼容旧代码
- 企业级支持:适合构建大型企业应用
- 社区活跃:庞大的开发者社区和丰富的资源
4. 什么是 Java 的多态特性?
多态(Polymorphism) 是Java面向对象的三大特性之一(封装、继承、多态),允许一个对象在不同情况下表现出不同的行为。
多态的类型:
-
编译时多态(静态多态):
- 方法重载(Overloading):同一个类中多个同名方法,参数列表不同
- 在编译时确定调用哪个方法
-
运行时多态(动态多态):
- 方法重写(Overriding):子类重写父类的方法
- 在运行时根据对象的实际类型确定调用哪个方法
实现多态的必要条件:
- 继承或实现(子类继承父类或实现接口)
- 方法重写(子类重写父类或接口的方法)
- 父类引用指向子类对象
示例:
1// 父类引用指向子类对象2Animal animal = new Dog();3animal.makeSound(); // 调用的是Dog类的makeSound方法45// 方法参数多态6public void feedAnimal(Animal animal) {7 animal.eat(); // 根据传入的具体动物调用相应的eat方法8}多态的好处:
- 提高代码的可扩展性和复用性
- 降低代码耦合度
- 使程序更加灵活和模块化
- 支持接口编程而非实现编程
5. Java 中的参数传递是按值还是按引用?
Java中的参数传递只有按值传递(pass by value),没有按引用传递。
对于基本类型:
- 传递的是实际值的副本
- 方法内对参数的修改不会影响原始变量
对于引用类型:
- 传递的是引用的副本(对象引用的值)
- 方法内对引用本身的修改不会影响原始引用
- 但可以通过引用副本修改引用对象的内容,这会影响到原对象
示例:
1public void testPassByValue() {2 // 基本类型3 int x = 10;4 changeValue(x); // x仍然是105 6 // 引用类型7 StringBuilder sb = new StringBuilder("Hello");8 changeReference(sb); // sb仍然指向原对象9 modifyReference(sb); // sb的内容变为"Hello World"10}1112void changeValue(int num) {13 num = 20; // 只改变副本,不影响原值14}1516void changeReference(StringBuilder s) {17 s = new StringBuilder("Hi"); // 只改变引用副本,不影响原引用18}1920void modifyReference(StringBuilder s) {21 s.append(" World"); // 修改引用指向的对象内容,会影响原对象22}常见误解: 很多人认为Java对象是按引用传递的,这是因为可以通过方法修改对象内容,但严格来说这仍是值传递(传递的是引用的值)。
6. 为什么 Java 不支持多重继承?
Java不支持类的多重继承(一个类继承多个父类),但支持实现多个接口。这样设计的主要原因是:
1. 钻石问题(Diamond Problem):
- 如果A类有一个方法show(),B和C都继承A并重写show(),D同时继承B和C,调用D.show()会有歧义
- 哪个父类的方法应该被执行?存在方法冲突和二义性
2. 简化设计:
- 单继承使类层次结构更清晰
- 避免了多父类带来的复杂性和歧义
- 降低了维护成本
3. 实际需求的解决方案:
- Java通过接口实现多重继承的功能特性
- 一个类可以实现多个接口
- 从Java 8开始,接口可以有默认方法和静态方法
- 可以使用组合代替继承
示例:
1// 使用接口实现多重继承功能2interface Flyable {3 void fly();4}56interface Swimmable {7 void swim();8}910// 一个类实现多个接口11class Duck implements Flyable, Swimmable {12 public void fly() {13 System.out.println("Duck flying");14 }15 16 public void swim() {17 System.out.println("Duck swimming");18 }19}接口的钻石问题解决: Java 8引入默认方法后,也可能遇到钻石问题,此时需要显式指定要使用哪个接口的默认方法,或者重写该方法。
7. Java 面向对象编程与面向过程编程的区别是什么?
面向过程编程(Procedural Programming):
- 以过程(算法)为中心,强调步骤和顺序
- 通过一系列的指令和函数调用完成任务
- 数据和操作数据的函数是分离的
- 程序被组织为一系列函数
面向对象编程(Object-Oriented Programming, OOP):
- 以对象为中心,强调数据和行为的封装
- 通过对象之间的交互完成任务
- 数据和操作数据的方法被封装在对象中
- 程序被组织为一组相互协作的对象
主要区别:
-
基本单元:
- 面向过程:函数是基本单元
- 面向对象:类和对象是基本单元
-
数据组织:
- 面向过程:数据和函数分离
- 面向对象:数据和方法封装在对象中
-
重用性:
- 面向过程:通过函数复用
- 面向对象:通过继承和多态实现更灵活的复用
-
维护性:
- 面向过程:修改函数可能影响多处
- 面向对象:封装使修改更加局部化
-
抽象级别:
- 面向过程:较低抽象级别,关注如何实现
- 面向对象:较高抽象级别,关注做什么
Java作为OOP语言的特点:
- 所有代码都在类中
- 支持封装、继承、多态三大核心特性
- 提供接口机制实现多重继承功能
- 强调对象之间的交互而非过程控制
8. Java 方法重载和方法重写之间的区别是什么?
方法重载(Overloading):
- 在同一个类中定义多个同名但参数不同的方法
- 编译时多态,编译时根据参数确定调用哪个方法
- 参数必须不同(类型、数量或顺序)
- 返回类型可以相同也可以不同
- 访问修饰符可以相同也可以不同
- 异常声明可以相同也可以不同
方法重写(Overriding):
- 子类重新实现父类中已有的方法
- 运行时多态,运行时根据对象类型确定调用哪个方法
- 方法签名必须相同(名称和参数列表)
- 返回类型必须相同或是父类方法返回类型的子类型
- 访问修饰符不能比父类方法更严格
- 不能抛出比父类方法更宽泛的检查异常
对比表格:
| 特性 | 方法重载(Overloading) | 方法重写(Overriding) |
|---|---|---|
| 发生位置 | 同一个类 | 子类 |
| 参数 | 必须不同 | 必须相同 |
| 返回类型 | 可以不同 | 必须相同或子类型 |
| 访问修饰符 | 可以不同 | 不能更严格 |
| 异常 | 可以不同 | 不能更宽泛 |
| 多态类型 | 编译时多态 | 运行时多态 |
| 绑定 | 静态绑定 | 动态绑定 |
示例:
1class Parent {2 void display(int a) {3 System.out.println("Parent: " + a);4 }5 6 Object getObject() {7 return new Object();8 }9}1011class Child extends Parent {12 // 方法重载13 void display(int a, int b) {14 System.out.println("Child: " + a + ", " + b);15 }16 17 // 方法重写18 @Override19 void display(int a) {20 System.out.println("Child overriding: " + a);21 }22 23 // 合法的返回类型协变24 @Override25 String getObject() {26 return "Child";27 }28}9. 什么是 Java 内部类?它有什么作用?
**内部类(Inner Class)**是定义在另一个类内部的类。Java支持四种内部类:
1. 成员内部类(Member Inner Class):
- 定义在类的成员位置
- 可以访问外部类的所有成员(包括私有)
- 可以被访问修饰符修饰
- 可以使用外部类.this引用外部类实例
- 必须通过外部类实例创建
1public class Outer {2 private int outerField = 1;3 4 class Inner {5 void display() {6 System.out.println("OuterField: " + outerField); // 可访问外部类私有成员7 System.out.println("OuterThis: " + Outer.this);8 }9 }10 11 void createInner() {12 Inner inner = new Inner(); // 创建内部类实例13 }14}1516// 外部创建内部类实例17Outer outer = new Outer();18Outer.Inner inner = outer.new Inner();2. 静态内部类(Static Nested Class):
- 使用static关键字定义
- 不能访问外部类的非静态成员
- 不需要外部类实例即可创建
1public class Outer {2 private static int staticOuterField = 1;3 private int outerField = 2;4 5 static class StaticNested {6 void display() {7 System.out.println(staticOuterField); // 可以访问外部类的静态成员8 // System.out.println(outerField); // 编译错误,无法访问非静态成员9 }10 }11}1213// 创建静态内部类实例14Outer.StaticNested nested = new Outer.StaticNested();3. 局部内部类(Local Inner Class):
- 定义在方法或代码块内
- 只能在定义它的方法或代码块中使用
- 可以访问外部类成员和有效final的局部变量
1public class Outer {2 void method(final int param) {3 final int localVar = 10;4 5 class LocalInner {6 void display() {7 System.out.println(param); // 访问有效final参数8 System.out.println(localVar); // 访问有效final局部变量9 }10 }11 12 LocalInner inner = new LocalInner();13 inner.display();14 }15}4. 匿名内部类(Anonymous Inner Class):
- 没有名字的局部内部类
- 同时声明和实例化
- 通常用于创建接口或抽象类的实现
1public class Outer {2 void createRunnable() {3 Runnable r = new Runnable() {4 @Override5 public void run() {6 System.out.println("Anonymous Inner Class");7 }8 };9 new Thread(r).start();10 }11}内部类的作用:
- 封装:可以将类封装在另一个类中
- 访问控制:内部类可以访问外部类的私有成员
- 逻辑分组:将密切相关的类分组在一起
- 回调实现:便于实现回调和事件处理
- 提高可读性和维护性:代码组织更清晰
10. JDK8 有哪些新特性?
JDK 8(Java SE 8)在2014年发布,带来了许多重要的新特性,是Java语言的一次重大升级:
1. Lambda表达式和函数式接口
- 允许将函数作为方法参数传递
- 简化匿名内部类的使用
1// 使用Lambda表达式2Collections.sort(list, (a, b) -> a.compareTo(b));2. Stream API
- 支持对集合进行函数式操作
- 提供过滤、映射、归约等操作
1List<String> filtered = items.stream()2 .filter(item -> item.startsWith("A"))3 .collect(Collectors.toList());3. 默认方法和静态方法
- 接口可以有方法实现(默认方法和静态方法)
- 解决了接口演化的问题
1interface Vehicle {2 default void print() {3 System.out.println("I am a vehicle!");4 }5 6 static void blowHorn() {7 System.out.println("Blowing horn!");8 }9}4. 方法引用
- 可以引用已有方法作为Lambda表达式
1// 方法引用示例2list.forEach(System.out::println);5. 新的日期和时间API
- 在java.time包中引入全新的日期时间API
- 线程安全、不可变、更好的设计
1LocalDate today = LocalDate.now();2LocalDateTime dateTime = LocalDateTime.of(2023, Month.JANUARY, 1, 10, 30);6. Optional类
- 更优雅地处理空值
- 避免NullPointerException
1Optional<String> name = Optional.ofNullable(getUserName());2String result = name.orElse("Unknown");7. Nashorn JavaScript引擎
- 新的JavaScript引擎,替代了旧的Rhino引擎
- 提供更好的性能和兼容性
8. Base64编码API
- 在java.util包中内置Base64编码支持
1String encoded = Base64.getEncoder().encodeToString("Hello".getBytes());9. 并行数组操作
- Arrays类中新增并行操作方法
1Arrays.parallelSort(array);10. 注解增强
- 支持重复注解
- 支持类型注解(可以在任何使用类型的地方使用注解)
11. JVM改进
- 移除永久代(PermGen),引入元空间(Metaspace)
- G1垃圾收集器改进
12. 其他新特性
- CompletableFuture提供强大的异步编程支持
- StampedLock提供乐观读锁支持
- String.join()方法简化字符串连接
- Files类增强,支持更多文件操作
11. Java 中 String、StringBuffer 和 StringBuilder 的区别是什么?
可变性:
- String:不可变,一旦创建内容不能修改
- StringBuffer:可变,线程安全
- StringBuilder:可变,非线程安全,但性能更好
线程安全性:
- String:不可变,因此线程安全
- StringBuffer:线程安全,方法使用synchronized修饰
- StringBuilder:非线程安全,没有同步措施
性能比较:
- String:每次操作都会创建新对象,性能最差
- StringBuffer:同步操作带来额外开销,性能中等
- StringBuilder:无同步开销,性能最好
适用场景:
- String:适用于少量操作、不经常修改、多线程共享的场景
- StringBuffer:适用于多线程环境下字符串频繁修改的场景
- StringBuilder:适用于单线程环境下字符串频繁修改的场景
内部实现:
- 所有实现都是基于字符数组
- StringBuffer和StringBuilder都继承自AbstractStringBuilder
- 区别在于StringBuffer的方法使用synchronized关键字修饰
代码示例:
1// String 每次操作创建新对象2String str = "Hello";3str = str + " World"; // 创建了新的String对象45// StringBuffer 同步操作,线程安全6StringBuffer sbuf = new StringBuffer("Hello");7sbuf.append(" World"); // 原对象被修改,线程安全89// StringBuilder 非同步操作,性能最佳10StringBuilder sbld = new StringBuilder("Hello");11sbld.append(" World"); // 原对象被修改,非线程安全性能测试(添加10000个字符):
- StringBuilder: ~10ms
- StringBuffer: ~100ms
- String: ~50000ms
12. Java 的 StringBuilder 是怎么实现的?
StringBuilder是Java中用于高效处理可变字符串的类,它的主要实现特点包括:
1. 内部数据结构:
- 内部使用字符数组(
char[])存储数据(JDK 9之前) - JDK 9及之后使用字节数组(
byte[]),采用Latin-1或UTF-16编码 - 继承自AbstractStringBuilder抽象类
2. 动态扩容机制:
- 初始默认容量为16个字符
- 当内容超出容量时自动扩容
- 扩容策略:新容量 = 旧容量 * 2 + 2
- 容量可以通过构造函数预先指定
3. 非线程安全:
- 没有同步措施保证线程安全
- 优先考虑性能,没有同步开销
- 多线程环境需使用StringBuffer
4. 主要操作方法:
append(): 添加内容到末尾insert(): 在指定位置插入内容delete(): 删除指定范围的内容replace(): 替换指定范围的内容reverse(): 反转字符串内容toString(): 转换为String对象
5. 源码关键片段:
1// JDK 8中的关键实现(简化版)2abstract class AbstractStringBuilder {3 char[] value; // 存储字符4 int count; // 当前使用的长度5 6 // 扩容方法7 void expandCapacity(int minimumCapacity) {8 int newCapacity = (value.length * 2) + 2;9 if (newCapacity < minimumCapacity)10 newCapacity = minimumCapacity;11 value = Arrays.copyOf(value, newCapacity);12 }13}1415// StringBuilder继承实现16public final class StringBuilder extends AbstractStringBuilder {17 // append方法(无同步)18 @Override19 public StringBuilder append(String str) {20 super.append(str);21 return this;22 }23 // 其他方法...24}6. 优化技巧:
- 预估容量以减少扩容次数
- 链式调用提高可读性和便捷性
- 单线程环境下优先使用StringBuilder
13. Java 中包装类型和基本类型的区别是什么?
**基本类型(Primitive Types)和包装类型(Wrapper Classes)**是Java中两种不同的数据类型系统:
基本类型:
- 直接存储值,非对象
- 存储在栈内存中
- 性能更好,内存占用小
- 不能为null
- 不具备方法和属性
- 包括:byte、short、int、long、float、double、char、boolean
包装类型:
- 对基本类型的对象封装
- 引用存储在栈中,对象存储在堆中
- 性能较差,内存占用大
- 可以为null
- 具有类的方法和属性
- 包括:Byte、Short、Integer、Long、Float、Double、Character、Boolean
主要区别:
| 特性 | 基本类型 | 包装类型 |
|---|---|---|
| 默认值 | 0/false | null |
| 可为null | 否 | 是 |
| 存储位置 | 栈内存 | 堆内存 |
| 性能 | 高 | 较低 |
| 方法调用 | 不支持 | 支持 |
| 泛型支持 | 不支持 | 支持 |
自动装箱与拆箱:
- 自动装箱(Autoboxing):基本类型自动转换为包装类型
- 自动拆箱(Unboxing):包装类型自动转换为基本类型
1// 自动装箱2Integer num = 10; // int 自动转为 Integer34// 自动拆箱5int value = num; // Integer 自动转为 int使用场景:
- 基本类型:对性能要求高、内存敏感、值不为null的场景
- 包装类型:泛型集合、可能为null的情况、需要使用对象方法的场景
注意事项:
- 包装类型的"=="比较可能出现意外结果(比较对象引用)
- Integer缓存池(-128到127的值被缓存)会影响"=="比较结果
- 包装类型涉及装箱/拆箱操作,频繁操作会影响性能
14. 接口和抽象类有什么区别?
接口(Interface)和抽象类(Abstract Class)都是Java中支持抽象的机制,但它们有以下重要区别:
1. 实现与继承
- 抽象类:类通过
extends关键字继承,只能单继承 - 接口:类通过
implements关键字实现,可以实现多个接口
2. 成员特性
- 抽象类:可以有构造方法、实例变量、普通方法、抽象方法
- 接口:只能有常量(public static final)、抽象方法、默认方法(Java 8+)、静态方法(Java 8+)、私有方法(Java 9+)
3. 修饰符
- 抽象类:类和方法可以有各种访问修饰符
- 接口:方法默认是public abstract,变量默认是public static final
4. 设计目的
- 抽象类:表示"是什么"(is-a)关系,强调类的继承
- 接口:表示"能做什么"(can-do)关系,强调行为的契约
5. 使用场景
- 抽象类:当需要共享代码和状态时
- 接口:当需要定义契约而不关心具体实现时
6. 默认方法(Java 8+)
- 抽象类:可以有默认实现的方法
- 接口:从Java 8开始可以有默认方法(default)和静态方法(static)
代码对比:
1// 抽象类示例2abstract class Animal {3 // 构造方法4 public Animal() { }5 6 // 实例变量7 protected String name;8 9 // 普通方法10 public void setName(String name) {11 this.name = name;12 }13 14 // 抽象方法15 public abstract void makeSound();16}1718// 接口示例19interface Flyable {20 // 常量(隐式public static final)21 int MAX_HEIGHT = 10000;22 23 // 抽象方法(隐式public abstract)24 void fly();25 26 // 默认方法(Java 8+)27 default void glide() {28 System.out.println("Gliding...");29 }30 31 // 静态方法(Java 8+)32 static boolean canFly(Object obj) {33 return obj instanceof Flyable;34 }35 36 // 私有方法(Java 9+)37 private void helper() {38 System.out.println("Helper method");39 }40}最佳实践:
- 优先使用接口,更灵活
- 当需要共享代码实现时使用抽象类
- 使用接口定义类型,使用抽象类提供基础实现
- 有时可以组合使用:抽象类实现接口,提供部分默认实现
15. JDK 和 JRE 有什么区别?
**JDK (Java Development Kit)和JRE (Java Runtime Environment)**是Java平台的两个重要组件:
JDK (Java开发工具包):
- 包含了开发Java应用程序所需的所有工具
- 包含JRE,提供运行环境
- 包含编译器(javac),用于将Java源代码转换为字节码
- 包含调试器(jdb),用于调试Java程序
- 包含文档生成器(javadoc),用于生成API文档
- 包含其他开发工具:jar、javap、jlink等
JRE (Java运行时环境):
- 提供运行Java应用程序的环境
- 包含JVM (Java虚拟机),执行Java字节码
- 包含核心类库,提供标准API
- 包含运行时库,支持JVM运行
- 不包含开发工具(如编译器)
组件关系图:
1┌────────────────────────────────────┐2│ JDK (Java Development Kit) │3│ ┌──────────────────────────────┐ │4│ │ 开发工具: │ │5│ │ - javac (编译器) │ │6│ │ - jdb (调试器) │ │7│ │ - javadoc (文档生成器) │ │8│ │ - jar, jlink, jps等 │ │9│ │ │ │10│ │ ┌────────────────────────┐ │ │11│ │ │ JRE │ │ │12│ │ │ ┌─────────────────┐ │ │ │13│ │ │ │ Java虚拟机 │ │ │ │14│ │ │ │ (JVM) │ │ │ │15│ │ │ └─────────────────┘ │ │ │16│ │ │ │ │ │17│ │ │ ┌─────────────────┐ │ │ │18│ │ │ │ 类库和API │ │ │ │19│ │ │ └─────────────────┘ │ │ │20│ │ └────────────────────────┘ │ │21│ └──────────────────────────────┘ │22└────────────────────────────────────┘主要区别:
| 特性 | JDK | JRE |
|---|---|---|
| 主要用途 | 开发Java程序 | 运行Java程序 |
| 包含组件 | 开发工具 + JRE | JVM + 类库 |
| 目标用户 | 开发人员 | 终端用户 |
| 安装大小 | 较大 | 较小 |
实际应用:
- 如果只需要运行Java应用程序,安装JRE即可
- 如果需要开发Java应用程序,必须安装JDK
- 大多数开发人员直接安装JDK,因为它包含JRE
版本发展:
- 从JDK 9开始,Oracle改变了版本命名和发布策略
- JDK现在包含JRE,但不再单独提供JRE下载
- 可以使用jlink工具创建自定义运行时镜像
16. 你使用过哪些 JDK 提供的工具?
JDK提供了丰富的开发和诊断工具,以下是一些常用工具及其用途:
1. 编译和构建工具:
- javac:Java编译器,将.java源文件编译为.class字节码文件
- jar:创建和管理JAR(Java Archive)文件,打包Java应用
- javadoc:从源代码中生成API文档
- jlink:(Java 9+)创建自定义运行时镜像,生成包含特定模块的精简JRE
2. 运行工具:
- java:运行Java应用程序的启动器
- javaw:Windows平台上不带控制台的Java启动器
- jdb:Java调试器,用于调试Java程序
- jshell:(Java 9+)Java交互式REPL环境
3. 诊断工具:
- jstat:监视JVM统计信息(GC、类加载等)
- jmap:生成堆转储(heap dump),分析内存使用
- jstack:生成线程堆栈转储,分析线程状态和死锁
- jcmd:向运行中的JVM发送诊断命令
- jconsole:图形化JVM监控工具
- jvisualvm:更强大的可视化监控、故障排除和分析工具
- jmc:Java Mission Control,高级监控和管理工具
4. 安全工具:
- keytool:管理密钥和证书,用于安全通信
- jarsigner:JAR文件签名和验证工具
5. 国际化工具:
- native2ascii:转换包含非ASCII字符的文件
6. 远程方法调用工具:
- rmic:生成远程对象存根和骨架类
- rmid:启动激活系统守护进程
7. 脚本工具:
- jrunscript:Java命令行脚本外壳
工具使用场景:
1# 编译Java源文件2javac MyClass.java34# 运行Java应用5java -cp . com.example.MainClass67# 创建JAR包8jar cf myapp.jar *.class910# 查看运行中的Java进程11jps -l1213# 分析堆内存使用情况14jmap -heap <pid>1516# 检查线程状态和死锁17jstack <pid>1819# 监控GC活动20jstat -gc <pid> 1000最常用工具组合:
- 日常开发:javac、java、jar
- 性能分析:jstat、jmap、jstack、jconsole
- 问题诊断:jvisualvm、jmc
17. Java 中 hashCode 和 equals 方法是什么?它们与 == 操作符有什么区别?
equals方法:
- 定义在Object类中,用于比较两个对象的内容是否相等
- 默认实现比较对象的引用(即内存地址)
- 可被子类重写以实现自定义的相等性比较逻辑
- 应该满足自反性、对称性、传递性、一致性和非空性
hashCode方法:
- 定义在Object类中,返回对象的哈希码值(一个整数)
- 默认实现返回对象的内存地址的某种转换(依赖于JVM实现)
- 用于在哈希表数据结构(如HashMap、HashSet)中高效存储和检索对象
- 相等的对象必须有相等的哈希码
== 操作符:
- 用于比较基本类型的值或引用类型的引用(内存地址)
- 对于基本类型:比较实际值是否相等
- 对于引用类型:比较引用是否指向同一对象
三者的关键区别:
| 特性 | hashCode | equals | == |
|---|---|---|---|
| 返回类型 | int | boolean | boolean |
| 比较内容 | 生成对象的哈希值 | 对象的逻辑相等性 | 值相等或引用相等 |
| 使用场景 | 哈希数据结构 | 对象内容比较 | 基本类型比较或引用比较 |
hashCode与equals的关系:
- 如果两个对象equals比较相等,它们的hashCode值必须相同
- 如果两个对象hashCode值相同,它们不一定equals相等
- 重写equals方法时必须同时重写hashCode方法
示例代码:
1public class Person {2 private String name;3 private int age;4 5 // 构造器等代码省略6 7 @Override8 public boolean equals(Object obj) {9 if (this == obj) return true; // 引用相同直接返回true10 if (obj == null || getClass() != obj.getClass()) return false;11 12 Person person = (Person) obj;13 return age == person.age && 14 Objects.equals(name, person.name);15 }16 17 @Override18 public int hashCode() {19 return Objects.hash(name, age);20 }21}2223// 使用示例24Person p1 = new Person("John", 30);25Person p2 = new Person("John", 30);26Person p3 = p1;2728System.out.println(p1.equals(p2)); // true (内容相等)29System.out.println(p1 == p2); // false (不同对象)30System.out.println(p1 == p3); // true (相同引用)31System.out.println(p1.hashCode() == p2.hashCode()); // true (内容相等,哈希码相等)常见错误:
- 只重写equals不重写hashCode
- 在equals中使用==比较String或其他对象
- 忽略equals的对称性或传递性
18. Java 中的 hashCode 和 equals 方法之间有什么关系?
hashCode和equals方法之间存在密切的关系,这种关系由Object类的通用约定(general contract)定义:
1. 一致性原则:
- 如果两个对象通过equals方法比较相等,则它们的hashCode方法必须返回相同的值
- 反之不成立:hashCode相同的两个对象不一定equals相等(哈希冲突)
2. 稳定性原则:
- 在对象的equals比较上使用的属性没有修改的情况下,多次调用hashCode应返回相同的值
- 不同的程序执行可以产生不同的哈希码(不需要在程序执行之间保持一致)
3. 性能考虑:
- hashCode应该分散良好,尽量减少碰撞
- hashCode计算应该高效,因为在哈希表中频繁调用
实际应用场景:
- 在HashMap、HashSet等基于哈希的集合类中广泛使用
- 这些集合先使用hashCode快速确定可能的存储桶
- 然后使用equals进行精确匹配
哈希集合的工作原理:
1步骤1: 计算key的hashCode2步骤2: 根据hashCode确定存储桶位置3步骤3: 在该存储桶中用equals方法查找精确匹配正确实现示例:
1public class Student {2 private String id;3 private String name;4 5 // 构造器等代码省略6 7 @Override8 public boolean equals(Object o) {9 if (this == o) return true;10 if (o == null || getClass() != o.getClass()) return false;11 12 Student student = (Student) o;13 return Objects.equals(id, student.id); // 仅基于id比较14 }15 16 @Override17 public int hashCode() {18 return Objects.hash(id); // 仅基于id计算哈希19 // 注意:equals和hashCode使用相同的字段20 }21}不正确实现的后果:
-
只重写equals,不重写hashCode:
- 对象在HashMap中可能无法正常查找
- 可能导致意外的重复元素出现在HashSet中
-
只重写hashCode,不重写equals:
- 不符合equals和hashCode的约定
- 可能导致不一致的行为
-
equals和hashCode使用不同的字段:
- 可能导致equals相等的对象在哈希集合中被视为不同对象
最佳实践:
- 同时重写equals和hashCode方法
- 使用相同的字段来决定对象相等性和计算哈希码
- 考虑使用IDE生成这些方法或使用Lombok等库
- Java 7+中使用Objects.equals()和Objects.hash()
19. 什么是 Java 中的动态代理?
动态代理是Java反射机制的一部分,允许在运行时创建接口的实现类,而无需在编译时就确定具体的实现类。
动态代理的主要用途:
- 面向切面编程(AOP)
- 远程方法调用(RMI)
- 依赖注入框架
- 数据库连接和事务管理
- 方法调用的日志记录、性能监控、安全控制等
Java中两种主要的动态代理机制:
1. JDK动态代理:
- 基于Java反射API实现
- 只能代理实现了接口的类
- 通过
java.lang.reflect.Proxy类和InvocationHandler接口实现 - 生成的代理类是接口的实现
JDK动态代理示例:
1import java.lang.reflect.InvocationHandler;2import java.lang.reflect.Method;3import java.lang.reflect.Proxy;45// 定义接口6interface UserService {7 void addUser(String name);8 String findUser(int id);9}1011// 实现类12class UserServiceImpl implements UserService {13 @Override14 public void addUser(String name) {15 System.out.println("Adding user: " + name);16 }17 18 @Override19 public String findUser(int id) {20 return "User " + id;21 }22}2324// 调用处理器25class LoggingInvocationHandler implements InvocationHandler {26 private final Object target;27 28 public LoggingInvocationHandler(Object target) {29 this.target = target;30 }31 32 @Override33 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {34 System.out.println("Before method: " + method.getName());35 Object result = method.invoke(target, args);36 System.out.println("After method: " + method.getName());37 return result;38 }39}4041// 使用动态代理42public class Main {43 public static void main(String[] args) {44 // 创建目标对象45 UserService userService = new UserServiceImpl();46 47 // 创建调用处理器48 InvocationHandler handler = new LoggingInvocationHandler(userService);49 50 // 创建代理对象51 UserService proxy = (UserService) Proxy.newProxyInstance(52 UserService.class.getClassLoader(),53 new Class<?>[] { UserService.class },54 handler55 );56 57 // 通过代理调用方法58 proxy.addUser("Alice");59 String user = proxy.findUser(123);60 }61}2. CGLIB代理:
- 基于ASM字节码操作框架
- 可以代理没有实现接口的类(通过继承)
- 性能通常优于JDK动态代理
- 不能代理final类或final方法
- Spring框架当中如果bean没有实现接口则默认使用CGLIB代理
CGLIB代理示例:
1import net.sf.cglib.proxy.Enhancer;2import net.sf.cglib.proxy.MethodInterceptor;3import net.sf.cglib.proxy.MethodProxy;45import java.lang.reflect.Method;67// 普通类(不实现接口)8class UserService {9 public void addUser(String name) {10 System.out.println("Adding user: " + name);11 }12 13 public String findUser(int id) {14 return "User " + id;15 }16}1718// 方法拦截器19class LoggingMethodInterceptor implements MethodInterceptor {20 @Override21 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {22 System.out.println("Before method: " + method.getName());23 Object result = proxy.invokeSuper(obj, args);24 System.out.println("After method: " + method.getName());25 return result;26 }27}2829// 使用CGLIB代理30public class Main {31 public static void main(String[] args) {32 // 创建增强器33 Enhancer enhancer = new Enhancer();34 // 设置父类35 enhancer.setSuperclass(UserService.class);36 // 设置回调37 enhancer.setCallback(new LoggingMethodInterceptor());38 39 // 创建代理对象40 UserService proxy = (UserService) enhancer.create();41 42 // 通过代理调用方法43 proxy.addUser("Bob");44 String user = proxy.findUser(456);45 }46}JDK代理与CGLIB代理的对比:
| 特性 | JDK动态代理 | CGLIB代理 |
|---|---|---|
| 实现方式 | 基于接口 | 基于继承 |
| 代理对象类型 | 接口的实现 | 目标类的子类 |
| 限制 | 只能代理接口 | 不能代理final类/方法 |
| 性能 | 较低 | 较高(尤其多次调用) |
| 内置于JDK | 是 | 否(需要额外依赖) |
| 用途 | 面向接口编程 | 非接口类的代理 |
20. JDK 动态代理和 CGLIB 动态代理有什么区别?
JDK动态代理和CGLIB动态代理是Java中两种主要的动态代理实现方式,它们具有不同的工作原理和适用场景:
1. 实现原理:
- JDK动态代理:基于Java标准库的
java.lang.reflect.Proxy类实现,通过反射机制在运行时生成接口实现类 - CGLIB动态代理:基于字节码操作库ASM实现,通过生成目标类的子类来实现代理
2. 代理对象创建:
-
JDK动态代理:
java1Interface proxy = (Interface) Proxy.newProxyInstance(2 classLoader,3 new Class[] { Interface.class },4 new InvocationHandler() {5 @Override6 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {7 // 前置处理8 Object result = method.invoke(target, args);9 // 后置处理10 return result;11 }12 }13); -
CGLIB动态代理:
java1Enhancer enhancer = new Enhancer();2enhancer.setSuperclass(TargetClass.class);3enhancer.setCallback(new MethodInterceptor() {4 @Override5 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {6 // 前置处理7 Object result = proxy.invokeSuper(obj, args);8 // 后置处理9 return result;10 }11});12TargetClass proxy = (TargetClass) enhancer.create();
3. 关键区别:
| 区别点 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 代理方式 | 接口代理 | 类继承代理 |
| 必要条件 | 目标类必须实现接口 | 目标类不需要实现接口 |
| 生成方式 | 生成接口的实现类 | 生成目标类的子类 |
| 依赖关系 | JDK内置(rt.jar) | 第三方库(cglib.jar) |
| 方法调用 | 通过反射调用 | 通过FastClass机制 |
| 使用限制 | 只能代理接口方法 | 不能代理final类或方法 |
| 调用速度 | 首次调用慢,后续调用一般 | 首次调用慢,后续调用快 |
| 内存占用 | 较低 | 较高(生成更多类) |
4. 性能对比:
- 首次调用:两者都需要生成代理类,性能相似
- 后续调用:CGLIB通常更快,因为使用FastClass机制而非反射
- JDK 8+:JDK代理性能得到显著改进,差距缩小
5. 适用场景:
-
JDK动态代理:
- 基于接口的编程
- 不需要额外依赖
- 内存受限的环境
- 代理接口方法
-
CGLIB动态代理:
- 目标类没有实现接口
- 需要代理非接口方法
- 性能要求高的场景
- 需要代理类而非接口
6. 框架使用:
-
Spring AOP:
- 优先使用JDK动态代理
- 目标类没有实现接口时,自动切换到CGLIB代理
- 可通过
proxy-target-class配置强制使用CGLIB
-
Hibernate:
- 使用CGLIB创建延迟加载代理
7. 代码示例对比:
1// 两种代理方式对比23// 共同的接口4interface Service {5 void doSomething();6}78// 目标类9class ServiceImpl implements Service {10 @Override11 public void doSomething() {12 System.out.println("Doing something...");13 }14}1516// JDK动态代理17Service jdkProxy = (Service) Proxy.newProxyInstance(18 Service.class.getClassLoader(),19 new Class<?>[] { Service.class },20 (proxy, method, args) -> {21 System.out.println("JDK proxy before");22 Object result = method.invoke(new ServiceImpl(), args);23 System.out.println("JDK proxy after");24 return result;25 }26);2728// CGLIB动态代理29Enhancer enhancer = new Enhancer();30enhancer.setSuperclass(ServiceImpl.class);31enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {32 System.out.println("CGLIB proxy before");33 Object result = proxy.invokeSuper(obj, args);34 System.out.println("CGLIB proxy after");35 return result;36});37Service cglibProxy = (Service) enhancer.create();总结:选择JDK代理还是CGLIB代理主要取决于目标类是否实现接口、性能要求以及项目依赖情况。
21. Java 中的注解原理是什么?
**注解(Annotation)**是Java 5引入的一种特殊类型的接口,用于为代码添加元数据。它们不会直接影响代码逻辑,但可以被编译器、开发工具和运行时环境使用,实现额外的功能。
1. 注解的定义:
1// 定义一个简单的注解2public @interface MyAnnotation {3 String value() default "";4 int count() default 0;5}2. 元注解:用于注解其他注解的特殊注解。
-
@Retention:定义注解的保留策略
- SOURCE:仅在源码中保留,编译时丢弃
- CLASS:保留到编译后的类文件,但运行时不可用(默认值)
- RUNTIME:运行时可通过反射访问
-
@Target:定义注解可应用的元素类型
- TYPE:类、接口、枚举
- FIELD:字段
- METHOD:方法
- PARAMETER:方法参数
- CONSTRUCTOR:构造器
- LOCAL_VARIABLE:局部变量
- ANNOTATION_TYPE:注解
- PACKAGE:包
- TYPE_PARAMETER(Java 8):类型参数
- TYPE_USE(Java 8):类型使用
-
@Documented:指示注解应被JavaDoc工具记录
-
@Inherited:表示注解可被子类继承
-
@Repeatable(Java 8):表示注解可在同一元素上多次使用
3. 注解的工作原理:
编译时处理:
- 编译器在编译时识别注解
- 可通过注解处理器(Annotation Processor)在编译期处理注解
- 注解处理器可生成新的源文件、修改编译过程等
运行时处理:
- 使用Java反射API获取运行时注解信息
- 运行时根据注解信息修改程序行为
4. 反射获取注解示例:
1// 获取类上的注解2Class<?> clazz = MyClass.class;3MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);4if (annotation != null) {5 System.out.println(annotation.value());6 System.out.println(annotation.count());7}89// 获取方法上的注解10Method method = clazz.getMethod("myMethod");11MyAnnotation methodAnnotation = method.getAnnotation(MyAnnotation.class);1213// 获取字段上的注解14Field field = clazz.getDeclaredField("myField");15MyAnnotation fieldAnnotation = field.getAnnotation(MyAnnotation.class);5. 注解处理器:
- 实现
javax.annotation.processing.Processor接口 - 在编译时对代码中的注解进行处理
- 用于生成额外代码、验证或文档生成等
- 通过SPI机制注册处理器
1@SupportedAnnotationTypes("com.example.MyAnnotation")2@SupportedSourceVersion(SourceVersion.RELEASE_8)3public class MyProcessor extends AbstractProcessor {4 @Override5 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {6 // 处理注解逻辑7 return true;8 }9}6. 注解的实现机制:
- 注解本质上是特殊的接口,继承自
java.lang.annotation.Annotation - 编译器会为每个注解生成代理类,实现注解接口
- 注解信息被存储在类文件的属性表中
- JVM加载类时,会解析这些属性并构建内存中的注解对象
7. 注解的应用场景:
- 配置信息:如Spring的
@Component,@Autowired - 编译检查:如
@Override,@Deprecated - 代码生成:如Lombok的
@Data,@Builder - 运行时处理:如JUnit的
@Test,@Before - API文档:如Swagger的
@Api,@ApiOperation
8. 常见的内置注解:
@Override:声明方法覆盖父类方法@Deprecated:标记已过时的元素@SuppressWarnings:抑制编译器警告@SafeVarargs:抑制与可变参数相关的警告@FunctionalInterface:声明函数式接口
9. Java 8后的注解增强:
- 类型注解:可以在任何使用类型的地方使用注解
- 重复注解:同一元素可以多次应用相同的注解
- 方法参数反射:可以获取方法参数的名称
22. 你使用过 Java 的反射机制吗?如何应用反射?
**反射(Reflection)**是Java的一个强大特性,允许程序在运行时检查和操作类、接口、字段和方法。反射打破了Java的封装性,使程序能够访问原本无法访问的成员,并在运行时动态地创建对象和调用方法。
1. 反射的核心类:
- Class类:表示类的元信息,是反射的入口点
- Constructor:表示类的构造方法
- Method:表示类的方法
- Field:表示类的字段
- Parameter(Java 8):表示方法参数
- Annotation:表示注解
2. 获取Class对象的方式:
1// 方式1:通过对象的getClass()方法2String str = "Hello";3Class<?> clazz1 = str.getClass();45// 方式2:通过类字面常量6Class<?> clazz2 = String.class;78// 方式3:通过Class.forName()方法9Class<?> clazz3 = Class.forName("java.lang.String");1011// 方式4:通过类加载器12ClassLoader classLoader = Thread.currentThread().getContextClassLoader();13Class<?> clazz4 = classLoader.loadClass("java.lang.String");3. 反射创建对象:
1// 使用Class.newInstance()方法(已过时)2Object obj1 = clazz.newInstance();34// 获取Constructor并创建对象5Constructor<?> constructor1 = clazz.getConstructor(); // 无参构造器6Object obj2 = constructor1.newInstance();78// 带参数的构造器9Constructor<?> constructor2 = clazz.getConstructor(String.class);10Object obj3 = constructor2.newInstance("Hello");4. 访问和修改字段:
1// 获取公共字段2Field publicField = clazz.getField("fieldName");34// 获取任何字段(包括private)5Field privateField = clazz.getDeclaredField("privatefield");6privateField.setAccessible(true); // 绕过访问限制78// 获取字段值9Object value = privateField.get(obj);1011// 设置字段值12privateField.set(obj, newValue);5. 调用方法:
1// 获取公共方法2Method publicMethod = clazz.getMethod("methodName", paramTypes);34// 获取任何方法(包括private)5Method privateMethod = clazz.getDeclaredMethod("privateMethod", paramTypes);6privateMethod.setAccessible(true); // 绕过访问限制78// 调用方法9Object result = privateMethod.invoke(obj, args);1011// 调用静态方法12Object result = staticMethod.invoke(null, args);6. 获取泛型信息:
1// 获取字段的泛型类型2Field field = clazz.getDeclaredField("listField");3Type genericType = field.getGenericType();45if (genericType instanceof ParameterizedType) {6 ParameterizedType pType = (ParameterizedType) genericType;7 Type[] typeArguments = pType.getActualTypeArguments();8 for (Type typeArgument : typeArguments) {9 Class<?> typeClass = (Class<?>) typeArgument;10 System.out.println("泛型类型: " + typeClass);11 }12}7. 反射获取注解:
1// 获取类上的注解2Annotation[] annotations = clazz.getAnnotations();34// 获取指定类型的注解5MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);67// 获取方法上的注解8Method method = clazz.getMethod("myMethod");9MyAnnotation methodAnnotation = method.getAnnotation(MyAnnotation.class);8. 反射的应用场景:
-
框架开发:
- Spring的依赖注入和AOP
- Hibernate的ORM映射
- JUnit的测试运行器
-
动态代理:
- JDK动态代理使用反射实现接口方法调用
- 实现AOP切面功能
-
序列化与反序列化:
- JSON/XML与对象的相互转换
-
配置文件处理:
- 根据配置动态加载和使用类
-
注解处理:
- 运行时解析注解并执行相应逻辑
9. 反射的优缺点:
优点:
- 提高了程序的灵活性和扩展性
- 使框架开发成为可能
- 支持动态加载和使用类
缺点:
- 性能开销较大,反射操作比直接调用慢
- 破坏了封装性,可能导致安全问题
- 代码可读性和可维护性降低
- 编译时类型检查失效,可能导致运行时错误
10. 提高反射性能的技巧:
- 缓存Constructor/Method/Field对象
- 减少反射调用次数
- 使用setAccessible(true)提高私有成员访问速度
11. 反射的安全限制:
- 需要适当的权限才能反射访问私有成员
- SecurityManager可以限制反射操作
- Java模块系统(Java 9+)可以控制反射访问
23. 什么是 Java 中的不可变类?
**不可变类(Immutable Class)**是指一旦创建,其实例的状态就不能被改变的类。在Java中,String、Integer等包装类以及BigDecimal、BigInteger都是不可变类的典型例子。
1. 不可变类的特点:
- 实例创建后状态不能被修改
- 所有字段都是final的(最佳实践)
- 适当地封装字段(通常是private)
- 不提供修改状态的方法
- 保护性地管理任何可变组件
- 必要时创建对象的防御性拷贝
2. 创建不可变类的规则:
- 将类声明为final,防止子类化
- 将所有字段声明为private final
- 不提供修改字段的方法
- 特别注意包含可变对象引用的字段:
- 构造器中创建可变对象的副本
- getter方法返回可变对象的副本
- 确保正确处理对象的序列化(如果需要可序列化)
3. 不可变类的示例:
1public final class ImmutablePerson {2 private final String name;3 private final int age;4 private final List<String> hobbies; // 可变对象引用56 public ImmutablePerson(String name, int age, List<String> hobbies) {7 this.name = name;8 this.age = age;9 // 创建防御性副本10 this.hobbies = new ArrayList<>(hobbies);11 }1213 public String getName() {14 return name;15 }1617 public int getAge() {18 return age;19 }2021 public List<String> getHobbies() {22 // 返回防御性副本23 return new ArrayList<>(hobbies);24 }2526 // 不提供setter方法2728 // 提供修改状态的方法时返回新实例29 public ImmutablePerson withName(String newName) {30 return new ImmutablePerson(newName, age, hobbies);31 }3233 public ImmutablePerson withAge(int newAge) {34 return new ImmutablePerson(name, newAge, hobbies);35 }36}4. String类的不可变性:
String是Java中最常用的不可变类:
- String类被声明为final
- 内部字符数组是private final的
- 没有修改内部状态的方法
- 所有修改操作都返回新的String对象
1public final class String implements Serializable, Comparable<String>, CharSequence {2 private final char[] value; // JDK 9之前3 // 或4 private final byte[] value; // JDK 9之后5 6 // 其他字段和方法7}5. 不可变类的优势:
-
线程安全:
- 不可变对象天生是线程安全的,无需同步
- 可以在多线程间自由共享
-
简化并发编程:
- 不存在竞态条件
- 可以安全地用作HashMap的键
-
防御性拷贝不再需要:
- 状态不会改变,因此不需要防御性拷贝
- 但仍需注意可变组件
-
失败原子性:
- 操作要么成功,要么失败,没有中间状态
-
缓存友好:
- 对象不变,哈希码可以缓存
6. 不可变类的局限性:
-
为每个不同的值创建新对象:
- 可能导致过多对象创建
- 在某些场景下有性能影响
-
构造复杂对象系统可能较为繁琐:
- 每次修改都需要创建新实例
7. 不可变类设计模式:
- Builder模式:
- 用于创建复杂的不可变对象
- 分离构建过程和表示
1public final class ImmutablePerson {2 private final String name;3 private final int age;4 private final List<String> hobbies;5 6 private ImmutablePerson(Builder builder) {7 this.name = builder.name;8 this.age = builder.age;9 this.hobbies = new ArrayList<>(builder.hobbies);10 }11 12 // Getters...13 14 public static class Builder {15 private String name;16 private int age;17 private List<String> hobbies = new ArrayList<>();18 19 public Builder name(String name) {20 this.name = name;21 return this;22 }23 24 public Builder age(int age) {25 this.age = age;26 return this;27 }28 29 public Builder addHobby(String hobby) {30 this.hobbies.add(hobby);31 return this;32 }33 34 public ImmutablePerson build() {35 return new ImmutablePerson(this);36 }37 }38}- 工厂方法:
- 提供静态工厂方法创建对象
- 隐藏实现细节,增强API灵活性
8. 在现代Java中的应用:
- Java Records (JDK 16+)提供了创建不可变数据类的简洁方式
- Collections.unmodifiableXxx()方法创建不可变集合视图
- Stream API的使用减少了创建中间不可变对象的需求
- Collectors.toUnmodifiableXxx()收集到不可变集合
24. 什么是 Java 的 SPI(Service Provider Interface)机制?
**SPI(Service Provider Interface)**是Java提供的一种服务发现机制,允许应用程序动态地发现和加载服务提供者的实现。SPI机制通过在classpath中的特定目录下定义接口的实现来工作,使得服务提供者和服务使用者解耦。
1. SPI的基本概念:
- 服务接口:定义服务的行为和能力
- 服务提供者:实现服务接口的具体类
- 服务加载器:用于发现和加载服务实现的工具类(java.util.ServiceLoader)
2. SPI的工作流程:
- 定义服务接口
- 创建接口的实现类
- 在META-INF/services/目录下创建以接口全限定名命名的文件
- 在文件中列出实现类的全限定名
- 使用ServiceLoader加载实现类
3. 简单示例:
1) 定义服务接口:
1package com.example.spi;23public interface MessageService {4 String getMessage();5}2) 创建实现类:
1package com.example.spi.impl;23import com.example.spi.MessageService;45public class EmailMessageService implements MessageService {6 @Override7 public String getMessage() {8 return "Email Message";9 }10}1112public class SmsMessageService implements MessageService {13 @Override14 public String getMessage() {15 return "SMS Message";16 }17}3) 创建配置文件:
在META-INF/services/com.example.spi.MessageService文件中添加:
1com.example.spi.impl.EmailMessageService2com.example.spi.impl.SmsMessageService4) 使用ServiceLoader加载服务:
1ServiceLoader<MessageService> serviceLoader = ServiceLoader.load(MessageService.class);2for (MessageService service : serviceLoader) {3 System.out.println(service.getMessage());4}4. SPI在Java核心库中的应用:
-
JDBC驱动加载:
- java.sql.Driver接口
- 各数据库厂商提供Driver实现
- DriverManager通过SPI加载驱动
-
日志框架集成:
- java.util.logging.spi包
- 替换默认日志实现
-
Java扩展机制:
- javax.imageio.spi包的图像I/O
- javax.sound.sampled.spi的声音系统
-
Java加密扩展(JCE):
- java.security.Provider实现
- 加密算法提供者
5. Java 9 模块系统中的SPI:
Java 9引入了模块系统(JPMS),SPI机制有所变化:
- 使用
provides...with...声明服务提供者 - 使用
uses声明服务使用者
1// 模块声明文件(module-info.java)2module com.example.provider {3 exports com.example.spi;4 provides com.example.spi.MessageService with5 com.example.spi.impl.EmailMessageService,6 com.example.spi.impl.SmsMessageService;7}89module com.example.consumer {10 requires com.example.provider;11 uses com.example.spi.MessageService;12}6. SPI的特点和优势:
- 解耦:服务接口与实现分离
- 可扩展:不修改原有代码的情况下增加新功能
- 灵活性:运行时动态选择实现
- 面向接口编程:基于接口而非实现
- 动态服务发现:无需硬编码依赖关系
7. SPI的局限性:
- 延迟加载问题:ServiceLoader会一次性实例化所有服务
- 无法按需加载:缺乏参数化的初始化机制
- 异常处理复杂:创建实例时的异常难以定位
- 不支持命名服务:无法根据名称获取指定实现
- 线程安全问题:ServiceLoader非线程安全
8. SPI的实际应用场景:
- 数据库驱动:MySQL、PostgreSQL等JDBC驱动
- 日志框架:SLF4J、Log4j等
- 加密服务提供者:不同的安全算法实现
- 编译器插件:如Java编译器的注解处理器
- 编解码器:音频、视频编解码器
9. Spring与SPI:
Spring框架提供了类似的机制:
- Spring的@Component扫描机制
- Spring的FactoryBean接口
- Spring Boot的自动配置
10. 与其他设计模式的关系:
- 工厂模式:ServiceLoader作为工厂创建服务实例
- 策略模式:不同实现代表不同策略
- 适配器模式:可用于适配不同服务实现
- 依赖注入:实现服务的依赖反转
25. Java 泛型的作用是什么?
**泛型(Generics)**是Java 5引入的一种特性,允许在定义类、接口和方法时使用类型参数,提供编译时类型安全检查,消除强制类型转换,增强代码的可读性和重用性。
1. 泛型的主要作用:
-
类型安全:
- 编译时类型检查,避免运行时ClassCastException
- 错误提前暴露到编译阶段
-
消除强制类型转换:
- 自动完成类型转换,减少冗余代码
- 提高代码可读性和可维护性
-
实现通用算法:
- 编写适用于多种类型的通用代码
- 实现"一次编写,多处使用"
2. 泛型的基本用法:
泛型类:
1// 定义泛型类2public class Box<T> {3 private T value;4 5 public void set(T value) {6 this.value = value;7 }8 9 public T get() {10 return value;11 }12}1314// 使用泛型类15Box<Integer> intBox = new Box<>();16intBox.set(123);17Integer value = intBox.get(); // 无需类型转换1819Box<String> strBox = new Box<>();20strBox.set("Hello");21String text = strBox.get(); // 无需类型转换泛型方法:
1// 定义泛型方法2public <E> void printArray(E[] array) {3 for (E element : array) {4 System.out.println(element);5 }6}78// 使用泛型方法9Integer[] intArray = {1, 2, 3};10printArray(intArray);1112String[] strArray = {"Hello", "World"};13printArray(strArray);泛型接口:
1// 定义泛型接口2public interface Comparable<T> {3 int compareTo(T o);4}56// 实现泛型接口7public class Person implements Comparable<Person> {8 private String name;9 private int age;10 11 @Override12 public int compareTo(Person other) {13 return Integer.compare(this.age, other.age);14 }15}3. 泛型通配符:
- 无界通配符
<?>:- 表示任何类型
- 主要用于不需要写入的场景
1public void printList(List<?> list) {2 for (Object o : list) {3 System.out.println(o);4 }5}- 上界通配符
<? extends T>:- 表示T或T的子类型
- 主要用于从集合中读取数据
1public void drawShapes(List<? extends Shape> shapes) {2 for (Shape s : shapes) {3 s.draw(); // 安全,因为所有元素至少是Shape4 }5 // shapes.add(new Circle()); // 编译错误,不能添加元素6}- 下界通配符
<? super T>:- 表示T或T的超类型
- 主要用于向集合中写入数据
1public void addRectangles(List<? super Rectangle> shapes) {2 shapes.add(new Rectangle()); // 安全,因为列表接受Rectangle或其超类3 shapes.add(new Square()); // 安全,Square是Rectangle的子类4 // Rectangle r = shapes.get(0); // 编译错误,不能保证读取类型5}4. PECS原则(Producer Extends Consumer Super):
- 如果你从集合中读取数据(Producer),使用
extends - 如果你向集合中写入数据(Consumer),使用
super
1// 生产者场景 - 读取数据2public void copyElements(List<? extends E> src, List<? super E> dest) {3 for (E e : src) { // src是生产者4 dest.add(e); // dest是消费者5 }6}5. 类型擦除:
- Java泛型在编译后会进行类型擦除
- 泛型信息不会保留到运行时
- 原始类型替换泛型类型参数
擦除前:
1List<String> list = new ArrayList<String>();2list.add("Hello");3String s = list.get(0);擦除后(概念上):
1List list = new ArrayList();2list.add("Hello");3String s = (String) list.get(0);6. 泛型的限制:
- 不能使用基本类型作为泛型类型参数
- 不能创建泛型类型的实例:
new T() - 不能创建泛型数组:
new T[] - 不能对泛型类型使用
instanceof - 静态字段不能使用泛型类的类型参数
- 异常类不能是泛型的
7. 解决泛型限制的方法:
- 使用类型标记(Class)解决创建实例问题:
1public <T> T createInstance(Class<T> clazz) throws Exception {2 return clazz.getDeclaredConstructor().newInstance();3}- 使用通配符和反射创建泛型数组:
1public <T> T[] createArray(Class<T> clazz, int size) {2 @SuppressWarnings("unchecked")3 T[] array = (T[]) Array.newInstance(clazz, size);4 return array;5}8. 泛型最佳实践:
-
尽量指定具体类型参数:
- 优先使用
List<String>而非原始类型List - 提高代码可读性和类型安全性
- 优先使用
-
合理使用通配符:
- 应用PECS原则
- 集合只读时使用
? extends T - 集合只写时使用
? super T
-
不要忽略编译器警告:
- 解决警告而非抑制
- 必要时使用@SuppressWarnings,但要添加注释说明
-
优先考虑泛型方法:
- 单个方法需要泛型时,使用泛型方法而非泛型类
- 增强方法的适用性
-
明确设计泛型边界:
- 使用有界泛型增加API的表达力
- 例如
<T extends Comparable<T>>
9. 泛型和集合框架:
- Java集合框架广泛使用泛型
- 提供类型安全的容器
- 避免手动类型转换
10. 泛型在框架中的应用:
- Spring的依赖注入
- Hibernate的查询API
- Jackson的JSON处理
- Guava的各种工具类
26. 什么是不可变类?如何创建一个不可变类?
**不可变类(Immutable Class)**是一种一旦创建,其状态(即对象的数据)就不能被修改的类。String、Integer等包装类以及BigDecimal、BigInteger都是Java中的不可变类。
不可变类的特点:
- 创建后状态不能改变
- 所有字段都是final的
- 安全地用于多线程环境,无需同步
- 可以缓存实例,节省内存(如Integer的缓存)
创建不可变类的规则:
-
类声明为final:防止创建子类破坏不可变性
-
所有字段声明为private和final:防止直接访问和修改字段
-
不提供修改字段的方法:不提供setter方法
-
确保可变成员变量的安全:
- 不直接返回对可变对象的引用
- 不存储传入的可变对象引用,而是创建副本
-
构造函数中创建可变对象的副本
示例代码:
1public final class ImmutablePerson {2 private final String name;3 private final int age;4 private final List<String> hobbies;5 6 public ImmutablePerson(String name, int age, List<String> hobbies) {7 this.name = name;8 this.age = age;9 10 // 创建可变对象的副本,而不是存储原始引用11 this.hobbies = new ArrayList<>(hobbies);12 }13 14 public String getName() {15 return name;16 }17 18 public int getAge() {19 return age;20 }21 22 public List<String> getHobbies() {23 // 返回防御性副本,而不是原始引用24 return new ArrayList<>(hobbies);25 }26 27 // 提供修改方法,但返回新的实例,而不是修改现有实例28 public ImmutablePerson withName(String newName) {29 return new ImmutablePerson(newName, this.age, this.hobbies);30 }31 32 public ImmutablePerson withAge(int newAge) {33 return new ImmutablePerson(this.name, newAge, this.hobbies);34 }35 36 public ImmutablePerson withAddedHobby(String hobby) {37 List<String> newHobbies = new ArrayList<>(this.hobbies);38 newHobbies.add(hobby);39 return new ImmutablePerson(this.name, this.age, newHobbies);40 }41}不可变类的优点:
- 线程安全:无需同步,可以安全地在多线程间共享
- 简化开发:无需防御性复制,不用担心状态变化
- 可缓存:可以安全地重用实例,提高性能
- 可作为Map键或Set元素:状态不变,hashCode不变,确保集合正确性
不可变类的缺点:
- 每次修改都创建新对象:可能增加内存消耗和垃圾回收压力
- 需要多个类表示一个概念:需要配套的Builder类以支持方便构建
最佳实践:
- 除非必要,优先考虑使用不可变类
- 使用Builder模式简化复杂不可变对象的创建
- 通过工厂方法提供常用实例,减少对象创建
27. Java中SPI机制是什么?有哪些应用场景?
**SPI(Service Provider Interface)**是Java提供的一种服务发现机制,允许第三方为某个接口提供实现。
SPI的核心思想:当服务提供者提供了一种接口的实现,服务消费者可以通过查找发现并使用该服务,而不需要了解具体实现细节。
SPI的工作原理:
- 定义服务接口
- 提供该接口的实现
- 在实现类的META-INF/services目录下创建以接口全限定名为文件名的文件
- 在文件中写入实现类的全限定名
- 使用ServiceLoader加载实现类
SPI实现步骤示例:
- 定义接口:
1package com.example.spi;23public interface MessageService {4 String getMessage();5}- 提供实现类:
1package com.example.spi.impl;23import com.example.spi.MessageService;45public class EmailMessageService implements MessageService {6 @Override7 public String getMessage() {8 return "Email Message";9 }10}1112public class SMSMessageService implements MessageService {13 @Override14 public String getMessage() {15 return "SMS Message";16 }17}- 创建配置文件:
在
META-INF/services/com.example.spi.MessageService文件中:
1com.example.spi.impl.EmailMessageService2com.example.spi.impl.SMSMessageService- 使用SPI加载服务:
1ServiceLoader<MessageService> serviceLoader = ServiceLoader.load(MessageService.class);2for (MessageService service : serviceLoader) {3 System.out.println(service.getMessage());4}Java SPI的应用场景:
-
JDBC驱动加载:
- Java通过SPI机制加载不同数据库的驱动
- 应用程序只需调用
DriverManager.getConnection(url) - 具体驱动由各数据库厂商实现
-
日志框架:
- SLF4J使用SPI发现日志实现
- 应用代码使用SLF4J API
- 具体实现可以是Log4j、Logback等
-
JavaEE中的组件发现:
- Servlet容器
- JPA提供者
- JAXB实现
-
Spring中的应用:
- Spring Boot自动配置
- Spring的
SpringFactoriesLoader是SPI的一种变体
-
Dubbo的扩展机制:
- Dubbo基于SPI设计了自己的扩展点加载机制
- 支持IOC和AOP特性
SPI的优缺点:
优点:
- 松耦合:服务调用方与提供方解耦
- 可扩展性:系统可以方便地引入新的实现
- 动态加载:运行时发现服务实现
缺点:
- 延迟加载:ServiceLoader加载所有实现,可能影响启动性能
- 并发不安全:ServiceLoader非线程安全
- 异常处理机制不完善:难以定位具体错误
- 功能简单:缺乏IoC、AOP等高级特性
SPI与API的区别:
- API(Application Programming Interface):服务提供方定义接口,供调用方使用
- SPI(Service Provider Interface):调用方定义接口,供服务提供方实现
最佳实践:
- 框架设计时,考虑使用SPI增强扩展性
- 在META-INF/services目录使用UTF-8编码的配置文件
- 在服务提供方模块的pom.xml中配置打包时包含META-INF目录
- 考虑使用增强的SPI框架,如Dubbo的扩展加载机制
28. 什么是强引用、软引用、弱引用和虚引用?
Java提供了四种引用类型,它们在对象生命周期管理和内存回收方面有不同的特性。这些引用类型从强到弱依次是:强引用、软引用、弱引用和虚引用。
1. 强引用(Strong Reference)
- 特点:最常见的引用类型,如
Object obj = new Object() - GC行为:只要对象可达(被强引用),垃圾回收器永远不会回收它
- 使用场景:常规对象引用
- 生命周期:直到引用变量被销毁或显式设置为null
1// 创建强引用2Object strongRef = new Object();3// 销毁引用4strongRef = null; // 对象可能被GC回收2. 软引用(Soft Reference)
- 特点:表示有用但非必需的对象
- GC行为:仅在内存不足时被回收
- 使用场景:实现内存敏感的缓存,当内存不足时自动释放缓存
- 创建方式:使用
java.lang.ref.SoftReference类
1// 创建软引用2Object obj = new Object();3SoftReference<Object> softRef = new SoftReference<>(obj);4obj = null; // 原始强引用置为null56// 使用软引用7Object retrievedObj = softRef.get(); // 可能返回null,如果对象已被GC回收8if (retrievedObj != null) {9 // 使用对象10}3. 弱引用(Weak Reference)
- 特点:比软引用更弱,表示非必需的对象
- GC行为:下一次垃圾回收时无条件回收
- 使用场景:规范化映射(canonical mapping),防止内存泄漏
- 创建方式:使用
java.lang.ref.WeakReference类
1// 创建弱引用2Object obj = new Object();3WeakReference<Object> weakRef = new WeakReference<>(obj);4obj = null; // 原始强引用置为null56// 使用弱引用7Object retrievedObj = weakRef.get(); // 可能返回null8if (retrievedObj != null) {9 // 使用对象10}4. 虚引用(Phantom Reference)
- 特点:最弱的引用类型,不能通过它访问对象
- GC行为:对象被回收时,引用会被加入引用队列
- 使用场景:跟踪对象的回收状态,执行清理操作
- 创建方式:使用
java.lang.ref.PhantomReference类,必须提供引用队列
1// 创建虚引用2Object obj = new Object();3ReferenceQueue<Object> refQueue = new ReferenceQueue<>();4PhantomReference<Object> phantomRef = new PhantomReference<>(obj, refQueue);5obj = null; // 原始强引用置为null67// 无法通过虚引用获取对象8Object retrievedObj = phantomRef.get(); // 总是返回null910// 检查引用是否入队(对象是否已回收)11Reference<?> polledRef = refQueue.poll();12if (polledRef == phantomRef) {13 // 对象已被回收,执行清理操作14}5. 引用队列(Reference Queue)
- 与软引用、弱引用和虚引用一起使用
- 当引用的对象被垃圾回收器回收时,引用对象会被添加到引用队列中
- 可用于在对象回收后执行后续操作
1ReferenceQueue<Object> refQueue = new ReferenceQueue<>();2WeakReference<Object> weakRef = new WeakReference<>(new Object(), refQueue);34// 检查引用队列5Reference<?> polledRef = refQueue.poll();6if (polledRef != null) {7 // 对象已被回收,处理引用8}6. 各引用类型的应用场景
-
强引用:日常编程中的默认引用
-
软引用:
- 实现内存敏感的缓存
- 图片缓存
- 大对象缓存
-
弱引用:
- 实现
WeakHashMap,键不再使用时自动删除条目 - 规范化映射,一个对象对应唯一实例
- 观察者模式中的监听器列表,防止内存泄漏
- 实现
-
虚引用:
- 跟踪对象回收状态
- 管理直接内存资源
- 实现清理/终结机制
7. 不同引用类型的对比
| 引用类型 | 回收时机 | 获取对象 | 引用队列 | 主要用途 |
|---|---|---|---|---|
| 强引用 | 从不 | 直接访问 | 不适用 | 常规对象引用 |
| 软引用 | 内存不足时 | 通过get()方法 | 可选 | 内存敏感的缓存 |
| 弱引用 | 下次GC时 | 通过get()方法 | 可选 | 规范化映射 |
| 虚引用 | 下次GC时 | 不能获取 | 必需 | 资源清理 |
8. 最佳实践
- 在缓存实现中,根据缓存重要性选择软引用或弱引用
- 处理大型数据时,考虑使用软引用减少OOM风险
- 实现观察者模式时,使用弱引用避免内存泄漏
- 需要跟踪对象生命周期时,考虑使用虚引用和引用队列
- 使用引用队列时,通常需要单独的清理线程处理回收的引用
29. ConcurrentHashMap 在 Java 7 和 Java 8 中的实现有何不同?
ConcurrentHashMap是Java中线程安全的HashMap实现,在Java 7和Java 8中有着显著的实现差异。
Java 7中的ConcurrentHashMap
1. 基本结构:
- 采用**分段锁(Segment)**设计
- 继承自ReentrantLock的Segment数组,默认16个
- 每个Segment内部是一个小型的哈希表(HashEntry数组)
2. 工作原理:
- 对整个Map进行分段,每个Segment独立加锁
- 允许多个线程同时操作不同段的数据
- 理论上支持Segment数量的并发度
3. 关键特性:
- 锁粒度:Segment级别,默认16个锁
- 并发度:取决于Segment数量,默认16
- 内部结构:二级哈希表(Segment → HashEntry)
- 扩容机制:每个Segment独立扩容,不影响其他Segment
4. 代码示例:
1// Java 7中的ConcurrentHashMap内部结构(简化版)2static final class Segment<K,V> extends ReentrantLock implements Serializable {3 transient volatile HashEntry<K,V>[] table;4 transient int count;5 // ...6}78static final class HashEntry<K,V> {9 final int hash;10 final K key;11 volatile V value;12 volatile HashEntry<K,V> next;13}Java 8中的ConcurrentHashMap
1. 基本结构:
- 摒弃了分段锁设计
- 采用Node数组+链表+红黑树结构
- 类似于HashMap,但所有操作都保证线程安全
2. 工作原理:
- 使用CAS(Compare and Swap)操作和synchronized关键字
- CAS用于操作Node数组,synchronized用于链表/红黑树操作
- 基于Node的hash值锁定,更细粒度的锁定
3. 关键特性:
- 锁粒度:桶(bucket)级别,比Segment更细
- 并发度:理论上等于桶数量
- 内部结构:数组+链表+红黑树(链表长度>8时转为红黑树)
- 扩容机制:支持并发扩容,多线程协作完成
4. 代码示例:
1// Java 8中的ConcurrentHashMap内部结构(简化版)2static final class Node<K,V> implements Map.Entry<K,V> {3 final int hash;4 final K key;5 volatile V val;6 volatile Node<K,V> next;7}89// 红黑树节点10static final class TreeNode<K,V> extends Node<K,V> {11 TreeNode<K,V> parent;12 TreeNode<K,V> left;13 TreeNode<K,V> right;14 TreeNode<K,V> prev;15 boolean red;16}主要区别对比
| 特性 | Java 7 ConcurrentHashMap | Java 8 ConcurrentHashMap |
|---|---|---|
| 锁机制 | 分段锁(Segment) | synchronized + CAS |
| 锁粒度 | Segment级别(较粗) | 桶级别(更细) |
| 数据结构 | Segment数组 + HashEntry数组 | Node数组 + 链表/红黑树 |
| 树化 | 不支持 | 链表长度>8时转为红黑树 |
| 并发度 | 取决于Segment数量,固定 | 理论上等于桶数量,更高 |
| 扩容机制 | Segment独立扩容 | 并发扩容,多线程协作 |
| 内存占用 | 较高(Segment对象开销) | 较低(无Segment开销) |
| 查询性能 | 较好 | 更好(受益于红黑树) |
| 迭代器 | 弱一致性 | 弱一致性 |
核心API操作对比
1. put操作
- Java 7:获取Segment锁,然后在Segment内部操作
- Java 8:CAS尝试直接插入,失败则对桶加synchronized锁
2. get操作
- Java 7:不加锁,通过volatile保证可见性
- Java 8:不加锁,通过volatile保证可见性
3. size操作
- Java 7:先尝试不加锁统计,失败后锁住所有Segment
- Java 8:使用CounterCell数组,分散计数,提高并发性
4. 扩容
- Java 7:单线程扩容Segment内部的HashEntry数组
- Java 8:多线程协作扩容,每个线程处理一部分桶
性能考虑
-
Java 8优势:
- 锁粒度更细,理论上并发度更高
- 红黑树提高了大容量场景下的查询性能
- 多线程协作扩容,提高扩容效率
-
适用场景:
- 读操作远多于写操作:两个版本都很好
- 写操作频繁:Java 8优势更明显
- 大数据量、高并发:Java 8性能更好
30. Java内存模型(JMM)是什么?它有哪些特性?
**Java内存模型(Java Memory Model,JMM)**是Java虚拟机规范中定义的一种抽象模型,它规定了Java程序中各种变量的访问规则,以及在多线程环境中如何处理共享变量的可见性、原子性和有序性问题。
1. JMM的核心目标
- 定义Java多线程程序中变量的访问规则
- 屏蔽各种硬件和操作系统的内存访问差异
- 提供一致的内存可见性保证
- 规范多线程环境下指令重排序的影响
2. Java内存模型的基本结构
JMM将内存分为主内存和工作内存两部分:
-
主内存(Main Memory):
- 所有线程共享
- 存储Java对象的实例数据
- 包含原始变量(如int、long等)以及引用变量的引用值
-
工作内存(Working Memory):
- 线程私有,类似于CPU缓存
- 存储线程需要的变量的副本
- 线程不能直接访问主内存中的变量
3. JMM的核心特性
3.1 原子性(Atomicity):
- 基本保证:基本类型的读写操作是原子的(long和double除外)
- 增强保证:通过synchronized和Lock等实现复合操作的原子性
- 相关API:java.util.concurrent.atomic包中的原子类
1// 原子变量示例2AtomicInteger counter = new AtomicInteger(0);3counter.incrementAndGet(); // 原子操作3.2 可见性(Visibility):
- 一个线程对共享变量的修改能够及时被其他线程看到
- 实现方式:volatile、synchronized、final关键字
- 可见性不保证原子性
1// volatile保证可见性2private volatile boolean flag = false;34// 线程A5flag = true; // 修改对其他线程立即可见67// 线程B8while (!flag) {9 // 能够感知flag的变化10}3.3 有序性(Ordering):
- 程序按代码顺序执行
- JVM为优化性能可能进行指令重排序
- 重排序不会改变单线程执行结果,但会影响多线程执行结果
- 通过volatile、synchronized、Lock等保证有序性
1// 重排序示例2int a = 0;3boolean flag = false;45// 可能的重排序6flag = true;7a = 1;89// 通过volatile防止重排序10private volatile int a = 0;11private volatile boolean flag = false;4. Happens-Before原则
JMM定义了多种Happens-Before规则,保证线程间的操作可见性:
- 程序顺序规则:单线程内按代码顺序执行
- 监视器锁规则:释放锁操作happens-before于随后对同一锁的获取
- volatile变量规则:写volatile变量happens-before于随后对这个变量的读
- 线程启动规则:启动线程的操作happens-before于被启动线程的任何操作
- 线程终止规则:线程中的所有操作happens-before于其他线程感知到该线程结束
- 中断规则:调用线程的interrupt()方法happens-before于被中断线程检测到中断事件
- 终结器规则:对象的构造函数执行结束happens-before于终结器(finalize)方法
- 传递性:如果A happens-before B,且B happens-before C,则A happens-before C
5. 内存屏障(Memory Barriers)
JMM使用内存屏障来禁止特定类型的处理器重排序:
- LoadLoad屏障:确保Load1先于Load2完成
- StoreStore屏障:确保Store1先于Store2完成
- LoadStore屏障:确保Load1先于Store2完成
- StoreLoad屏障:确保Store1先于Load2完成(全能屏障)
6. volatile关键字的内存语义
- 可见性:保证变量的修改对所有线程立即可见
- 有序性:禁止指令重排序
- 实现机制:
- 写volatile变量时,会插入StoreStore和StoreLoad屏障
- 读volatile变量时,会插入LoadLoad和LoadStore屏障
1// volatile变量使用场景2public class SafePublication {3 private volatile Object instance;4 5 public void publish() {6 instance = new Object(); // 写volatile变量7 }8 9 public Object getInstance() {10 return instance; // 读volatile变量11 }12}7. final关键字的内存语义
- 保证final字段在构造函数完成时被正确初始化
- JMM会禁止final字段的初始化被重排序到构造函数之外
- 保证其他线程看到的final字段值都是初始化后的值
8. synchronized的内存语义
- 获取锁:会清空工作内存,从主内存重新加载共享变量
- 释放锁:会将工作内存中的修改刷新到主内存
- 提供了互斥性和可见性保证
9. JMM对并发编程的影响
- 单例模式实现:双重检查锁定需要volatile修饰实例变量
1public class Singleton {2 private static volatile Singleton instance;3 4 private Singleton() {}5 6 public static Singleton getInstance() {7 if (instance == null) {8 synchronized (Singleton.class) {9 if (instance == null) {10 instance = new Singleton();11 }12 }13 }14 return instance;15 }16}44. Java注解是什么?如何自定义注解及其应用场景?
**注解(Annotations)**是Java 5引入的一种特殊类型的标记,可以应用于类、方法、字段、参数和其他程序元素。它们提供了一种声明式的编程方式,使开发者能够向代码中添加元数据信息,这些信息可以被编译器、运行时环境或第三方工具使用。
1. 注解的基础概念
1.1 注解的定义
注解以@interface关键字定义:
1public @interface MyAnnotation {2 String value() default "defaultValue";3 int count() default 0;4 boolean enabled() default true;5}1.2 元注解
元注解是用于注解其他注解的注解,Java提供了几个标准的元注解:
-
@Retention:指定注解的保留策略
- SOURCE:只在源码中保留,编译时丢弃
- CLASS:保留在编译后的类文件中,但运行时不可用(默认)
- RUNTIME:保留在运行时,可以通过反射访问
-
@Target:指定注解可以应用于哪些程序元素
- TYPE:类、接口、枚举
- FIELD:字段
- METHOD:方法
- PARAMETER:方法参数
- CONSTRUCTOR:构造方法
- LOCAL_VARIABLE:局部变量
- ANNOTATION_TYPE:注解类型
- PACKAGE:包
- TYPE_PARAMETER:Java 8+,类型参数
- TYPE_USE:Java 8+,类型使用
-
@Documented:指定该注解是否包含在JavaDoc文档中
-
@Inherited:指定该注解可以被子类继承
-
@Repeatable:Java 8+,指定该注解可以在同一程序元素上多次使用
1.3 常见的内置注解
Java提供了一些内置的注解:
- @Override:表示方法重写父类方法
- @Deprecated:表示元素已过时
- @SuppressWarnings:抑制编译器警告
- @FunctionalInterface:Java 8+,表示函数式接口
- @SafeVarargs:Java 7+,抑制可变参数相关的警告
- @Native:Java 8+,表示常量可以被本地代码引用
2. 自定义注解
2.1 创建自定义注解
1// 自定义运行时注解2@Retention(RetentionPolicy.RUNTIME)3@Target({ElementType.TYPE, ElementType.METHOD})4public @interface Authorized {5 String[] roles() default {"USER"};6 boolean guestAllowed() default false;7}89// 自定义编译时注解10@Retention(RetentionPolicy.SOURCE)11@Target(ElementType.METHOD)12public @interface MethodInfo {13 String author() default "unknown";14 String date();15 String comment() default "";16}2.2 自定义可重复注解
1// 定义容器注解2@Retention(RetentionPolicy.RUNTIME)3@Target(ElementType.METHOD)4public @interface Schedules {5 Schedule[] value();6}78// 定义可重复的注解9@Retention(RetentionPolicy.RUNTIME)10@Target(ElementType.METHOD)11@Repeatable(Schedules.class)12public @interface Schedule {13 String cron();14 String description() default "";15}1617// 使用可重复注解18@Schedule(cron = "0 0 12 * * ?", description = "Daily job")19@Schedule(cron = "0 0 0 * * ?", description = "Nightly job")20public void periodicJob() {21 // 方法实现22}2.3 注解的使用方式
1// 应用在类上2@Authorized(roles = {"ADMIN", "MANAGER"})3public class AdminController {4 5 // 应用在方法上6 @Authorized(roles = {"ADMIN"})7 public void deleteUser(int userId) {8 // 方法实现9 }10 11 // 应用在方法上,使用默认值12 @Authorized13 public void viewUsers() {14 // 方法实现15 }16}1718// 编译时注解应用19@MethodInfo(author = "John", date = "2023-10-15", comment = "Initial implementation")20public void processData() {21 // 方法实现22}3. 注解处理方式
注解本身不会做任何事情,它们需要被处理才能发挥作用。处理注解的方式主要有两种:编译时处理和运行时处理。
3.1 运行时处理(通过反射API)
1// 运行时处理注解示例2public class AuthorizationHandler {3 public static boolean isAuthorized(Object instance, String methodName, String[] userRoles) {4 try {5 Class<?> clazz = instance.getClass();6 7 // 检查类级别的授权8 if (clazz.isAnnotationPresent(Authorized.class)) {9 Authorized auth = clazz.getAnnotation(Authorized.class);10 if (!hasCommonElement(auth.roles(), userRoles) && !auth.guestAllowed()) {11 return false;12 }13 }14 15 // 检查方法级别的授权16 Method method = clazz.getMethod(methodName);17 if (method.isAnnotationPresent(Authorized.class)) {18 Authorized auth = method.getAnnotation(Authorized.class);19 return hasCommonElement(auth.roles(), userRoles) || auth.guestAllowed();20 }21 22 return true; // 如果没有注解,默认授权23 } catch (NoSuchMethodException e) {24 return false; // 方法不存在25 }26 }27 28 private static boolean hasCommonElement(String[] array1, String[] array2) {29 for (String str1 : array1) {30 for (String str2 : array2) {31 if (str1.equals(str2)) {32 return true;33 }34 }35 }36 return false;37 }38}3940// 使用处理器41public void executeMethod(Object controller, String methodName) {42 String[] userRoles = getCurrentUserRoles(); // 获取当前用户角色43 if (AuthorizationHandler.isAuthorized(controller, methodName, userRoles)) {44 // 调用方法45 // ...46 } else {47 throw new SecurityException("Unauthorized access");48 }49}3.2 编译时处理(通过Annotation Processing Tool)
使用Java的注解处理器API,可以在编译时处理注解:
1@SupportedAnnotationTypes("com.example.MethodInfo")2@SupportedSourceVersion(SourceVersion.RELEASE_8)3public class MethodInfoProcessor extends AbstractProcessor {4 5 @Override6 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {7 for (TypeElement annotation : annotations) {8 Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);9 10 for (Element element : elements) {11 if (element.getKind() == ElementKind.METHOD) {12 MethodInfo methodInfo = element.getAnnotation(MethodInfo.class);13 14 // 获取注解信息15 String author = methodInfo.author();16 String date = methodInfo.date();17 String comment = methodInfo.comment();18 19 // 处理注解,例如生成文档或者验证20 processingEnv.getMessager().printMessage(21 Diagnostic.Kind.NOTE,22 "Method: " + element.getSimpleName() +23 ", Author: " + author +24 ", Date: " + date +25 ", Comment: " + comment,26 element27 );28 }29 }30 }31 32 return true;33 }34}要使用这个注解处理器,需要:
- 将处理器打包成JAR文件
- 在
META-INF/services/javax.annotation.processing.Processor文件中注册 - 在编译时包含该JAR文件
4. 注解的应用场景
4.1 框架配置和元数据
例如Spring框架中的注解用于配置:
1@Controller2@RequestMapping("/users")3public class UserController {4 @Autowired5 private UserService userService;6 7 @GetMapping("/{id}")8 public ResponseEntity<User> getUser(@PathVariable("id") Long id) {9 // 实现10 }11}4.2 编译时检查和代码生成
例如Lombok库使用注解来生成代码:
1@Data // 自动生成getter、setter、equals、hashCode、toString2@Builder // 生成建造者模式代码3public class User {4 private Long id;5 private String username;6 private String email;7}4.3 运行时行为修改
例如在Web应用中进行权限检查:
1@RequiresRole("admin")2public void deleteUser(Long userId) {3 // 实现4}4.4 文档生成
例如Swagger/OpenAPI注解用于生成API文档:
1@ApiOperation(value = "Get user by ID", response = User.class)2@GetMapping("/{id}")3public ResponseEntity<User> getUser(@PathVariable @ApiParam(value = "User ID") Long id) {4 // 实现5}4.5 单元测试
JUnit框架中使用注解标记测试方法:
1@Test2public void testUserRegistration() {3 // 测试逻辑4}56@Before7public void setup() {8 // 设置测试环境9}1011@After12public void cleanup() {13 // 清理测试环境14}4.6 依赖注入和控制反转
例如在Spring框架中:
1@Component2public class UserServiceImpl implements UserService {3 @Autowired4 private UserRepository userRepository;5 6 // 实现方法7}5. 实际应用案例
5.1 自定义验证注解
1// 定义验证注解2@Retention(RetentionPolicy.RUNTIME)3@Target(ElementType.FIELD)4public @interface ValidateEmail {5 String message() default "Invalid email format";6 boolean nullable() default false;7}89// 定义处理器10public class ValidationProcessor {11 public List<String> validate(Object object) {12 List<String> errors = new ArrayList<>();13 Class<?> clazz = object.getClass();14 15 for (Field field : clazz.getDeclaredFields()) {16 if (field.isAnnotationPresent(ValidateEmail.class)) {17 ValidateEmail annotation = field.getAnnotation(ValidateEmail.class);18 field.setAccessible(true);19 20 try {21 String email = (String) field.get(object);22 if (email == null) {23 if (!annotation.nullable()) {24 errors.add(field.getName() + ": Email cannot be null");25 }26 } else if (!isValidEmail(email)) {27 errors.add(field.getName() + ": " + annotation.message());28 }29 } catch (IllegalAccessException e) {30 errors.add("Cannot access field: " + field.getName());31 }32 }33 }34 35 return errors;36 }37 38 private boolean isValidEmail(String email) {39 // 简单的邮箱验证逻辑40 return email.matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");41 }42}4344// 使用验证注解45public class User {46 private Long id;47 private String name;48 49 @ValidateEmail(message = "Please provide a valid email address")50 private String email;51 52 // 构造方法、getter和setter53}5455// 验证对象56User user = new User(1L, "John Doe", "invalid-email");57ValidationProcessor validator = new ValidationProcessor();58List<String> errors = validator.validate(user);59if (!errors.isEmpty()) {60 for (String error : errors) {61 System.out.println("Validation error: " + error);62 }63}5.2 ORM映射注解
1// 表注解2@Retention(RetentionPolicy.RUNTIME)3@Target(ElementType.TYPE)4public @interface Table {5 String name();6}78// 列注解9@Retention(RetentionPolicy.RUNTIME)10@Target(ElementType.FIELD)11public @interface Column {12 String name() default "";13 boolean primary() default false;14}1516// 实体类17@Table(name = "users")18public class User {19 @Column(name = "id", primary = true)20 private Long id;21 22 @Column(name = "username")23 private String username;24 25 @Column(name = "email")26 private String email;27 28 // 构造方法、getter和setter29}3031// ORM处理器32public class OrmMapper {33 public String generateInsertSql(Object entity) {34 Class<?> clazz = entity.getClass();35 if (!clazz.isAnnotationPresent(Table.class)) {36 throw new IllegalArgumentException("Class must be annotated with @Table");37 }38 39 Table tableAnnotation = clazz.getAnnotation(Table.class);40 String tableName = tableAnnotation.name();41 42 List<String> columnNames = new ArrayList<>();43 List<String> paramPlaceholders = new ArrayList<>();44 List<Object> values = new ArrayList<>();45 46 try {47 for (Field field : clazz.getDeclaredFields()) {48 if (field.isAnnotationPresent(Column.class)) {49 field.setAccessible(true);50 Column column = field.getAnnotation(Column.class);51 52 // 跳过自增主键53 if (column.primary() && field.get(entity) == null) {54 continue;55 }56 57 String columnName = column.name().isEmpty() ? 58 field.getName() : column.name();59 60 columnNames.add(columnName);61 paramPlaceholders.add("?");62 values.add(field.get(entity));63 }64 }65 } catch (IllegalAccessException e) {66 throw new RuntimeException("Error accessing fields", e);67 }68 69 StringBuilder sql = new StringBuilder();70 sql.append("INSERT INTO ")71 .append(tableName)72 .append(" (")73 .append(String.join(", ", columnNames))74 .append(") VALUES (")75 .append(String.join(", ", paramPlaceholders))76 .append(")");77 78 return sql.toString();79 // 实际应用中还需要一个方法来返回values列表用于设置PreparedStatement参数80 }81}6. 注解的最佳实践
6.1 命名和组织
- 使用清晰、描述性的名称
- 将相关注解组织在同一个包中
- 遵循Java命名约定,使用驼峰式命名
6.2 文档和注释
- 为每个注解提供清晰的JavaDoc
- 解释注解的目的、用法和限制
- 提供使用示例
6.3 设计考虑
- 确保注解定义简单明了
- 提供合理的默认值
- 避免过度使用注解
- 考虑注解的组合使用
- 注解应该是声明性的,不应包含业务逻辑
6.4 处理器设计
- 使注解处理器可重用
- 提供明确的错误信息
- 确保处理器的线程安全性
- 缓存反射结果以提高性能
6.5 安全考虑
- 验证注解的使用是否符合预期
- 防止恶意代码利用注解处理器
- 考虑注解在反射中的安全影响
7. Java注解与其他技术的比较
7.1 注解 vs XML配置
优点:
- 类型安全,编译时检查
- 与代码放在一起,易于维护
- 代码更简洁
缺点:
- 修改需要重新编译
- 不适合频繁变化的配置
- 对代码有侵入性
7.2 注解 vs 代码约定
优点:
- 更明确的语义
- IDE支持更好
- 可以包含元数据
缺点:
- 引入了额外的概念
- 可能使简单的事情复杂化
8. Java注解的局限性
- 只能包含常量值,不能包含代码块
- 不能直接继承其他注解
- 处理注解需要额外的代码
- 过度使用会导致代码可读性下降
- 依赖反射可能影响性能
9. 未来发展趋势
- 更强大的编译时注解处理
- 与模块系统的更好集成
- 更丰富的元注解
- 改进的泛型和注解的结合使用
- 无锁并发编程:依赖于对JMM的深入理解
- 线程池优化:合理设置工作线程数,避免过多的上下文切换
- 避免伪共享:对齐内存以提高缓存效率
10. JMM与硬件内存架构的关系
- JMM是一个抽象模型,与具体硬件无关
- JVM通过内存屏障等机制将JMM映射到不同硬件架构
- 屏蔽了不同平台内存模型的差异,实现"一次编写,到处运行"
11. JDK 9及以后的改进
- 增强了volatile的语义
- 改进了final字段的初始化保证
- 引入VarHandle API,提供更细粒度的内存访问控制
最佳实践:
- 优先使用高级并发工具(java.util.concurrent包)
- 正确使用synchronized、volatile和final
- 避免复杂的无锁算法,除非有性能瓶颈
- 理解内存可见性问题,避免常见并发陷阱
- 编写线程安全代码时,考虑原子性、可见性和有序性三个维度
31. Java 8中的Stream API有什么特点?如何使用?
Stream API是Java 8引入的一种处理集合的方式,它提供了一种高效且易于使用的处理数据的方法,支持函数式编程风格的操作。
1. Stream API的核心特点
- 函数式编程:使用Lambda表达式和方法引用进行操作
- 声明式编程:关注"做什么"而非"怎么做"
- 流水线操作:支持中间操作和终端操作链式调用
- 延迟执行:中间操作不会立即执行,直到遇到终端操作
- 内部迭代:由Stream API控制迭代,而非由用户代码显式控制
2. Stream的创建方式
1// 从集合创建2List<String> list = Arrays.asList("a", "b", "c");3Stream<String> stream1 = list.stream();4Stream<String> parallelStream = list.parallelStream(); // 并行流56// 从数组创建7String[] array = {"a", "b", "c"};8Stream<String> stream2 = Arrays.stream(array);910// Stream.of静态方法11Stream<String> stream3 = Stream.of("a", "b", "c");1213// 使用Stream.generate创建无限流14Stream<String> stream4 = Stream.generate(() -> "element").limit(10);1516// 使用Stream.iterate创建无限流17Stream<Integer> stream5 = Stream.iterate(0, n -> n + 2).limit(10);1819// 使用IntStream、LongStream、DoubleStream创建数值流20IntStream intStream = IntStream.range(1, 5); // 1, 2, 3, 421LongStream longStream = LongStream.rangeClosed(1, 5); // 1, 2, 3, 4, 53. Stream的操作类型
Stream API操作分为两类:中间操作(Intermediate Operations)和终端操作(Terminal Operations)。
3.1 中间操作:返回一个新的Stream,可以链式调用
-
过滤操作:
filter(Predicate<T>): 过滤元素distinct(): 去重limit(long): 限制元素数量skip(long): 跳过元素
-
映射操作:
map(Function<T, R>): 元素转换flatMap(Function<T, Stream<R>>): 压平嵌套流mapToInt/Long/Double(): 转换为原始类型流
-
排序操作:
sorted(): 自然顺序排序sorted(Comparator<T>): 指定排序方式
3.2 终端操作:触发流的计算,返回非Stream结果
-
匹配操作:
anyMatch(Predicate<T>): 任一元素匹配allMatch(Predicate<T>): 所有元素匹配noneMatch(Predicate<T>): 没有元素匹配
-
查找操作:
findFirst(): 返回第一个元素findAny(): 返回任意元素
-
归约操作:
reduce(BinaryOperator<T>): 将流元素组合reduce(T, BinaryOperator<T>): 指定初始值的归约count(): 计数max(Comparator<T>): 最大值min(Comparator<T>): 最小值
-
收集操作:
collect(Collector<T, A, R>): 收集到容器toArray(): 转为数组
-
遍历操作:
forEach(Consumer<T>): 对每个元素执行操作
4. Stream API的常见用例
4.1 过滤和收集:
1List<String> filtered = persons.stream()2 .filter(p -> p.getAge() > 18)3 .map(Person::getName)4 .collect(Collectors.toList());4.2 统计操作:
1// 计算总和2int sum = numbers.stream()3 .mapToInt(Integer::intValue)4 .sum();56// 计算平均值7double average = persons.stream()8 .mapToInt(Person::getAge)9 .average()10 .orElse(0);1112// 分组统计13Map<Department, Long> countByDept = employees.stream()14 .collect(Collectors.groupingBy(15 Employee::getDepartment,16 Collectors.counting()17 ));4.3 查找和匹配:
1// 查找任意一个成年人2Optional<Person> adult = persons.stream()3 .filter(p -> p.getAge() >= 18)4 .findAny();56// 检查是否所有人都成年7boolean allAdult = persons.stream()8 .allMatch(p -> p.getAge() >= 18);4.4 归约和汇总:
1// 计算总和2int total = numbers.stream()3 .reduce(0, Integer::sum);45// 连接字符串6String joined = strings.stream()7 .reduce("", String::concat);89// 使用joining更高效连接字符串10String joinedBetter = strings.stream()11 .collect(Collectors.joining(", "));4.5 分组和分区:
1// 按部门分组2Map<Department, List<Employee>> byDept = employees.stream()3 .collect(Collectors.groupingBy(Employee::getDepartment));45// 按成年与否分区6Map<Boolean, List<Person>> adultPartition = persons.stream()7 .collect(Collectors.partitioningBy(p -> p.getAge() >= 18));5. 并行流(Parallel Stream)
Stream API提供了并行处理能力,可以利用多核处理器的优势:
1// 创建并行流2Stream<String> parallelStream = list.parallelStream();3// 或将顺序流转换为并行流4Stream<String> parallel = stream.parallel();56// 并行处理示例7long count = persons.parallelStream()8 .filter(p -> p.getAge() > 18)9 .count();并行流注意事项:
- 确保操作无状态且线程安全
- 避免使用共享状态
- 对于较小的数据集,顺序流可能更快
- 某些操作(如findFirst)在并行流上效率可能更低
- 使用有序集合时,排序操作可能抵消并行优势
6. Collectors工具类
java.util.stream.Collectors提供了多种预定义的收集器:
1// 收集到List2List<String> list = stream.collect(Collectors.toList());34// 收集到Set5Set<String> set = stream.collect(Collectors.toSet());67// 收集到Map8Map<Integer, String> map = persons.stream()9 .collect(Collectors.toMap(Person::getId, Person::getName));1011// 连接字符串12String joined = stream.collect(Collectors.joining(", "));1314// 分组15Map<Department, List<Employee>> byDept = employees.stream()16 .collect(Collectors.groupingBy(Employee::getDepartment));1718// 嵌套分组19Map<Department, Map<EmployeeType, List<Employee>>> byDeptAndType = 20 employees.stream()21 .collect(Collectors.groupingBy(22 Employee::getDepartment,23 Collectors.groupingBy(Employee::getType)24 ));2526// 计算统计信息27DoubleSummaryStatistics stats = employees.stream()28 .collect(Collectors.summarizingDouble(Employee::getSalary));7. Stream API的优势和限制
优势:
- 代码简洁清晰,提高可读性
- 支持函数式编程范式
- 延迟执行提高效率
- 并行处理能力
- 内部迭代减少模板代码
限制:
- 不能修改源数据结构
- 只能遍历一次
- 不保证执行顺序(除非明确排序)
- 调试复杂性增加
8. Stream API的最佳实践
- 优先使用特定类型的流(IntStream、LongStream、DoubleStream)处理数值
- 适当使用并行流提高性能
- 避免在流操作中修改外部状态
- 链式调用保持代码简洁
- 使用方法引用代替简单的Lambda表达式
- 对于复杂的收集操作,优先使用Collectors工具类
- 考虑使用peek()方法进行调试
32. 什么是FunctionalInterface?Java 8中的函数式接口有哪些?
**函数式接口(FunctionalInterface)**是Java 8引入的一个重要概念,指只有一个抽象方法的接口,用于支持Lambda表达式和方法引用。
1. 函数式接口的特点
- 只包含一个抽象方法(Single Abstract Method,简称SAM)
- 可以包含多个默认方法和静态方法
- 可以包含Object类中的public方法的抽象形式
- 可以使用
@FunctionalInterface注解标记(非必须但推荐)
2. @FunctionalInterface注解
@FunctionalInterface是一个标记注解,用于表示一个接口是函数式接口:
1@FunctionalInterface2public interface MyFunctional {3 void execute(); // 唯一的抽象方法4 5 // 默认方法,不影响函数式接口的性质6 default void defaultMethod() {7 System.out.println("Default Method");8 }9 10 // 静态方法,不影响函数式接口的性质11 static void staticMethod() {12 System.out.println("Static Method");13 }14 15 // Object类的方法,不计入抽象方法数量16 @Override17 boolean equals(Object obj);18}3. Java 8内置的函数式接口
Java 8在java.util.function包中提供了多个标准的函数式接口,分为以下几类:
3.1 基础函数式接口
-
Consumer:接收一个输入参数但不返回任何结果
java1@FunctionalInterface2public interface Consumer<T> {3 void accept(T t);4}5// 使用示例6Consumer<String> printer = s -> System.out.println(s);7printer.accept("Hello World"); -
Supplier:不接收参数但返回一个结果
java1@FunctionalInterface2public interface Supplier<T> {3 T get();4}5// 使用示例6Supplier<String> supplier = () -> "Hello World";7String result = supplier.get(); -
Function:接收一个输入参数并返回一个结果
java1@FunctionalInterface2public interface Function<T, R> {3 R apply(T t);4}5// 使用示例6Function<String, Integer> length = s -> s.length();7Integer len = length.apply("Hello"); -
Predicate:接收一个参数,返回一个布尔结果
java1@FunctionalInterface2public interface Predicate<T> {3 boolean test(T t);4}5// 使用示例6Predicate<String> isEmpty = s -> s.isEmpty();7boolean empty = isEmpty.test("Hello"); -
UnaryOperator:接收一个参数,返回相同类型的结果
java1@FunctionalInterface2public interface UnaryOperator<T> extends Function<T, T> {3}4// 使用示例5UnaryOperator<String> toUpperCase = s -> s.toUpperCase();6String upper = toUpperCase.apply("hello"); -
BinaryOperator:接收两个相同类型参数,返回相同类型的结果
java1@FunctionalInterface2public interface BinaryOperator<T> extends BiFunction<T, T, T> {3}4// 使用示例5BinaryOperator<Integer> add = (a, b) -> a + b;6Integer sum = add.apply(5, 3);
3.2 特殊类型的函数式接口
-
BiConsumer:接收两个输入参数,不返回结果
java1@FunctionalInterface2public interface BiConsumer<T, U> {3 void accept(T t, U u);4}5// 使用示例6BiConsumer<String, Integer> printEntry = (key, value) ->7 System.out.println(key + ": " + value);8printEntry.accept("Age", 30); -
BiFunction:接收两个输入参数,返回一个结果
java1@FunctionalInterface2public interface BiFunction<T, U, R> {3 R apply(T t, U u);4}5// 使用示例6BiFunction<String, String, Integer> sumLengths =7 (s1, s2) -> s1.length() + s2.length();8Integer totalLength = sumLengths.apply("Hello", "World"); -
BiPredicate:接收两个输入参数,返回一个布尔结果
java1@FunctionalInterface2public interface BiPredicate<T, U> {3 boolean test(T t, U u);4}5// 使用示例6BiPredicate<String, Integer> checkLength =7 (s, len) -> s.length() == len;8boolean result = checkLength.test("Hello", 5);
3.3 原始类型特化的函数式接口
为避免基本类型的装箱拆箱开销,Java 8提供了针对基本类型的特化接口:
-
IntFunction,LongFunction,DoubleFunction: 接收一个基本类型参数,返回泛型结果
-
ToIntFunction,*ToLongFunction,ToDoubleFunction: 接收一个泛型参数,返回基本类型结果
-
IntConsumer,LongConsumer,DoubleConsumer: 接收一个基本类型参数,不返回结果
-
IntPredicate,LongPredicate,DoublePredicate: 接收一个基本类型参数,返回布尔结果
-
IntSupplier,LongSupplier,DoubleSupplier: 不接收参数,返回基本类型结果
-
IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator: 接收一个基本类型参数,返回相同类型结果
1// 使用示例2IntFunction<String> intToString = i -> Integer.toString(i);3String str = intToString.apply(42);45IntConsumer printer = i -> System.out.println(i);6printer.accept(42);78IntPredicate isEven = i -> i % 2 == 0;9boolean even = isEven.test(42);4. 多个函数式接口的组合
函数式接口通常提供了组合方法,允许通过默认方法组合多个操作:
1// Predicate组合2Predicate<String> isEmpty = String::isEmpty;3Predicate<String> isNotEmpty = isEmpty.negate();4Predicate<String> isNotEmptyAndLong = isNotEmpty.and(s -> s.length() > 10);56// Function组合 (andThen和compose)7Function<String, String> toUpperCase = String::toUpperCase;8Function<String, String> trim = String::trim;9Function<String, String> trimAndUpperCase = trim.andThen(toUpperCase);10Function<String, String> upperCaseAndTrim = trim.compose(toUpperCase);1112// Consumer组合13Consumer<String> logger = s -> System.out.println("Log: " + s);14Consumer<String> printer = s -> System.out.println("Print: " + s);15Consumer<String> logAndPrint = logger.andThen(printer);5. 自定义函数式接口
可以根据需要定义自己的函数式接口,只需确保它只有一个抽象方法:
1@FunctionalInterface2public interface TriFunction<T, U, V, R> {3 R apply(T t, U u, V v);4}56// 使用示例7TriFunction<String, String, String, Integer> calculateLength = 8 (s1, s2, s3) -> s1.length() + s2.length() + s3.length();9Integer length = calculateLength.apply("Hello", "Functional", "Interface");6. Lambda表达式和方法引用
函数式接口与Lambda表达式和方法引用密切相关:
Lambda表达式作为函数式接口的实例:
1Predicate<String> isEmpty = s -> s.isEmpty();2Consumer<String> printer = s -> System.out.println(s);方法引用作为函数式接口的实例:
1// 静态方法引用2Function<String, Integer> parseInt = Integer::parseInt;34// 实例方法引用(特定实例)5Consumer<String> printer = System.out::println;67// 实例方法引用(任意实例)8Function<String, Integer> getLength = String::length;910// 构造方法引用11Supplier<List<String>> newList = ArrayList::new;12Function<String, Integer> newInteger = Integer::new;7. 常见应用场景
函数式接口常用于以下场景:
-
集合操作:Stream API操作、集合排序等
java1// 列表排序2list.sort(Comparator.comparing(Person::getName));34// Stream操作5list.stream()6 .filter(p -> p.getAge() > 18)7 .map(Person::getName)8 .forEach(System.out::println); -
异步任务和回调:
java1// 线程2new Thread(() -> System.out.println("Running in a thread")).start();34// 任务提交5ExecutorService executor = Executors.newSingleThreadExecutor();6executor.submit(() -> performTask()); -
事件处理:
java1button.addActionListener(e -> System.out.println("Button clicked")); -
策略模式:
java1public void processStrings(List<String> strings, Predicate<String> filter) {2 for (String s : strings) {3 if (filter.test(s)) {4 System.out.println(s);5 }6 }7}89// 使用不同策略10processStrings(list, s -> s.startsWith("A"));11processStrings(list, s -> s.length() > 5); -
延迟执行/懒加载:
java1public String getExpensiveValue(Supplier<String> supplier) {2 // 只在需要时调用get()3 if (isRequired()) {4 return supplier.get(); // 延迟计算5 }6 return "Default";7}
8. 最佳实践
- 使用
@FunctionalInterface注解标记函数式接口 - 优先使用Java标准库提供的函数式接口
- 在需要重用的Lambda表达式中,考虑使用方法引用
- 利用函数式接口的组合方法构建复杂行为
- 为提高性能,针对基本类型使用特化的函数式接口
- 不要过度使用Lambda表达式,保持代码可读性
- 在文档中清晰说明函数式接口的行为
33. 什么是Java的方法引用?如何使用?
**方法引用(Method Reference)**是Java 8引入的一种语法糖,作为Lambda表达式的简化形式,使代码更加简洁易读。当Lambda表达式的全部内容仅仅是调用一个已存在的方法时,可以使用方法引用来替代。
1. 方法引用的语法
方法引用使用双冒号::操作符,有以下四种形式:
- 静态方法引用:
类名::静态方法名 - 特定对象的实例方法引用:
对象实例::实例方法名 - 特定类型的任意对象的实例方法引用:
类名::实例方法名 - 构造方法引用:
类名::new
2. 方法引用的类型详解
2.1 静态方法引用
引用一个类的静态方法。语法:ClassName::staticMethodName
1// Lambda表达式2Function<String, Integer> parser1 = s -> Integer.parseInt(s);34// 等价的方法引用5Function<String, Integer> parser2 = Integer::parseInt;67// 使用示例8int value = parser2.apply("123"); // 返回1232.2 特定对象的实例方法引用
引用一个特定对象实例的方法。语法:objectReference::instanceMethodName
1// Lambda表达式2Consumer<String> printer1 = s -> System.out.println(s);34// 等价的方法引用5Consumer<String> printer2 = System.out::println;67// 使用示例8printer2.accept("Hello World"); // 打印"Hello World"910// 自定义对象的方法引用11StringBuilder sb = new StringBuilder();12Consumer<String> appender1 = s -> sb.append(s);13Consumer<String> appender2 = sb::append;2.3 特定类型的任意对象的实例方法引用
引用特定类型的任意对象的实例方法。语法:ClassName::instanceMethodName
这种形式适用于Lambda表达式的第一个参数是方法的接收者(调用者),剩余参数是方法的参数。
1// Lambda表达式2Function<String, Integer> lengthFunc1 = s -> s.length();34// 等价的方法引用5Function<String, Integer> lengthFunc2 = String::length;67// 使用示例8int len = lengthFunc2.apply("Hello"); // 返回5910// 带参数的方法11BiPredicate<String, String> contains1 = (s, prefix) -> s.startsWith(prefix);12BiPredicate<String, String> contains2 = String::startsWith;2.4 构造方法引用
引用一个类的构造方法。语法:ClassName::new
1// 无参构造方法引用2Supplier<List<String>> listFactory1 = () -> new ArrayList<>();3Supplier<List<String>> listFactory2 = ArrayList::new;45// 使用示例6List<String> list = listFactory2.get(); // 创建新的ArrayList实例78// 带参数的构造方法引用9Function<Integer, List<String>> sizedListFactory = ArrayList::new;10List<String> sizedList = sizedListFactory.apply(10); // 创建初始容量为10的ArrayList1112// 多参数构造方法13BiFunction<String, Integer, Person> personCreator = Person::new;14Person person = personCreator.apply("John", 25); // 调用Person(String name, int age)构造方法3. Lambda表达式与方法引用的对比
3.1 Lambda表达式转换为方法引用的条件:
- Lambda表达式的全部内容是调用一个已存在的方法
- 不对参数做任何修改就传入方法
- 不对方法返回值做任何修改
3.2 对比示例:
1// 示例1:直接调用方法2// Lambda表达式3Consumer<String> printer1 = s -> System.out.println(s);4// 方法引用5Consumer<String> printer2 = System.out::println;67// 示例2:调用参数的方法8// Lambda表达式9Predicate<String> isEmpty1 = s -> s.isEmpty();10// 方法引用11Predicate<String> isEmpty2 = String::isEmpty;1213// 示例3:使用一个参数调用另一个对象的方法14// Lambda表达式15BiPredicate<List<String>, String> contains1 = (list, element) -> list.contains(element);16// 方法引用17BiPredicate<List<String>, String> contains2 = List::contains;1819// 示例4:构造器引用20// Lambda表达式21Function<String, Integer> intConverter1 = s -> new Integer(s);22// 构造器引用23Function<String, Integer> intConverter2 = Integer::new;无法使用方法引用的情况:
1// 对参数进行处理,无法简化为方法引用2Function<String, Integer> lengthPlus1 = s -> s.length() + 1;34// 调用方法后对结果进行处理,无法简化为方法引用5Function<String, Boolean> isEmpty = s -> s.length() == 0;67// 组合多个方法调用,无法简化为方法引用8Function<String, String> normalize = s -> s.trim().toLowerCase();4. 方法引用在Stream API中的应用
方法引用与Stream API结合使用非常强大:
1// 对象集合处理2List<Person> persons = Arrays.asList(3 new Person("John", 25),4 new Person("Alice", 30),5 new Person("Bob", 20)6);78// 获取所有名字9List<String> names = persons.stream()10 .map(Person::getName) // 使用方法引用11 .collect(Collectors.toList());1213// 按年龄排序14persons.sort(Comparator.comparing(Person::getAge));1516// 获取年龄总和17int totalAge = persons.stream()18 .mapToInt(Person::getAge) // 使用方法引用19 .sum();2021// 分组统计22Map<Integer, List<Person>> groupByAge = persons.stream()23 .collect(Collectors.groupingBy(Person::getAge));2425// 打印每个人的信息26persons.forEach(System.out::println); // 使用方法引用5. 方法引用与集合和数组操作
1String[] names = {"John", "Alice", "Bob"};2List<String> nameList = Arrays.asList(names);34// 数组转List5List<String> nameList2 = Arrays.stream(names)6 .collect(Collectors.toList());78// 排序(使用方法引用)9Arrays.sort(names, String::compareToIgnoreCase);10nameList.sort(String::compareToIgnoreCase);1112// List转数组13String[] namesArray = nameList.stream()14 .toArray(String[]::new); // 构造器引用6. 方法引用的类型推断
Java编译器能够根据上下文自动推断方法引用的签名:
1// 自动推断出需要的函数式接口类型2Runnable r = System.out::println; // 推断为不带参数的println方法3Consumer<String> c = System.out::println; // 推断为带String参数的println方法45// 自动选择正确的重载方法6BiFunction<Integer, Integer, Integer> max = Math::max; // 选择int max(int, int)7. 方法引用与泛型方法
方法引用也可以引用泛型方法:
1// 使用泛型方法引用2List<String> list = Arrays.asList("a", "b", "c");3List<Integer> lengths = list.stream()4 .map(String::length) // 泛型方法引用5 .collect(Collectors.toList());67// 类上的泛型方法引用8Function<List<String>, String> joiner = String::join; // 引用静态方法String.join(CharSequence, Iterable)8. 方法引用的优势
- 简洁性:代码更加简洁,减少样板代码
- 可读性:直接表明调用了哪个方法,意图更清晰
- 性能:与等价的Lambda表达式相比没有性能差异
- 重用:可以直接利用已有的方法实现
9. 使用建议
- 当Lambda表达式仅调用一个现有方法时,优先使用方法引用
- 根据实际情况选择最清晰的语法形式
- 避免过度使用导致代码难以理解
- 在文档中解释复杂的方法引用的用途
34. Java 8中的Optional类是什么?如何正确使用?
Optional 是Java 8引入的一个容器类,代表一个值存在或不存在。Optional的主要目的是解决空指针异常(NullPointerException)问题,提供了一种更优雅的方式来处理可能为null的对象,避免显式的null检查。
1. Optional的核心特点
- 可以包含非null值,也可以表示"无值"状态
- 避免null检查的样板代码
- 使代码更具可读性
- 迫使开发者思考"值不存在"的情况
2. 创建Optional对象
Optional提供了三种静态工厂方法来创建实例:
1// 创建一个空的Optional2Optional<String> empty = Optional.empty();34// 创建一个包含非null值的Optional5Optional<String> present = Optional.of("Hello");67// 创建一个可能包含null值的Optional8Optional<String> nullable = Optional.ofNullable(mayBeNullString);注意事项:
Optional.of(null)会抛出NullPointerExceptionOptional.ofNullable(null)会返回一个空的Optional
3. 检查值是否存在
检查Optional是否包含值:
1// 使用isPresent方法2Optional<String> opt = Optional.ofNullable(getValue());3if (opt.isPresent()) {4 System.out.println("Value: " + opt.get());5} else {6 System.out.println("No value present");7}89// 使用isEmpty方法 (Java 11+)10if (opt.isEmpty()) {11 System.out.println("No value present");12}4. 访问Optional中的值
访问Optional中的值有多种方法,根据不同需求选择:
4.1 安全地获取值:
1// 使用get()方法 (不推荐,可能抛出NoSuchElementException)2String value = opt.get();34// 使用orElse()方法提供默认值5String value = opt.orElse("Default value");67// 使用orElseGet()方法提供默认值的供应者8String value = opt.orElseGet(() -> computeDefaultValue());910// 使用orElseThrow()方法在值不存在时抛出异常11String value = opt.orElseThrow(); // Java 10+, 抛出NoSuchElementException12String value = opt.orElseThrow(() -> new IllegalStateException("Value not found"));4.2 条件操作:
1// 存在值时执行操作2opt.ifPresent(value -> System.out.println("Value: " + value));34// 存在值时执行一个操作,否则执行另一个操作 (Java 9+)5opt.ifPresentOrElse(6 value -> System.out.println("Value: " + value),7 () -> System.out.println("No value present")8);5. 转换Optional值
Optional提供了转换值的方法,支持链式调用:
1// map()方法转换值2Optional<String> nameOpt = Optional.of("John");3Optional<Integer> lengthOpt = nameOpt.map(String::length);45// flatMap()方法处理返回Optional的转换6Optional<User> userOpt = Optional.ofNullable(getUser());7Optional<String> emailOpt = userOpt.flatMap(User::getEmail);8// 假设User::getEmail返回Optional<String>910// 过滤值11Optional<String> longNameOpt = nameOpt.filter(name -> name.length() > 5);1213// 或操作 (Java 9+)14Optional<String> result = opt1.or(() -> opt2);6. Optional的正确使用模式
6.1 使用map转换值:
1// 不使用Optional2User user = getUser();3String zipCode = null;4if (user != null) {5 Address address = user.getAddress();6 if (address != null) {7 zipCode = address.getZipCode();8 }9}1011// 使用Optional12Optional<User> userOpt = Optional.ofNullable(getUser());13Optional<String> zipCodeOpt = userOpt14 .map(User::getAddress)15 .map(Address::getZipCode);16String zipCode = zipCodeOpt.orElse("Unknown");6.2 避免嵌套条件:
1// 使用Optional链式调用避免嵌套if-else2String result = Optional.ofNullable(user)3 .map(User::getAddress)4 .map(Address::getCity)5 .map(City::getPostalCode)6 .orElse("Unknown");6.3 结合流操作:
1// 获取有效的邮件地址2List<String> validEmails = users.stream()3 .map(User::getEmail)4 .filter(Optional::isPresent)5 .map(Optional::get)6 .collect(Collectors.toList());78// Java 9+中更简洁的写法9List<String> validEmails = users.stream()10 .map(User::getEmail)11 .flatMap(Optional::stream)12 .collect(Collectors.toList());7. Optional的常见误用和最佳实践
7.1 不要作为类字段: Optional不是为了作为类的字段而设计的,因为它不可序列化
1// 错误:不要这样做2public class User {3 private Optional<String> email; // 不推荐4}56// 正确:直接使用可空字段7public class User {8 private String email; // 可以为null9 10 public Optional<String> getEmail() {11 return Optional.ofNullable(email);12 }13}7.2 不要作为方法参数: Optional不应该作为方法参数,这会使API更复杂
1// 错误:不要这样做2public void processUser(Optional<User> userOpt) {3 // ...4}56// 正确:使用重载或显式参数7public void processUser(User user) {8 // 处理非null用户9}1011public void processWithoutUser() {12 // 处理无用户情况13}7.3 避免get()方法: 尽量避免使用get()方法,因为它可能抛出异常
1// 不推荐:可能抛出NoSuchElementException2String name = userOpt.get();34// 推荐:提供默认值或处理异常5String name = userOpt.orElse("");6// 或7String name = userOpt.orElseThrow(() -> new CustomException("User not found"));7.4 理解orElse和orElseGet的区别:
1// orElse总是执行默认值表达式,即使Optional有值2String value = opt.orElse(expensiveOperation());34// orElseGet仅在Optional为空时执行默认值表达式5String value = opt.orElseGet(() -> expensiveOperation());8. Java 9及以后的Optional增强
Java 9增加的方法:
or(): 提供备选OptionalifPresentOrElse(): 存在时执行一个操作,否则执行另一个操作stream(): 将Optional转换为Stream
1// or()方法示例2Optional<String> result = opt1.or(() -> opt2);34// stream()方法示例5Stream<String> stream = opt.stream(); // 返回0或1个元素的StreamJava 10增加的方法:
orElseThrow(): 无参数版本,抛出NoSuchElementException
9. Optional使用的性能考虑
使用Optional可能会引入一些性能开销:
- 额外的对象创建
- 包装和解包的成本
- 方法调用的开销
在性能关键的代码中,可能需要评估使用Optional的成本。对于内部方法,可以考虑显式的null检查,而为公共API提供Optional。
10. 最佳实践总结
-
使用场景:
- 作为返回类型,表示可能没有结果
- 作为包装不确定存在的值
- 用于链式调用和函数式编程
-
避免使用场景:
- 类的字段
- 方法参数
- 集合的元素类型
- 序列化对象
-
编码建议:
- 优先使用orElse、orElseGet、orElseThrow而不是get
- 使用ifPresent或map处理存在的值
- 链式调用减少嵌套代码
- 理解并合理使用flatMap
- 在API边界使用Optional
- 避免创建嵌套的Optional
35. Java I/O与NIO有什么区别?应该如何选择?
Java I/O和**NIO(New I/O)**是Java中进行输入/输出操作的两个主要API,它们在设计理念、实现方式和适用场景上有显著区别。
1. 基本概念对比
| 特性 | Java I/O (io包) | Java NIO (nio包) |
|---|---|---|
| 设计理念 | 面向流 (Stream-oriented) | 面向缓冲区 (Buffer-oriented) |
| 数据传输方式 | 阻塞式 (Blocking) | 可选阻塞/非阻塞 (Blocking/Non-blocking) |
| I/O模型 | 单向传输 | 通道可双向传输 |
| 处理器使用 | 线程执行期间阻塞 | 单线程可管理多个连接 |
| 缓冲区 | 无内置缓冲区概念 | 基于缓冲区操作 |
| 选择器 | 不支持 | 支持Selector多路复用 |
| API复杂性 | 简单直观 | 较复杂 |
| 出现时间 | JDK 1.0 | JDK 1.4 |
2. 核心组件比较
2.1 Java I/O的核心组件:
-
字节流:处理字节数据
InputStream/OutputStream及其子类
-
字符流:处理字符数据
Reader/Writer及其子类
-
装饰器模式:通过组合提供额外功能
BufferedInputStream/BufferedOutputStreamDataInputStream/DataOutputStream- 等等
2.2 Java NIO的核心组件:
-
Buffer:数据容器
ByteBuffer,CharBuffer,IntBuffer等
-
Channel:连接到IO设备的通道
FileChannel,SocketChannel,ServerSocketChannel等
-
Selector:实现多路复用的选择器
- 允许单线程监控多个Channel的事件
-
Charset:字符集编解码器
- 处理字节和字符之间的转换
3. 详细功能对比
3.1 数据传输模式
Java I/O:
- 面向流的处理方式
- 数据被视为连续的字节流
- 按顺序处理,每次处理一个字节
- 阻塞式API,调用线程等待操作完成
1// I/O读取文件示例2try (FileInputStream fis = new FileInputStream("file.txt")) {3 byte[] buffer = new byte[1024];4 int bytesRead;5 while ((bytesRead = fis.read(buffer)) != -1) {6 // 处理读取的数据7 process(buffer, 0, bytesRead);8 }9}Java NIO:
- 面向缓冲区的处理方式
- 数据被加载到缓冲区中进行处理
- 可以前后移动位置(flip, rewind)
- 支持非阻塞模式
1// NIO读取文件示例2try (FileChannel channel = FileChannel.open(Paths.get("file.txt"), StandardOpenOption.READ)) {3 ByteBuffer buffer = ByteBuffer.allocate(1024);4 while (channel.read(buffer) != -1) {5 buffer.flip(); // 切换到读模式6 while (buffer.hasRemaining()) {7 // 处理数据8 byte b = buffer.get();9 process(b);10 }11 buffer.clear(); // 清空缓冲区准备再次读取12 }13}3.2 阻塞与非阻塞
Java I/O:
- 总是阻塞模式
- 一个线程只能处理一个连接
- 高并发场景下需要大量线程
Java NIO:
- 支持非阻塞模式
- 通过Selector实现多路复用
- 一个线程可以监控多个Channel
- 更高效的资源使用
1// NIO非阻塞服务器示例2ServerSocketChannel serverChannel = ServerSocketChannel.open();3serverChannel.bind(new InetSocketAddress(8080));4serverChannel.configureBlocking(false);56Selector selector = Selector.open();7serverChannel.register(selector, SelectionKey.OP_ACCEPT);89while (true) {10 int readyChannels = selector.select();11 if (readyChannels == 0) continue;12 13 Set<SelectionKey> selectedKeys = selector.selectedKeys();14 Iterator<SelectionKey> keyIterator = selectedKeys.iterator();15 16 while (keyIterator.hasNext()) {17 SelectionKey key = keyIterator.next();18 19 if (key.isAcceptable()) {20 // 处理连接请求21 handleAccept(key);22 } else if (key.isReadable()) {23 // 处理读事件24 handleRead(key);25 }26 27 keyIterator.remove();28 }29}3.3 内存使用
Java I/O:
- 每次操作都直接在JVM和操作系统间复制数据
- 不使用中间缓冲区
- 可能引起频繁的系统调用
Java NIO:
- 使用Buffer作为中间数据存储
- 减少系统调用次数
- 可以实现零拷贝(Zero-Copy)
- 直接缓冲区(Direct Buffer)可以减少JVM和操作系统间的数据复制
1// 使用直接缓冲区2ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);3// 与操作系统内存映射,减少复制3.4 API复杂度
Java I/O:
- 简单直观的API
- 易于理解和使用
- 适合初学者和简单场景
Java NIO:
- 较复杂的API
- 需要理解Buffer、Channel、Selector等概念
- 状态管理更复杂(如buffer的position, limit, capacity)
- 学习曲线较陡
4. 文件操作对比
4.1 读取文件
Java I/O:
1try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {2 String line;3 while ((line = reader.readLine()) != null) {4 System.out.println(line);5 }6}Java NIO:
1// 小文件读取2String content = new String(Files.readAllBytes(Paths.get("file.txt")));34// 大文件读取5try (FileChannel channel = FileChannel.open(Paths.get("file.txt"), StandardOpenOption.READ)) {6 ByteBuffer buffer = ByteBuffer.allocate(1024);7 StringBuilder content = new StringBuilder();8 while (channel.read(buffer) > 0) {9 buffer.flip();10 content.append(StandardCharsets.UTF_8.decode(buffer));11 buffer.clear();12 }13 return content.toString();14}1516// 或使用Files工具类(内部使用NIO实现)17List<String> lines = Files.readAllLines(Paths.get("file.txt"));4.2 写入文件
Java I/O:
1try (BufferedWriter writer = new BufferedWriter(new FileWriter("file.txt"))) {2 writer.write("Hello, World!");3 writer.newLine();4 writer.write("Second line");5}Java NIO:
1// 简单写入2Files.write(Paths.get("file.txt"), "Hello, World!".getBytes());34// 使用Channel写入5try (FileChannel channel = FileChannel.open(6 Paths.get("file.txt"), 7 StandardOpenOption.CREATE, 8 StandardOpenOption.WRITE)) {9 10 ByteBuffer buffer = ByteBuffer.wrap("Hello, World!".getBytes());11 channel.write(buffer);12}4.3 文件复制
Java I/O:
1try (InputStream in = new FileInputStream("source.txt");2 OutputStream out = new FileOutputStream("target.txt")) {3 4 byte[] buffer = new byte[8192];5 int bytesRead;6 while ((bytesRead = in.read(buffer)) != -1) {7 out.write(buffer, 0, bytesRead);8 }9}Java NIO:
1// 使用Channel复制2try (FileChannel sourceChannel = FileChannel.open(Paths.get("source.txt"), StandardOpenOption.READ);3 FileChannel targetChannel = FileChannel.open(Paths.get("target.txt"), 4 StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {5 6 sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);7 // 或者: targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());8}910// 或使用Files工具类11Files.copy(Paths.get("source.txt"), Paths.get("target.txt"), StandardCopyOption.REPLACE_EXISTING);5. 网络编程对比
5.1 阻塞式Socket通信
Java I/O:
1// 服务器端2ServerSocket serverSocket = new ServerSocket(8080);3Socket clientSocket = serverSocket.accept(); // 阻塞等待连接4BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));5PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);67String line;8while ((line = in.readLine()) != null) { // 阻塞读取9 out.println("Echo: " + line);10}1112// 客户端13Socket socket = new Socket("localhost", 8080);14PrintWriter out = new PrintWriter(socket.getOutputStream(), true);15BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));1617out.println("Hello, Server!");18System.out.println(in.readLine()); // 阻塞等待响应Java NIO:
1// 非阻塞服务器2ServerSocketChannel serverChannel = ServerSocketChannel.open();3serverChannel.bind(new InetSocketAddress(8080));4serverChannel.configureBlocking(false);56Selector selector = Selector.open();7serverChannel.register(selector, SelectionKey.OP_ACCEPT);89while (true) {10 selector.select(); // 等待事件11 Iterator<SelectionKey> keys = selector.selectedKeys().iterator();12 13 while (keys.hasNext()) {14 SelectionKey key = keys.next();15 keys.remove();16 17 if (key.isAcceptable()) {18 // 处理新连接19 ServerSocketChannel server = (ServerSocketChannel) key.channel();20 SocketChannel client = server.accept();21 client.configureBlocking(false);22 client.register(selector, SelectionKey.OP_READ);23 } else if (key.isReadable()) {24 // 处理读事件25 SocketChannel client = (SocketChannel) key.channel();26 ByteBuffer buffer = ByteBuffer.allocate(1024);27 int bytesRead = client.read(buffer);28 29 if (bytesRead > 0) {30 buffer.flip();31 client.write(buffer);32 buffer.clear();33 }34 }35 }36}6. 性能对比
Java I/O的优势:
- 简单场景下代码更简洁
- 单个连接处理的逻辑清晰
- 内存占用较少(不需要为每个连接分配缓冲区)
Java NIO的优势:
- 高并发场景下性能更好
- 可以用更少的线程处理更多连接
- 非阻塞模式下CPU利用率更高
- 大文件传输时零拷贝特性提供更好性能
- 内存映射文件能提供极高的I/O性能
7. 适用场景分析
7.1 适合Java I/O的场景:
- 连接数量较少且稳定的应用
- 消息较短的应用
- 实现简单性比性能更重要的场景
- 代码可读性要求高的场景
- 单线程应用
7.2 适合Java NIO的场景:
- 高并发、大量连接的服务器应用
- 需要非阻塞操作的场景
- 长连接应用(如即时通讯)
- 文件操作密集型应用
- 需要高吞吐量的场景
- 需要使用异步I/O模型的场景
8. Java 7及以后的改进
Java 7 引入的 NIO.2 (java.nio.file包) 提供了更现代的文件系统API:
- Path 接口替代File类
- Files 工具类提供丰富的文件操作方法
- 文件属性操作更加强大
- WatchService 用于监视文件系统变化
- 异步I/O 支持(AsynchronousFileChannel等)
1// NIO.2 文件操作示例2Path path = Paths.get("file.txt");3Files.write(path, "Hello World".getBytes(), StandardOpenOption.CREATE);45// 异步文件操作6AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(7 path, StandardOpenOption.READ);8ByteBuffer buffer = ByteBuffer.allocate(1024);910fileChannel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {11 @Override12 public void completed(Integer result, ByteBuffer attachment) {13 attachment.flip();14 CharBuffer charBuffer = StandardCharsets.UTF_8.decode(attachment);15 System.out.println(charBuffer.toString());16 }17 18 @Override19 public void failed(Throwable exc, ByteBuffer attachment) {20 exc.printStackTrace();21 }22});9. 选择指南
选择Java I/O还是NIO时,考虑以下因素:
- 并发连接数:高并发场景选择NIO
- 业务复杂度:简单业务逻辑可选I/O
- 性能要求:对性能要求高选择NIO
- 开发团队经验:团队对NIO不熟悉时,权衡开发成本
- 代码可维护性:I/O代码通常更易维护
- 阻塞需求:需要非阻塞操作必须选NIO
- 内存资源限制:线程资源紧张选择NIO
10. 最佳实践
- 不要混用:一个系统中尽量不要混用I/O和NIO
- 考虑使用框架:如Netty, MINA等封装了NIO的复杂性
- NIO调试复杂:做好日志记录和异常处理
- 文件操作推荐NIO.2:现代Java应用应使用java.nio.file包
- 网络应用中的选择:
- 小规模应用可以使用I/O
- 高并发应用选择NIO或基于NIO的框架
- 注意缓冲区管理:在NIO中正确管理Buffer的状态
36. Java中的垃圾回收机制是如何工作的?
**垃圾回收(Garbage Collection, GC)**是Java虚拟机(JVM)自动管理内存的核心机制,它能够自动识别和清理不再使用的对象,释放内存资源。理解垃圾回收机制对于编写高效的Java应用至关重要。
1. 垃圾回收的基本原理
垃圾回收的基本思路是识别哪些对象是"存活"的,然后将其他对象视为"垃圾"进行回收。JVM采用的判断对象是否可以回收的算法主要有两种:
-
引用计数法(Reference Counting):
- 为每个对象维护一个引用计数器
- 每当有引用指向对象时计数器加1,引用失效时减1
- 计数为0时对象可被回收
- 缺点:无法处理循环引用问题
-
可达性分析(Reachability Analysis):
- 从一系列"GC Roots"开始,进行引用链搜索
- 可从GC Roots直接或间接到达的对象被视为存活
- 不可达对象被视为垃圾
- JVM采用这种算法
2. GC Roots包括:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- JVM内部引用(类加载器、异常对象等)
3. Java中的引用类型
JDK 1.2后,Java将引用分为四种类型,为GC提供了更灵活的内存管理策略:
-
强引用(Strong Reference):
- 最常见的引用(如
Object obj = new Object()) - 只要强引用存在,对象就不会被回收
- 最常见的引用(如
-
软引用(Soft Reference):
- 内存不足时才会被回收
- 用于实现内存敏感的缓存
- 通过
SoftReference类实现
-
弱引用(Weak Reference):
- 下一次GC时无条件回收
- 用于实现规范化映射
- 通过
WeakReference和WeakHashMap实现
-
虚引用(Phantom Reference):
- 不影响对象生命周期,仅用于对象被回收时收到通知
- 必须和引用队列一起使用
- 通过
PhantomReference类实现
4. 垃圾回收算法
4.1 标记-清除算法(Mark-Sweep)
- 过程:首先标记所有需要回收的对象,然后统一回收所有被标记的对象
- 优点:算法简单
- 缺点:
- 效率不高(标记和清除两个过程效率都不高)
- 会产生大量内存碎片
1[标记前] O O X O X O O X O X2[标记后] O O M O M O O M O M (M=被标记为垃圾)3[清除后] O O _ O _ O O _ O _ (_=空闲内存)4.2 复制算法(Copying)
- 过程:将内存分为两块,每次只使用一块;当一块用完时,将存活对象复制到另一块,然后清空当前块
- 优点:
- 解决内存碎片问题
- 实现简单,运行高效
- 缺点:
- 内存利用率降低
- 对象存活率高时效率会降低
1[复制前] [O O X O X O] [ ] (X=垃圾对象)2[复制后] [ ] [O O O O ] (存活对象被复制)4.3 标记-整理算法(Mark-Compact)
- 过程:标记阶段与标记-清除算法一样,但后续不是直接清理,而是将存活对象移动到内存的一端,然后清理边界以外的内存
- 优点:
- 解决了内存碎片问题
- 避免了复制算法的空间浪费
- 缺点:
- 需要移动对象,效率低于复制算法
1[标记后] O O M O M O O M O M (M=被标记为垃圾)2[整理后] O O O O O O _ _ _ _ (_=空闲内存)4.4 分代收集算法(Generational Collection)
- 核心思想:根据对象存活周期将内存划分为几个区域
- 实现:将堆分为新生代和老年代,不同代使用不同的收集算法
- 优点:
- 针对不同年龄对象采用最合适的收集算法
- 提高GC效率
5. 堆内存结构和分代回收
在经典的分代垃圾回收模型中,堆内存被划分为:
5.1 新生代(Young Generation)
- Eden区:大多数新创建的对象最初都在Eden区分配
- Survivor区:包括From和To两个区域,存放经过GC后幸存的对象
5.2 老年代(Old Generation)
- 存放长期存活的对象
- 通常经历了多次GC仍存活的对象会被移到这里
5.3 对象的生命周期
- 新创建的对象首先分配在Eden区
- Eden区满后触发Minor GC,存活对象移动到Survivor区
- 经过多次Minor GC仍存活的对象进入老年代
- 老年代满时触发Major GC或Full GC
6. 垃圾回收器的类型
JVM提供了多种垃圾回收器,适用于不同的应用场景:
6.1 Serial收集器
- 单线程收集器,GC时暂停所有应用线程
- 简单高效,适合单核CPU和小内存环境
6.2 ParNew收集器
- Serial的多线程版本
- 常与CMS收集器配合使用
6.3 Parallel Scavenge收集器
- 关注吞吐量的多线程收集器
- 可控制吞吐量和最大暂停时间
6.4 Serial Old收集器
- Serial收集器的老年代版本
- 单线程,使用标记-整理算法
6.5 Parallel Old收集器
- Parallel Scavenge的老年代版本
- 多线程,使用标记-整理算法
6.6 CMS(Concurrent Mark Sweep)收集器
- 以最短停顿时间为目标
- 基于标记-清除算法
- 过程:初始标记→并发标记→重新标记→并发清除
- 优点:并发收集,停顿时间短
- 缺点:会产生内存碎片,对CPU资源敏感
6.7 G1(Garbage-First)收集器
- JDK 7更新,JDK 9默认
- 将堆划分为多个区域(Region)
- 优先收集垃圾最多的区域
- 过程:初始标记→并发标记→最终标记→筛选回收
- 优点:可预测停顿时间,无内存碎片
6.8 ZGC(Z Garbage Collector)
- JDK 11引入的低延迟收集器
- 可处理TB级内存,停顿时间10ms
- 基于Region,使用读屏障和有色指针
6.9 Shenandoah收集器
- OpenJDK特有,与ZGC目标类似
- 通过并发标记和复制实现低延迟
7. GC触发条件
7.1 Minor GC触发条件
- Eden区空间不足时
7.2 Major/Full GC触发条件
- 老年代空间不足
- Permanent Generation(永久代)/Metaspace空间不足
- System.gc()调用(建议JVM执行GC,但不保证执行)
- CMS GC出现promotion failed和concurrent mode failure
- Minor GC晋升到老年代的对象大小超过老年代剩余空间
8. GC调优参数
8.1 堆大小相关
1-Xms: 初始堆大小2-Xmx: 最大堆大小3-Xmn: 年轻代大小4-XX:SurvivorRatio: Eden区与Survivor区比例5-XX:NewRatio: 年轻代与老年代比例8.2 GC策略相关
1-XX:+UseSerialGC: 使用Serial+Serial Old收集器2-XX:+UseParallelGC: 使用Parallel Scavenge+Parallel Old收集器3-XX:+UseConcMarkSweepGC: 使用ParNew+CMS+Serial Old收集器4-XX:+UseG1GC: 使用G1收集器5-XX:+UseZGC: 使用ZGC收集器8.3 GC日志相关
1-XX:+PrintGCDetails: 打印GC详细信息2-XX:+PrintGCTimeStamps: 打印GC时间戳3-Xloggc:filename: 输出GC日志到文件9. 对象分配与回收优化
9.1 TLAB(Thread Local Allocation Buffer)
- 线程私有的对象分配区域
- 减少多线程分配对象时的同步开销
- 提高对象分配效率
9.2 逃逸分析
- JVM的一种优化技术
- 分析对象的作用域,确定对象是否可以在栈上分配
- 如果对象不会"逃逸"出方法,可以在栈上分配
9.3 写屏障和读屏障
- 在分代回收中用于记录跨代引用
- 确保垃圾回收的正确性
- 影响GC性能
10. JVM内存泄漏常见原因
- 静态集合类:静态集合持有对象引用
- 未关闭的资源:如文件、数据库连接等
- 监听器和回调:注册但未注销
- 单例对象:持有外部对象引用
- 内部类和匿名内部类:隐式持有外部类引用
1// 内存泄漏示例2public class LeakExample {3 // 静态集合导致的内存泄漏4 private static final List<Object> LEAK_LIST = new ArrayList<>();5 6 public void addToList(Object obj) {7 LEAK_LIST.add(obj); // 对象将永远不会被回收8 }9 10 // 未关闭资源导致的内存泄漏11 public void processFile(String path) throws Exception {12 InputStream is = new FileInputStream(path);13 // 使用流但未关闭,可能导致文件句柄泄漏14 // is.close(); 应该在finally块中关闭15 }16}11. 实际应用中的GC调优
11.1 调优目标
- 减少Full GC频率
- 降低GC停顿时间
- 提高吞吐量
11.2 调优步骤
- 确定目标:是降低延迟还是提高吞吐量
- 收集GC数据:使用JVM参数开启GC日志
- 分析GC日志:使用工具如GCViewer分析
- 调整参数:根据分析结果调整JVM参数
- 验证效果:重复收集数据并比较结果
11.3 常见调优实践
- 选择合适的收集器
- 调整堆大小和代的比例
- 设置合理的初始堆大小,避免频繁扩容
- 对大内存应用考虑G1或ZGC
- 避免创建大量临时对象
- 复用对象而非频繁创建
37. Java中的类加载机制是怎样的?
**类加载机制(Class Loading Mechanism)**是Java虚拟机将类的字节码加载到内存、创建对象、链接和初始化的过程,是Java动态扩展能力的重要体现。
1. 类加载的时机
类加载通常在以下情况触发:
- 创建类实例:使用new关键字、反射、克隆、反序列化
- 访问类的静态变量:首次引用类的静态字段(final常量除外)
- 访问类的静态方法
- 初始化子类时:会先初始化父类
- Java虚拟机启动时的主类
- 使用反射API:如Class.forName()
- 使用JDK 7动态语言支持:MethodHandle和MethodType
注意:通过数组定义引用类、访问类的final静态字段、子类引用父类的静态字段不会触发子类初始化。
2. 类加载的过程
类加载过程分为五个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)和初始化(Initialization)。其中验证、准备、解析统称为连接(Linking)。
2.1 加载(Loading)
加载是类加载的第一步,主要完成三件事:
- 通过类的全限定名获取类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的访问入口
获取字节流的方式多种多样:
- 从ZIP包中获取(如JAR、WAR、EAR文件)
- 从网络中获取(如Applet)
- 运行时计算生成(如动态代理)
- 从数据库中获取
- 从其他文件中获取(如JSP)
2.2 验证(Verification)
验证是连接阶段的第一步,目的是确保类的字节码符合JVM规范,不会危害虚拟机安全。主要包括:
- 文件格式验证:魔数、版本号、常量池等
- 元数据验证:是否符合Java语言规范
- 字节码验证:确保程序语义正确性
- 符号引用验证:确保解析能正常进行
2.3 准备(Preparation)
准备阶段为类的静态变量分配内存并设置初始值(零值),如:
1public static int value = 123; // 准备阶段value值为0,而非1232public static final int CONSTANT = 123; // 准备阶段CONSTANT值即为123这个阶段不包含实例变量,实例变量会在对象实例化时随对象分配到堆中。
2.4 解析(Resolution)
解析阶段是将常量池内的符号引用替换为直接引用的过程。符号引用包括:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
解析可能在初始化之后才开始,这是为了支持Java的动态绑定。
2.5 初始化(Initialization)
初始化是类加载的最后一步,会执行类构造器<clinit>()方法。这个方法由编译器自动生成,包含:
- 静态变量的赋值操作
- 静态代码块中的语句
执行顺序与源文件中出现的顺序一致:
1public class InitExample {2 static {3 i = 0; // 可以赋值,但不能访问4 // System.out.println(i); // 非法前向引用5 }6 7 static int i = 1; // 最终i的值为18 9 static {10 i = 2; // 修改为211 System.out.println(i); // 输出212 }13}初始化的特点:
- JVM保证类的
<clinit>()方法在多线程环境中被正确加锁、同步 - 父类的初始化先于子类
- 接口初始化不会导致父接口初始化
- 接口中的变量默认是static final的
3. 类加载器
类加载器(ClassLoader)负责加载阶段,通过类的全限定名查找并加载类的二进制数据。
3.1 Java的类加载器分类
3.1.1 启动类加载器(Bootstrap ClassLoader)
- 负责加载Java核心类库
- 位于JRE的
lib目录,如rt.jar - 由C++实现,是JVM的一部分
- 在Java代码中无法直接引用
3.1.2 扩展类加载器(Extension ClassLoader)
- 负责加载JRE的扩展目录(
lib/ext) - JDK 9后更名为平台类加载器(Platform ClassLoader)
3.1.3 应用程序类加载器(Application ClassLoader)
- 负责加载用户类路径(ClassPath)上的类
- 也称系统类加载器(System ClassLoader)
- 是应用程序默认的类加载器
3.1.4 自定义类加载器
- 通过继承
java.lang.ClassLoader实现 - 常用于实现特殊的类加载逻辑
3.2 双亲委派模型
类加载器使用双亲委派模型(Parents Delegation Model)进行组织。当一个类加载器收到类加载请求时,首先将请求委派给父加载器,依次向上。只有当父加载器无法加载时,子加载器才尝试自己加载。
![类加载器层次结构]
1BootstrapClassLoader2 ↑3ExtensionClassLoader4 ↑5ApplicationClassLoader6 ↑7 自定义ClassLoader双亲委派的工作流程:
- 检查类是否已被加载
- 如果未加载,委托给父加载器
- 如果父加载器无法加载,自己尝试加载
ClassLoader的loadClass方法源码简化版:
1protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {2 // 检查类是否已加载3 Class<?> c = findLoadedClass(name);4 if (c == null) {5 try {6 if (parent != null) {7 // 委托父加载器加载8 c = parent.loadClass(name, false);9 } else {10 // 使用启动类加载器加载11 c = findBootstrapClassOrNull(name);12 }13 } catch (ClassNotFoundException e) {14 // 父加载器无法加载15 }16 17 if (c == null) {18 // 父加载器无法加载,自己尝试加载19 c = findClass(name);20 }21 }22 if (resolve) {23 resolveClass(c);24 }25 return c;26}双亲委派的好处:
- 避免类的重复加载
- 保护核心API不被篡改(安全性)
- 确保类加载的层次性和优先级
3.3 打破双亲委派模型
有些场景需要打破双亲委派模型:
- JDK 1.2之前:自定义ClassLoader无法通过双亲委派
- SPI机制:如JDBC、JCE、JNDI等
- OSGi模块化:允许同一个类在不同的模块中加载
- Tomcat类加载机制:实现Web应用隔离
- 热部署和热替换:如Spring开发工具、JRebel
打破双亲委派的实现方式:
- 重写
loadClass()方法 - 使用上下文类加载器(Thread Context ClassLoader)
上下文类加载器示例:
1// 设置上下文类加载器2Thread.currentThread().setContextClassLoader(customClassLoader);34// 获取上下文类加载器5ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();67// 使用上下文类加载器加载类8Class<?> clazz = Class.forName("com.example.MyClass", true, contextClassLoader);3.4 自定义类加载器
实现自定义类加载器通常只需要重写findClass()方法:
1public class CustomClassLoader extends ClassLoader {2 @Override3 protected Class<?> findClass(String name) throws ClassNotFoundException {4 // 获取类的字节码5 byte[] classData = loadClassData(name);6 if (classData == null) {7 throw new ClassNotFoundException(name);8 }9 10 // 调用defineClass将字节码转为Class对象11 return defineClass(name, classData, 0, classData.length);12 }13 14 private byte[] loadClassData(String name) {15 // 实现自定义的字节码获取逻辑16 // 如从特定位置读取类文件、网络下载、动态生成等17 // 返回类的字节码数组18 }19}使用自定义类加载器:
1CustomClassLoader loader = new CustomClassLoader();2Class<?> clazz = loader.loadClass("com.example.MyClass");3Object instance = clazz.newInstance();自定义类加载器的应用场景:
- 加密解密类字节码(安全)
- 从网络或数据库加载类
- 热部署
- 类隔离(不同版本的类共存)
- 实现模块化系统(如OSGi)
4. Class对象和类加载器
Java中,类由其全限定名和加载它的类加载器共同确定其唯一性。不同类加载器加载的同名类被视为不同的类,它们的Class对象不相等。
1ClassLoader loader1 = new CustomClassLoader();2ClassLoader loader2 = new CustomClassLoader();34Class<?> class1 = loader1.loadClass("com.example.MyClass");5Class<?> class2 = loader2.loadClass("com.example.MyClass");67// 尽管类名相同,但由不同的类加载器加载8System.out.println(class1 == class2); // 输出false5. 类加载器和命名空间
每个类加载器都有自己的命名空间,由该加载器及其所有父加载器加载的类组成。
命名空间特点:
- 子加载器能访问父加载器加载的类
- 父加载器不能访问子加载器加载的类
- 同级但不同的类加载器相互隔离
这种隔离机制是实现应用隔离(如Web容器中的应用隔离)的基础。
6. 类加载的并行性
JVM规范没有强制要求类加载过程的并行性。但在实际实现中:
- 加载阶段可以并行
- 验证阶段可以并行
- 初始化阶段必须是线程安全的
7. 动态加载与反射
Java支持运行时动态加载类:
1// 动态加载类2Class<?> clazz = Class.forName("com.example.MyClass");34// 创建实例5Object obj = clazz.newInstance();67// 获取方法8Method method = clazz.getMethod("methodName", paramTypes);910// 调用方法11method.invoke(obj, params);与直接使用new关键字相比,动态加载提供了更大的灵活性,是Java反射API和很多框架的基础。
8. JDK 9模块系统的影响
JDK 9引入了模块系统(JPMS),对类加载机制产生了影响:
- 新增了模块路径(Module Path),与传统类路径并存
- 增强了类加载的安全性和访问控制
- 引入平台类加载器替代扩展类加载器
- 模块之间的依赖关系显式声明
9. 常见问题与解决方案
9.1 ClassNotFoundException vs NoClassDefFoundError
- ClassNotFoundException: 当应用尝试通过Class.forName、ClassLoader.loadClass或ClassLoader.findSystemClass加载类但找不到时抛出
- NoClassDefFoundError: 当Java虚拟机或ClassLoader实例尝试加载类的定义但找不到时抛出,通常是类加载时的链接错误
9.2 类加载器泄漏
- 发生在类加载器无法被垃圾回收的情况
- 常见于Web容器中的应用重新部署
- 解决方案:正确管理类加载器生命周期,使用弱引用
38. Java中的异常处理机制是怎样的?
异常处理机制是Java提供的处理程序运行时错误的强大工具,它能够改变程序的正常控制流程以处理错误情况,同时保持代码的结构性和可读性。
1. Java异常体系结构
Java异常体系以Throwable类为根,包含两个主要分支:Error和Exception。
1.1 Throwable类:
- 所有异常的父类
- 包含错误信息和栈追踪
- 主要方法:getMessage(), printStackTrace(), getStackTrace()
1.2 Error:
- 表示严重的系统级错误,通常是不可恢复的
- 程序通常无法处理
- 例如:OutOfMemoryError, StackOverflowError, VirtualMachineError
1.3 Exception:
- 表示程序可能处理的错误情况
- 分为两类:已检查异常(Checked Exception)和未检查异常(Unchecked Exception)
异常继承体系图:
1Throwable2 / \3 Error Exception4 / \5 (Checked Exceptions) RuntimeException6 |7 (Unchecked Exceptions)2. 已检查异常(Checked Exception)与未检查异常(Unchecked Exception)
2.1 已检查异常(Checked Exception):
- 编译时期就会检查的异常
- 必须显式处理(try-catch或throws声明)
- Exception的直接子类(除RuntimeException)
- 表示可预见但可能避免不了的异常情况
- 常见例子:IOException, SQLException, ClassNotFoundException
2.2 未检查异常(Unchecked Exception):
- 编译时期不会检查的异常
- 不需要显式处理
- RuntimeException及其子类
- 通常表示程序错误
- 常见例子:NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException
3. 异常处理语法
Java提供了try, catch, finally, throw和throws关键字来处理异常。
3.1 try-catch语句:
1try {2 // 可能抛出异常的代码3 int result = 10 / 0; // 会抛出ArithmeticException4} catch (ArithmeticException e) {5 // 处理特定类型的异常6 System.out.println("除零错误: " + e.getMessage());7} catch (Exception e) {8 // 处理其他类型的异常9 System.out.println("发生错误: " + e.getMessage());10}3.2 多catch块和异常类型合并:
Java 7引入了多种异常类型的合并处理:
1try {2 // 可能抛出多种异常的代码3} catch (IOException | SQLException e) {4 // 处理IOException或SQLException5 System.out.println("IO或SQL异常: " + e.getMessage());6}3.3 finally语句:
无论是否发生异常,finally块中的代码总会执行(除非JVM退出):
1FileInputStream fis = null;2try {3 fis = new FileInputStream("file.txt");4 // 处理文件5} catch (IOException e) {6 System.out.println("文件处理错误: " + e.getMessage());7} finally {8 // 清理资源,无论是否发生异常9 if (fis != null) {10 try {11 fis.close();12 } catch (IOException e) {13 // 处理关闭时可能发生的异常14 }15 }16}3.4 try-with-resources语句:
Java 7引入的自动资源管理语法,适用于实现AutoCloseable接口的对象:
1try (FileInputStream fis = new FileInputStream("file.txt");2 FileOutputStream fos = new FileOutputStream("output.txt")) {3 // 处理文件4 byte[] buffer = new byte[1024];5 int length;6 while ((length = fis.read(buffer)) > 0) {7 fos.write(buffer, 0, length);8 }9} catch (IOException e) {10 System.out.println("文件处理错误: " + e.getMessage());11}12// 资源会自动关闭,无需finally块3.5 throw语句:
用于显式抛出异常:
1public void deposit(double amount) {2 if (amount <= 0) {3 throw new IllegalArgumentException("存款金额必须为正数");4 }5 // 处理存款逻辑6}3.6 throws声明:
在方法签名中声明可能抛出的已检查异常:
1public void readFile(String fileName) throws IOException {2 FileInputStream fis = new FileInputStream(fileName);3 // 处理文件4}3.7 异常链:
传递原始异常信息:
1try {2 // 可能抛出异常的代码3} catch (SQLException e) {4 // 包装原始异常,并提供更多上下文5 throw new ServiceException("数据库操作失败", e);6}4. 自定义异常
创建自定义异常通常需要继承Exception(已检查异常)或RuntimeException(未检查异常):
1// 已检查异常2public class InsufficientFundsException extends Exception {3 private double amount;4 5 public InsufficientFundsException(double amount) {6 super("余额不足,还差" + amount + "元");7 this.amount = amount;8 }9 10 public double getAmount() {11 return amount;12 }13}1415// 未检查异常16public class UserNotFoundException extends RuntimeException {17 private String userId;18 19 public UserNotFoundException(String userId) {20 super("找不到用户: " + userId);21 this.userId = userId;22 }23 24 public String getUserId() {25 return userId;26 }27}使用自定义异常:
1public void withdraw(double amount) throws InsufficientFundsException {2 if (amount > balance) {3 throw new InsufficientFundsException(amount - balance);4 }5 balance -= amount;6}78public User findUser(String userId) {9 User user = userRepository.findById(userId);10 if (user == null) {11 throw new UserNotFoundException(userId);12 }13 return user;14}5. 异常处理最佳实践
5.1 异常粒度:
- 粒度适中,既不过细也不过粗
- 精确捕获需要处理的异常
- 不捕获无法处理的异常
5.2 异常转换:
- 底层异常转换为应用级异常
- 保留原始异常信息(使用异常链)
- 提供有意义的错误消息
1try {2 // 数据库操作3} catch (SQLException e) {4 throw new ServiceException("数据库查询失败", e);5}5.3 异常处理与日志:
- 不要捕获异常后不处理或仅打印
- 记录异常的完整信息,包括栈跟踪
- 避免过度记录(如多层传递的异常)
1try {2 // 操作3} catch (Exception e) {4 logger.error("操作失败", e); // 记录完整异常5 throw e; // 如果需要,可以重新抛出6}5.4 资源管理:
- 优先使用try-with-resources
- 确保所有资源都能正确关闭
- 资源关闭顺序与打开顺序相反
5.5 异常设计原则:
- 已检查异常用于可恢复的情况
- 未检查异常用于编程错误
- 避免过多的已检查异常
- 不要使用异常控制正常流程
5.6 性能考虑:
- 异常处理有性能开销
- 避免用异常处理正常情况
- 避免过度细粒度的try-catch块
错误示例:
1// 不要这样做2for (int i = 0; i < list.size(); i++) {3 try {4 process(list.get(i));5 } catch (Exception e) {6 logger.error("处理元素失败", e);7 }8}正确示例:
1// 更好的方式2try {3 for (int i = 0; i < list.size(); i++) {4 process(list.get(i));5 }6} catch (Exception e) {7 logger.error("处理列表失败", e);8}6. Java 7以后的异常处理增强
6.1 try-with-resources:
- 自动关闭实现AutoCloseable的资源
- 优于传统finally块
- 可以处理多个资源
- 可以捕获关闭资源时的异常
6.2 多异常捕获:
- 使用
|合并多个异常处理 - 减少代码重复
- 被捕获的异常类型必须没有继承关系
6.3 更精确的重抛异常:
- Java 7以前,重抛异常会丢失具体异常类型
- Java 7以后,编译器会分析可能抛出的异常类型
1// Java 7之前2public void method() throws Exception { // 只能声明为Exception3 try {4 // 可能抛出IOException或SQLException5 } catch (Exception e) {6 // 记录日志7 throw e; // 编译器认为这里抛出的是Exception8 }9}1011// Java 7之后12public void method() throws IOException, SQLException { // 可以更精确地声明13 try {14 // 可能抛出IOException或SQLException15 } catch (Exception e) {16 // 记录日志17 throw e; // 编译器可以分析出实际可能的异常类型18 }19}7. 常见异常及其处理方式
7.1 NullPointerException:
- 尝试访问null对象的方法或属性
- 防御性编程:检查null值
- 考虑使用Optional(Java 8+)
1// 预防NPE2if (obj != null) {3 obj.method();4}56// 使用Optional7Optional<User> userOpt = userService.findUser(id);8userOpt.ifPresent(user -> user.updateProfile());7.2 ArrayIndexOutOfBoundsException:
- 访问数组越界
- 检查索引范围
- 使用集合类替代数组
1// 防止越界2if (index >= 0 && index < array.length) {3 value = array[index];4}7.3 ClassCastException:
- 错误的类型转换
- 使用instanceof检查
- 使用泛型避免类型转换
1// 安全的类型转换2if (obj instanceof String) {3 String str = (String) obj;4 // 使用str5}7.4 IOException:
- I/O操作失败
- 使用try-with-resources
- 考虑重试机制
7.5 SQLException:
- 数据库操作失败
- 使用事务管理
- 处理特定的SQL错误码
7.6 OutOfMemoryError:
- 内存不足
- 增加JVM内存
- 检查内存泄漏
- 优化大对象处理
8. 异常处理策略
8.1 集中式异常处理:
在应用程序的顶层捕获并统一处理异常:
1// Spring MVC中的全局异常处理2@ControllerAdvice3public class GlobalExceptionHandler {4 5 @ExceptionHandler(UserNotFoundException.class)6 public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException e) {7 ErrorResponse error = new ErrorResponse("USER_NOT_FOUND", e.getMessage());8 return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);9 }10 11 @ExceptionHandler(Exception.class)12 public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {13 ErrorResponse error = new ErrorResponse("INTERNAL_ERROR", "服务器内部错误");14 return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);15 }16}8.2 事务性资源处理:
在异常发生时回滚事务:
1@Transactional2public void transferMoney(Account from, Account to, BigDecimal amount) {3 from.withdraw(amount); // 可能抛出InsufficientFundsException4 to.deposit(amount);5 // 如果发生异常,事务会自动回滚6}8.3 重试机制:
对于可能暂时失败的操作实现重试逻辑:
1public Response callExternalService() {2 int maxRetries = 3;3 int attempt = 0;4 5 while (attempt < maxRetries) {6 try {7 return externalService.call(); // 可能抛出NetworkException8 } catch (NetworkException e) {9 attempt++;10 if (attempt == maxRetries) {11 throw new ServiceUnavailableException("服务不可用", e);12 }13 // 指数退避14 try {15 Thread.sleep(100 * (long)Math.pow(2, attempt));16 } catch (InterruptedException ie) {17 Thread.currentThread().interrupt();18 throw new ServiceUnavailableException("重试中断", ie);19 }20 }21 }22 // 不应该执行到这里23 throw new IllegalStateException("重试逻辑错误");24}9. Java异常处理的成本
异常处理会带来性能成本,主要包括:
- 创建异常对象:构建异常对象和获取栈跟踪的成本
- 栈展开:JVM需要回溯调用栈
- 异常处理搜索:JVM查找匹配的catch块
性能优化建议:
- 不要用异常控制正常流程
- 避免捕获再立即重抛
- 精确捕获可能的异常
- 适当使用日志级别
39. Java集合框架是如何设计的?常用集合类的底层实现是怎样的?
**Java集合框架(Java Collections Framework)**是Java标准库中用于存储和操作数据集合的架构。它提供了一系列接口和类,使开发者能够以统一、高效的方式处理数据集合。
1. 集合框架的整体设计
Java集合框架的设计遵循了几个核心原则:
- 统一的架构:所有集合类都实现共同的接口
- 接口与实现分离:通过接口定义行为,具体类实现细节
- 互操作性:集合之间可以相互转换
- 性能高效:集合实现针对不同场景进行优化
- 扩展性:允许用户自定义集合类
1.1 集合框架的基本结构
Java集合框架主要包含以下几个核心接口:
![集合框架的继承关系]
1Iterable2 |3 Collection4 / | \5 / | \6 Set List Queue7 / \ \8 / \ \9 SortedSet RandomAccess Deque10 |11 NavigableSet1.2 Map接口及其实现
Map接口虽然不是Collection的子接口,但也是集合框架的重要组成部分:
1Map2 /|\3 / | \4 / | \5SortedMap | ConcurrentMap6 | |7NavigableMap2. 主要接口及其特点
2.1 Collection接口
- 集合层次结构的根接口
- 定义了所有集合共有的基本操作
- 主要方法:add(), remove(), contains(), size(), iterator()
2.2 List接口
- 有序集合,元素可以重复
- 支持按索引访问元素
- 主要实现:ArrayList, LinkedList, Vector
2.3 Set接口
- 不允许重复元素的集合
- 不保证元素顺序(特定实现除外)
- 主要实现:HashSet, LinkedHashSet, TreeSet
2.4 Queue接口
- 通常用于存储等待处理的元素
- 按特定顺序(如FIFO)检索元素
- 主要实现:LinkedList, PriorityQueue
2.5 Map接口
- 键值对映射
- 键不能重复
- 主要实现:HashMap, LinkedHashMap, TreeMap, ConcurrentHashMap
3. List实现类的底层原理
3.1 ArrayList
- 数据结构:基于动态数组
- 随机访问性能:O(1),通过索引访问元素
- 内部实现:
- 默认初始容量为10
- 动态增长:当容量不足时,扩容为当前容量的1.5倍
- 使用transient修饰符避免序列化整个数组
- 支持快速的随机访问
- 非线程安全
1// ArrayList核心源码简化版2public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {3 // 实际存储元素的数组4 transient Object[] elementData;5 // 当前元素数量6 private int size;7 8 // 添加元素9 public boolean add(E e) {10 ensureCapacityInternal(size + 1); // 确保容量足够11 elementData[size++] = e;12 return true;13 }14 15 // 按索引获取元素16 public E get(int index) {17 rangeCheck(index);18 return (E) elementData[index];19 }20 21 // 确保内部容量22 private void ensureCapacityInternal(int minCapacity) {23 if (minCapacity - elementData.length > 0)24 grow(minCapacity);25 }26 27 // 扩容逻辑28 private void grow(int minCapacity) {29 int oldCapacity = elementData.length;30 int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍扩容31 if (newCapacity < minCapacity) newCapacity = minCapacity;32 elementData = Arrays.copyOf(elementData, newCapacity);33 }34}3.2 LinkedList
- 数据结构:双向链表
- 随机访问性能:O(n),需要从头/尾遍历
- 内部实现:
- 每个元素都是一个包含前驱和后继引用的节点
- 同时实现了List和Deque接口
- 适合频繁的插入和删除操作
- 非线程安全
1// LinkedList核心源码简化版2public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E> {3 // 链表长度4 transient int size;5 // 头节点6 transient Node<E> first;7 // 尾节点8 transient Node<E> last;9 10 // 节点类11 private static class Node<E> {12 E item;13 Node<E> next;14 Node<E> prev;15 16 Node(Node<E> prev, E element, Node<E> next) {17 this.item = element;18 this.next = next;19 this.prev = prev;20 }21 }22 23 // 添加元素24 public boolean add(E e) {25 linkLast(e);26 return true;27 }28 29 // 在末尾添加元素30 void linkLast(E e) {31 final Node<E> l = last;32 final Node<E> newNode = new Node<>(l, e, null);33 last = newNode;34 if (l == null)35 first = newNode;36 else37 l.next = newNode;38 size++;39 }40 41 // 获取元素42 public E get(int index) {43 checkElementIndex(index);44 return node(index).item;45 }46 47 // 根据索引获取节点48 Node<E> node(int index) {49 if (index < (size >> 1)) {50 // 从头开始查找51 Node<E> x = first;52 for (int i = 0; i < index; i++)53 x = x.next;54 return x;55 } else {56 // 从尾开始查找57 Node<E> x = last;58 for (int i = size - 1; i > index; i--)59 x = x.prev;60 return x;61 }62 }63}3.3 Vector
- 数据结构:动态数组(与ArrayList相似)
- 特点:
- 线程安全(方法被synchronized修饰)
- 默认容量10,扩容策略是增长原始大小的100%
- 性能较ArrayList略低(同步开销)
- 已被ArrayList替代,仅为兼容旧代码保留
3.4 CopyOnWriteArrayList
- 数据结构:内部持有一个数组,所有修改操作都在新数组上进行
- 特点:
- 读操作不需要加锁,写操作创建数组副本
- 适用于读多写少的场景
- 迭代器不支持修改操作,但保证不抛出ConcurrentModificationException
- 内存占用较高
4. Set实现类的底层原理
4.1 HashSet
- 数据结构:基于HashMap实现
- 内部实现:
- 元素作为HashMap的键存储,值使用一个静态Object对象
- 使用元素的hashCode()和equals()方法判断重复
- 无序(元素的顺序可能随时间变化)
- 允许null元素
1// HashSet核心源码简化版2public class HashSet<E> extends AbstractSet<E> implements Set<E> {3 // 实际存储元素的HashMap4 private transient HashMap<E,Object> map;5 6 // 所有值对应的虚拟对象7 private static final Object PRESENT = new Object();8 9 // 构造函数10 public HashSet() {11 map = new HashMap<>();12 }13 14 // 添加元素15 public boolean add(E e) {16 return map.put(e, PRESENT) == null;17 }18 19 // 判断是否包含元素20 public boolean contains(Object o) {21 return map.containsKey(o);22 }23 24 // 移除元素25 public boolean remove(Object o) {26 return map.remove(o) == PRESENT;27 }28 29 // 迭代器30 public Iterator<E> iterator() {31 return map.keySet().iterator();32 }33 34 // 集合大小35 public int size() {36 return map.size();37 }38}4.2 LinkedHashSet
- 数据结构:基于LinkedHashMap实现的HashSet
- 特点:
- 维护插入顺序
- 性能略低于HashSet
- 迭代性能优于HashSet
4.3 TreeSet
- 数据结构:基于TreeMap实现的NavigableSet
- 特点:
- 元素按照自然顺序或提供的比较器排序
- 查找、添加和删除操作均为O(log n)
- 不允许null元素
- 非线程安全
1// TreeSet核心源码简化版2public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E> {3 // 实际存储的TreeMap4 private transient NavigableMap<E,Object> m;5 6 // 所有值对应的虚拟对象7 private static final Object PRESENT = new Object();8 9 // 构造函数10 public TreeSet() {11 this.m = new TreeMap<>();12 }13 14 public TreeSet(Comparator<? super E> comparator) {15 this.m = new TreeMap<>(comparator);16 }17 18 // 添加元素19 public boolean add(E e) {20 return m.put(e, PRESENT) == null;21 }22 23 // 第一个元素24 public E first() {25 return m.firstKey();26 }27 28 // 最后一个元素29 public E last() {30 return m.lastKey();31 }32}5. Map实现类的底层原理
5.1 HashMap
- 数据结构:哈希表 + 链表/红黑树
- 内部实现:
- 基于数组 + 链表组成的哈希桶,链表长度超过阈值(8)时转为红黑树
- 默认初始容量为16,负载因子为0.75
- 扩容时容量翻倍
- 非线程安全
- Java 8以后引入红黑树优化长链表性能
1// HashMap核心源码简化版2public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {3 // 哈希桶数组4 transient Node<K,V>[] table;5 6 // 键值对数量7 transient int size;8 9 // 扩容阈值10 int threshold;11 12 // 负载因子13 final float loadFactor;14 15 // 默认初始容量16 static final int DEFAULT_INITIAL_CAPACITY = 16;17 18 // 默认负载因子19 static final float DEFAULT_LOAD_FACTOR = 0.75f;20 21 // 树化阈值22 static final int TREEIFY_THRESHOLD = 8;23 24 // 基本节点25 static class Node<K,V> implements Map.Entry<K,V> {26 final int hash;27 final K key;28 V value;29 Node<K,V> next;30 31 // 构造函数32 Node(int hash, K key, V value, Node<K,V> next) {33 this.hash = hash;34 this.key = key;35 this.value = value;36 this.next = next;37 }38 }39 40 // 放入键值对41 public V put(K key, V value) {42 return putVal(hash(key), key, value, false, true);43 }44 45 // 获取键对应的值46 public V get(Object key) {47 Node<K,V> e = getNode(hash(key), key);48 return e == null ? null : e.value;49 }50}HashMap的哈希冲突解决:
- 链地址法:相同哈希值的元素放在同一链表
- 红黑树优化:链表长度超过阈值时转换为红黑树
- 哈希函数优化:减少哈希冲突
5.2 LinkedHashMap
- 数据结构:哈希表 + 双向链表
- 特点:
- 继承自HashMap,增加了双向链表维护元素顺序
- 可按插入顺序或访问顺序排序(构造时指定)
- 适合构建LRU缓存
- 迭代性能优于HashMap
5.3 TreeMap
- 数据结构:红黑树
- 特点:
- 基于红黑树(自平衡二叉查找树)实现
- 键按自然顺序或提供的比较器排序
- 查找、添加和删除操作均为O(log n)
- 非线程安全
5.4 ConcurrentHashMap
-
数据结构:分段锁数组(Java 7) / 哈希表+CAS+Synchronized(Java 8+)
-
Java 7实现:
- 数据分为多个Segment,每个Segment独立加锁
- 每个Segment类似一个独立的HashMap
- 读操作无锁,写操作仅锁定对应段
- 默认16个Segment
-
Java 8及以后实现:
- 摒弃了Segment设计,直接使用Node数组
- 使用CAS和synchronized保证线程安全
- 锁粒度更细,只锁定当前桶
- 支持红黑树优化
5.5 Hashtable
- 数据结构:哈希表
- 特点:
- 线程安全(方法被synchronized修饰)
- 不允许null键和null值
- 性能不如ConcurrentHashMap
- 已被更高效的实现替代
6. Queue/Deque实现类的底层原理
6.1 ArrayDeque
- 数据结构:循环数组
- 特点:
- 实现了Deque接口,可用作栈或队列
- 无固定容量限制,自动扩容
- 不允许null元素
- 作为栈比Stack性能更好,作为队列比LinkedList性能更好
- 非线程安全
6.2 PriorityQueue
- 数据结构:基于堆(完全二叉树)
- 特点:
- 元素按优先级排序(自然顺序或比较器)
- 堆顶元素始终是最小元素
- 插入和删除的时间复杂度为O(log n)
- 非线程安全
6.3 LinkedList作为Queue/Deque
- LinkedList同时实现了List和Deque接口
- 可以用作队列、栈或双端队列
- 链表结构使其在两端增删元素的性能很好
7. 并发集合类
Java提供了多种线程安全的集合实现:
7.1 ConcurrentHashMap
- 替代Hashtable的高效并发Map实现
- 读操作完全并发,写操作局部锁定
7.2 CopyOnWriteArrayList/CopyOnWriteArraySet
- 适用于读多写少的场景
- 修改操作通过复制整个数组实现
- 迭代过程中不会抛出ConcurrentModificationException
7.3 ConcurrentSkipListMap/ConcurrentSkipListSet
- 基于跳表实现的并发排序Map/Set
- 替代TreeMap/TreeSet的并发版本
- 所有操作都是线程安全的
8. 集合工具类
8.1 Collections类
提供了操作集合的静态方法:
- 同步包装器:synchronizedList(), synchronizedMap()
- 不可变包装器:unmodifiableList(), unmodifiableMap()
- 类型安全包装器:checkedList(), checkedMap()
- 单例集合:singletonList(), singletonMap()
- 空集合:emptyList(), emptyMap()
- 排序和查找:sort(), binarySearch()
- 其他操作:reverse(), shuffle(), min(), max()
8.2 Arrays类
提供了操作数组的静态方法:
- 数组与集合转换:asList()
- 排序和查找:sort(), binarySearch()
- 复制和填充:copyOf(), fill()
- 比较:equals(), compare()
- 流操作:stream()(Java 8+)
9. 集合框架的性能比较
| 集合类 | 随机访问 | 插入/删除 | 查找 | 内存占用 | 有序性 |
|---|---|---|---|---|---|
| ArrayList | O(1) | O(n)* | O(n) | 低 | 插入顺序 |
| LinkedList | O(n) | O(1)** | O(n) | 高 | 插入顺序 |
| HashSet | 不支持 | O(1) | O(1) | 中 | 无序 |
| LinkedHashSet | 不支持 | O(1) | O(1) | 高 | 插入顺序 |
| TreeSet | 不支持 | O(log n) | O(log n) | 中 | 排序 |
| HashMap | O(1) | O(1) | O(1) | 中 | 无序 |
| LinkedHashMap | O(1) | O(1) | O(1) | 高 | 可选 |
| TreeMap | O(log n) | O(log n) | O(log n) | 中 | 排序 |
*: 末尾插入为O(1),中间插入需要移动元素
**: 需要先找到位置,实际复杂度取决于插入位置
10. 选择合适的集合类
根据需求选择合适的集合实现:
- 需要按索引访问元素:ArrayList
- 需要频繁插入、删除元素:LinkedList
- 需要保证元素唯一性:HashSet
- 需要保证元素唯一且有序:TreeSet
- 需要键值对映射:HashMap
- 需要键值对且保持插入顺序:LinkedHashMap
- 需要键值对且按键排序:TreeMap
- 需要线程安全的Map:ConcurrentHashMap
- 需要优先级队列:PriorityQueue
- 需要双端队列:ArrayDeque
40. Java中的IO模型有哪些?NIO如何实现多路复用?
Java提供了多种I/O模型来处理输入/输出操作,每种模型都有其特定的用途和性能特点。理解这些I/O模型对于开发高性能的Java应用至关重要。
1. Java I/O模型概览
Java中主要有四种I/O模型:
1.1 BIO (Blocking I/O)
- 传统的阻塞式I/O模型
- 线程发起I/O操作后阻塞等待完成
- 简单直观但扩展性较差
1.2 NIO (Non-blocking I/O)
- 非阻塞I/O,基于选择器的多路复用
- 单线程可同时处理多个连接
- 提高了资源利用率和系统吞吐量
1.3 AIO (Asynchronous I/O)
- 异步I/O,也称为NIO.2
- 操作系统完成I/O操作后通知应用程序
- 真正的异步非阻塞I/O模型
1.4 IO多路复用
- 底层依赖操作系统的select/poll/epoll机制
- NIO通过Selector实现了Java层面的多路复用
- 允许单线程监控多个文件描述符
2. BIO (Blocking I/O)
2.1 工作原理:
- 一个连接一个线程处理
- I/O操作是同步阻塞的
- 线程在执行读/写操作时被阻塞,直到操作完成
2.2 代码示例:
1// BIO服务器示例2ServerSocket serverSocket = new ServerSocket(8080);3while (true) {4 // 阻塞等待客户端连接5 Socket clientSocket = serverSocket.accept();6 7 // 为每个客户端创建一个线程处理8 new Thread(() -> {9 try {10 BufferedReader reader = new BufferedReader(11 new InputStreamReader(clientSocket.getInputStream()));12 PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);13 14 String line;15 // 阻塞读取客户端数据16 while ((line = reader.readLine()) != null) {17 writer.println("Echo: " + line);18 }19 } catch (IOException e) {20 e.printStackTrace();21 } finally {22 try {23 clientSocket.close();24 } catch (IOException e) {25 e.printStackTrace();26 }27 }28 }).start();29}2.3 BIO的缺点:
- 连接数增加时,需要创建大量线程
- 线程资源消耗大
- 线程阻塞导致低效,每个连接无论活跃与否都占用一个线程
- 适合连接数少且稳定的场景
3. NIO (Non-blocking I/O)
3.1 核心组件:
-
Buffer (缓冲区)
- 数据的容器,直接与通道交互
- 常用实现有ByteBuffer, CharBuffer, IntBuffer等
- 提供了高效的数据读写操作
-
Channel (通道)
- 双向数据通道,支持读取和写入
- 常用实现有FileChannel, SocketChannel, ServerSocketChannel等
- 可以注册到Selector上进行多路复用
-
Selector (选择器)
- 允许单线程处理多个Channel
- 基于事件驱动模型
- 通过select()方法监控注册的Channel状态
3.2 NIO工作原理:
- 创建一个Selector
- 将Channel注册到Selector上,关注特定事件
- 调用Selector的select()方法阻塞等待事件发生
- 处理已就绪的事件
- 重复步骤3-4
3.3 代码示例:
1// NIO服务器示例2// 创建Selector3Selector selector = Selector.open();45// 创建ServerSocketChannel6ServerSocketChannel serverChannel = ServerSocketChannel.open();7serverChannel.bind(new InetSocketAddress(8080));8serverChannel.configureBlocking(false); // 设置为非阻塞模式910// 注册到Selector11serverChannel.register(selector, SelectionKey.OP_ACCEPT);1213while (true) {14 // 阻塞等待事件,返回准备就绪的通道数15 int readyChannels = selector.select();16 if (readyChannels == 0) continue;17 18 // 获取就绪事件19 Set<SelectionKey> selectedKeys = selector.selectedKeys();20 Iterator<SelectionKey> keyIterator = selectedKeys.iterator();21 22 while (keyIterator.hasNext()) {23 SelectionKey key = keyIterator.next();24 25 if (key.isAcceptable()) {26 // 处理接受连接事件27 ServerSocketChannel server = (ServerSocketChannel) key.channel();28 SocketChannel client = server.accept();29 client.configureBlocking(false);30 // 注册读事件31 client.register(selector, SelectionKey.OP_READ);32 } else if (key.isReadable()) {33 // 处理读事件34 SocketChannel client = (SocketChannel) key.channel();35 ByteBuffer buffer = ByteBuffer.allocate(1024);36 int bytesRead = client.read(buffer);37 38 if (bytesRead > 0) {39 buffer.flip();40 client.write(buffer);41 buffer.clear();42 } else if (bytesRead == -1) {43 // 客户端断开连接44 client.close();45 }46 }47 48 // 从集合中移除已处理的事件49 keyIterator.remove();50 }51}3.4 Buffer的基本操作:
1// 创建缓冲区2ByteBuffer buffer = ByteBuffer.allocate(1024);34// 写入数据5buffer.put("Hello".getBytes());67// 切换到读模式8buffer.flip();910// 读取数据11byte[] data = new byte[buffer.limit()];12buffer.get(data);1314// 清空缓冲区,准备写入15buffer.clear();3.5 Buffer的三个重要属性:
- capacity:缓冲区的总容量
- position:当前读写位置
- limit:可读写的上限
3.6 Buffer的主要方法:
- flip():从写模式切换到读模式
- rewind():重置position到0,保持limit不变
- clear():清空整个缓冲区
- compact():清空已读数据,保留未读数据
- mark()/reset():标记当前位置/恢复到标记位置
4. NIO的多路复用机制
4.1 多路复用概念:
多路复用是指使用一个线程监听多个连接(Channel),只有当连接上有数据可以非阻塞地进行I/O操作时,才对这些连接进行操作,避免同步阻塞造成的线程资源浪费。
4.2 实现原理:
- 事件注册:将Channel注册到Selector,关联感兴趣的事件
- 事件监听:Selector监听所有注册的Channel
- 事件分发:当Channel有事件发生时,Selector通知应用程序处理
4.3 可注册的事件类型:
- SelectionKey.OP_READ:Channel可读
- SelectionKey.OP_WRITE:Channel可写
- SelectionKey.OP_CONNECT:客户端Channel连接完成
- SelectionKey.OP_ACCEPT:服务器Channel接受新连接
4.4 SelectionKey:
SelectionKey代表Channel在Selector中的注册关系,包含:
- 对应的Channel和Selector
- 感兴趣的事件集合
- 已就绪的事件集合
- 可附加的对象(attachment)
1// SelectionKey操作示例2SelectionKey key = channel.register(selector, SelectionKey.OP_READ);34// 修改关注的事件5key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);67// 获取已就绪事件8int readyOps = key.readyOps();9boolean isReadable = key.isReadable(); // 等价于(readyOps & SelectionKey.OP_READ) != 01011// 附加对象12key.attach(new ClientSession());13ClientSession session = (ClientSession)key.attachment();4.5 Selector的核心方法:
- select():阻塞选择准备就绪的通道
- select(long timeout):最多阻塞timeout毫秒
- selectNow():非阻塞选择
- wakeup():唤醒阻塞在select()的线程
- close():关闭选择器
4.6 NIO多路复用的底层实现:
Java NIO的多路复用在不同操作系统上有不同实现:
- Windows:基于select实现
- Linux:2.6以前使用poll,2.6以后使用epoll
- macOS/BSD:使用kqueue
Selector的底层调用:
- Linux上创建Selector实际上是创建epoll实例
- 将Channel注册到Selector相当于调用epoll_ctl
- select操作对应epoll_wait系统调用
5. AIO (Asynchronous I/O)
5.1 概念:
- Java 7引入的NIO.2包提供了异步I/O功能
- 基于回调机制:应用程序发起I/O操作后立即返回
- 操作系统完成I/O后通知应用程序
- 真正的异步非阻塞I/O模型
5.2 核心组件:
- AsynchronousChannel:异步通道接口
- AsynchronousSocketChannel:异步Socket通道
- AsynchronousServerSocketChannel:异步服务器Socket通道
- AsynchronousFileChannel:异步文件通道
- CompletionHandler:操作完成时的回调接口
5.3 工作原理:
- 应用程序发起异步I/O操作
- 指定CompletionHandler回调
- I/O操作立即返回
- 操作系统完成I/O后调用回调函数
5.4 代码示例:
1// AIO服务器示例2AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();3serverChannel.bind(new InetSocketAddress(8080));45// 接受连接6serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {7 @Override8 public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {9 // 继续接受下一个连接10 serverChannel.accept(null, this);11 12 // 分配缓冲区13 ByteBuffer buffer = ByteBuffer.allocate(1024);14 15 // 异步读取16 clientChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {17 @Override18 public void completed(Integer bytesRead, ByteBuffer buffer) {19 if (bytesRead > 0) {20 buffer.flip();21 // 异步写回22 clientChannel.write(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {23 @Override24 public void completed(Integer bytesWritten, ByteBuffer buffer) {25 if (buffer.hasRemaining()) {26 // 继续写入剩余数据27 clientChannel.write(buffer, buffer, this);28 } else {29 // 准备下一次读取30 buffer.clear();31 clientChannel.read(buffer, buffer, CompletionHandler.this);32 }33 }34 35 @Override36 public void failed(Throwable exc, ByteBuffer attachment) {37 try {38 clientChannel.close();39 } catch (IOException e) {40 e.printStackTrace();41 }42 }43 });44 }45 }46 47 @Override48 public void failed(Throwable exc, ByteBuffer attachment) {49 try {50 clientChannel.close();51 } catch (IOException e) {52 e.printStackTrace();53 }54 }55 });56 }57 58 @Override59 public void failed(Throwable exc, Void attachment) {60 exc.printStackTrace();61 }62});6364// 保持主线程不退出65Thread.currentThread().join();5.5 Future方式:
除了使用CompletionHandler回调,AIO也支持Future风格:
1// 使用Future2AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(3 Paths.get("test.txt"), StandardOpenOption.READ);4 5ByteBuffer buffer = ByteBuffer.allocate(1024);6Future<Integer> result = fileChannel.read(buffer, 0);78// 可以执行其他操作...910// 阻塞等待结果11int bytesRead = result.get(); // 阻塞直到读取完成6. 各种I/O模型的对比
| 特性 | BIO | NIO | AIO |
|---|---|---|---|
| 阻塞类型 | 阻塞 | 非阻塞 | 异步非阻塞 |
| 编程复杂度 | 简单 | 复杂 | 较复杂 |
| 调用方式 | 同步 | 同步(多路复用) | 异步 |
| 适用场景 | 连接数少 | 高并发连接 | I/O密集型 |
| 可靠性 | 较高 | 较高 | 一般 |
| 吞吐量 | 低 | 高 | 高 |
7. Reactor模式与Proactor模式
7.1 Reactor模式:
- NIO采用的是Reactor模式
- 应用程序通过Selector主动监听事件
- 事件发生时,应用程序负责处理I/O操作
- 同步非阻塞模型
Reactor模式的角色:
- Reactor:负责监听和分发事件
- Handler:处理特定事件
- Acceptor:处理客户端连接请求
7.2 Proactor模式:
- AIO采用的是Proactor模式
- 应用程序发起异步I/O操作后立即返回
- 系统完成实际I/O操作并通知应用程序
- 异步非阻塞模型
Proactor模式的角色:
- Proactor:管理异步I/O操作
- Completion Handler:处理异步I/O完成事件
- Asynchronous Operation:表示异步操作
8. NIO的适用场景与实践
8.1 适用场景:
- 高并发连接服务器
- 需要管理大量连接但活动连接较少的情况
- 长连接应用(如聊天服务器)
- 需要高吞吐量和低延迟的场景
8.2 NIO的局限性:
- 编程模型复杂
- 调试困难
- 在连接数较少时,BIO可能更简单高效
- 依赖操作系统的I/O多路复用支持
8.3 常见问题及解决方案:
-
空轮询bug:早期JDK版本的Selector在某些Linux系统上会出现CPU 100%的bug
- 解决:升级JDK或重建Selector
-
内存泄漏:未正确关闭资源可能导致内存泄漏
- 解决:使用try-with-resources或finally块确保资源关闭
-
数据传输不完整:非阻塞模式下,read/write方法可能不会一次完成所有数据传输
- 解决:循环处理,或使用更高级的缓冲区管理
-
多线程访问:Selector和相关组件不是线程安全的
- 解决:适当使用同步机制,或采用多Selector模型
8.4 NIO最佳实践:
-
Buffer管理:
- 正确处理Buffer的读写模式切换(flip/clear/compact)
- 考虑使用直接缓冲区(DirectByteBuffer)提高性能
- 注意缓冲区大小设置,避免过大或过小
-
Channel管理:
- 确保正确关闭Channel
- 适当配置Channel参数(如TCP参数)
- 对于文件Channel,考虑使用内存映射或文件锁定
-
Selector管理:
- 定期检查Selector的健康状态
- 考虑多Selector多线程模型分担负载
- 适当处理wakeup()机制
-
异常处理:
- 捕获和处理特定的I/O异常
- 实现正确的错误恢复机制
- 避免异常导致整个服务器崩溃
9. 基于Java NIO的开源框架
9.1 Netty:
- 高性能的NIO客户端/服务器框架
- 简化了NIO编程模型
- 提供了丰富的协议支持和编解码器
- 广泛用于高性能网络应用开发
9.2 Apache MINA:
- 类似Netty的NIO框架
- 提供了抽象的I/O API
- 支持多种传输协议
9.3 Grizzly:
- Oracle开发的NIO框架
- 是Glassfish服务器的网络层
- 支持HTTP/HTTPS等协议
9.4 Eclipse Vert.x:
- 基于Netty的反应式应用平台
- 提供了多语言API
- 事件驱动、非阻塞的编程模型
10. 小结与展望
10.1 选择合适的I/O模型:
- BIO:简单应用,连接数少
- NIO:高并发服务器,需要处理大量连接
- AIO:需要真正异步I/O的场景,如大文件处理
10.2 未来趋势:
- 反应式编程模型的普及
- 基于NIO的框架继续发展
- 与Java异步编程模型(CompletableFuture, Flow API)的融合
- 对新型I/O设备(如NVMe SSD)的优化支持
41. Java多线程编程的基本概念和实现方式有哪些?
多线程编程是Java语言的核心特性之一,它允许程序同时执行多个线程,提高程序的效率和响应性。理解多线程的基本概念和实现方式对于开发高效的Java应用至关重要。
1. 多线程基础概念
1.1 进程与线程的区别
-
进程(Process):
- 操作系统资源分配的基本单位
- 拥有独立的内存空间
- 进程间通信(IPC)相对复杂
-
线程(Thread):
- CPU调度的基本单位
- 共享所属进程的内存空间
- 线程间通信相对简单
- 创建和销毁开销小于进程
1.2 多线程的优势
- 提高资源利用率:CPU空闲时可执行其他线程
- 提高响应性:耗时操作不会阻塞整个程序
- 简化程序设计:将复杂任务分解为多个并行执行的子任务
1.3 多线程的挑战
- 线程安全问题:共享资源访问冲突
- 死锁、活锁、饥饿:并发编程中的常见问题
- 难以调试:并发bug难以重现和定位
- 性能开销:线程创建、上下文切换的开销
2. 线程的生命周期
Java线程的生命周期包含以下状态,由Thread.State枚举定义:
- NEW:线程创建但未启动
- RUNNABLE:线程正在运行或等待CPU时间片
- BLOCKED:线程阻塞,等待获取监视器锁
- WAITING:线程无限期等待另一个线程执行特定操作
- TIMED_WAITING:线程等待另一个线程执行操作,但有超时限制
- TERMINATED:线程已执行完毕
线程状态转换图:
1NEW2 |3 v4 RUNNABLE <------+5 / | \ |6v v v |7BLOCKED WAITING TIMED_WAITING8 \ | /9 \ | /10 v v v11 TERMINATED3. 创建线程的方式
3.1 继承Thread类
1public class MyThread extends Thread {2 @Override3 public void run() {4 // 线程执行代码5 System.out.println("Thread running: " + Thread.currentThread().getName());6 }7 8 public static void main(String[] args) {9 MyThread thread = new MyThread();10 thread.start(); // 启动线程11 }12}优点:简单直观 缺点:Java单继承限制,无法继承其他类
3.2 实现Runnable接口
1public class MyRunnable implements Runnable {2 @Override3 public void run() {4 // 线程执行代码5 System.out.println("Thread running: " + Thread.currentThread().getName());6 }7 8 public static void main(String[] args) {9 Thread thread = new Thread(new MyRunnable());10 thread.start(); // 启动线程11 12 // 使用Lambda表达式(Java 8+)13 Thread thread2 = new Thread(() -> {14 System.out.println("Lambda thread running");15 });16 thread2.start();17 }18}优点:可以继承其他类,更好地面向对象 缺点:无法直接获取线程执行结果
3.3 实现Callable接口和使用FutureTask
1public class MyCallable implements Callable<Integer> {2 @Override3 public Integer call() throws Exception {4 // 计算并返回结果5 int sum = 0;6 for (int i = 1; i <= 100; i++) {7 sum += i;8 }9 return sum;10 }11 12 public static void main(String[] args) throws Exception {13 // 创建Callable实例14 MyCallable callable = new MyCallable();15 // 包装为FutureTask16 FutureTask<Integer> futureTask = new FutureTask<>(callable);17 // 创建线程执行任务18 Thread thread = new Thread(futureTask);19 thread.start();20 21 // 获取计算结果22 Integer result = futureTask.get(); // 阻塞等待结果23 System.out.println("Result: " + result);24 }25}优点:可以获取线程执行结果,可以抛出异常 缺点:使用较复杂
3.4 使用线程池
1import java.util.concurrent.ExecutorService;2import java.util.concurrent.Executors;3import java.util.concurrent.Future;45public class ThreadPoolExample {6 public static void main(String[] args) throws Exception {7 // 创建固定大小的线程池8 ExecutorService executor = Executors.newFixedThreadPool(5);9 10 // 提交Runnable任务11 executor.execute(() -> {12 System.out.println("Runnable task executed");13 });14 15 // 提交Callable任务并获取结果16 Future<Integer> future = executor.submit(() -> {17 Thread.sleep(1000);18 return 123;19 });20 21 Integer result = future.get();22 System.out.println("Result: " + result);23 24 // 关闭线程池25 executor.shutdown();26 }27}优点:
- 重用线程,减少创建和销毁线程的开销
- 控制并发数,避免资源耗尽
- 提供任务管理和执行服务
常见的线程池类型:
- FixedThreadPool:固定大小线程池
- CachedThreadPool:可缓存线程池,自动调整大小
- SingleThreadExecutor:单线程执行器
- ScheduledThreadPool:定时任务线程池
- ForkJoinPool:Fork/Join框架使用的线程池(Java 7+)
4. 线程同步与协作
4.1 线程同步
线程同步是确保多个线程安全访问共享资源的机制。
4.1.1 synchronized关键字
1// 同步方法2public synchronized void syncMethod() {3 // 同步代码4}56// 同步代码块7public void method() {8 synchronized(this) {9 // 同步代码10 }11}1213// 同步静态方法(类锁)14public static synchronized void staticSyncMethod() {15 // 同步代码16}1718// 同步类19public void method() {20 synchronized(MyClass.class) {21 // 同步代码22 }23}4.1.2 Lock接口
1import java.util.concurrent.locks.Lock;2import java.util.concurrent.locks.ReentrantLock;34public class LockExample {5 private final Lock lock = new ReentrantLock();6 private int count = 0;7 8 public void increment() {9 lock.lock(); // 获取锁10 try {11 count++;12 } finally {13 lock.unlock(); // 释放锁14 }15 }16 17 // 支持中断的锁获取18 public void incrementInterruptibly() throws InterruptedException {19 lock.lockInterruptibly(); // 可中断锁20 try {21 count++;22 } finally {23 lock.unlock();24 }25 }26 27 // 尝试获取锁28 public boolean tryIncrement() {29 if (lock.tryLock()) { // 尝试获取锁但不阻塞30 try {31 count++;32 return true;33 } finally {34 lock.unlock();35 }36 }37 return false;38 }39}4.1.3 ReadWriteLock
1import java.util.concurrent.locks.ReadWriteLock;2import java.util.concurrent.locks.ReentrantReadWriteLock;34public class ReadWriteLockExample {5 private final ReadWriteLock rwLock = new ReentrantReadWriteLock();6 private int data = 0;7 8 public int readData() {9 rwLock.readLock().lock(); // 获取读锁10 try {11 return data;12 } finally {13 rwLock.readLock().unlock(); // 释放读锁14 }15 }16 17 public void writeData(int newData) {18 rwLock.writeLock().lock(); // 获取写锁19 try {20 data = newData;21 } finally {22 rwLock.writeLock().unlock(); // 释放写锁23 }24 }25}4.1.4 volatile关键字
1public class VolatileExample {2 private volatile boolean flag = false; // 保证可见性3 4 public void setFlag() {5 flag = true;6 }7 8 public void doWork() {9 while (!flag) {10 // 等待flag变为true11 // 由于flag是volatile的,其他线程对flag的修改对本线程可见12 }13 // flag为true时继续执行14 }15}4.2 线程协作
线程协作是线程之间相互配合完成任务的机制。
4.2.1 wait/notify/notifyAll
1public class WaitNotifyExample {2 private final Object lock = new Object();3 private boolean condition = false;4 5 public void producer() {6 synchronized (lock) {7 while (condition) {8 try {9 lock.wait(); // 等待条件变化10 } catch (InterruptedException e) {11 Thread.currentThread().interrupt();12 }13 }14 condition = true;15 // 生产数据16 lock.notifyAll(); // 通知等待的消费者17 }18 }19 20 public void consumer() {21 synchronized (lock) {22 while (!condition) {23 try {24 lock.wait(); // 等待条件变化25 } catch (InterruptedException e) {26 Thread.currentThread().interrupt();27 }28 }29 condition = false;30 // 消费数据31 lock.notifyAll(); // 通知等待的生产者32 }33 }34}4.2.2 Condition接口
1import java.util.concurrent.locks.Condition;2import java.util.concurrent.locks.Lock;3import java.util.concurrent.locks.ReentrantLock;45public class ConditionExample {6 private final Lock lock = new ReentrantLock();7 private final Condition notFull = lock.newCondition();8 private final Condition notEmpty = lock.newCondition();9 private final Object[] items = new Object[100];10 private int count = 0, putIndex = 0, takeIndex = 0;11 12 public void put(Object item) throws InterruptedException {13 lock.lock();14 try {15 while (count == items.length) {16 notFull.await(); // 队列满了,等待消费者消费17 }18 items[putIndex] = item;19 putIndex = (putIndex + 1) % items.length;20 count++;21 notEmpty.signal(); // 通知消费者可以消费了22 } finally {23 lock.unlock();24 }25 }26 27 public Object take() throws InterruptedException {28 lock.lock();29 try {30 while (count == 0) {31 notEmpty.await(); // 队列空了,等待生产者生产32 }33 Object item = items[takeIndex];34 items[takeIndex] = null;35 takeIndex = (takeIndex + 1) % items.length;36 count--;37 notFull.signal(); // 通知生产者可以生产了38 return item;39 } finally {40 lock.unlock();41 }42 }43}4.2.3 CountDownLatch
1import java.util.concurrent.CountDownLatch;23public class CountDownLatchExample {4 public static void main(String[] args) throws InterruptedException {5 CountDownLatch latch = new CountDownLatch(3); // 初始计数为36 7 // 创建3个工作线程8 for (int i = 0; i < 3; i++) {9 final int num = i;10 new Thread(() -> {11 try {12 System.out.println("Worker " + num + " is working");13 Thread.sleep(1000); // 模拟工作14 latch.countDown(); // 完成工作,计数减115 System.out.println("Worker " + num + " finished");16 } catch (InterruptedException e) {17 Thread.currentThread().interrupt();18 }19 }).start();20 }21 22 System.out.println("Main thread waiting for all workers");23 latch.await(); // 主线程等待计数器归零24 System.out.println("All workers finished, main thread continues");25 }26}4.2.4 CyclicBarrier
1import java.util.concurrent.CyclicBarrier;23public class CyclicBarrierExample {4 public static void main(String[] args) {5 // 创建栅栏,所有线程到达后执行指定任务6 CyclicBarrier barrier = new CyclicBarrier(3, () -> {7 System.out.println("All threads have reached the barrier, continue processing");8 });9 10 for (int i = 0; i < 3; i++) {11 final int num = i;12 new Thread(() -> {13 try {14 System.out.println("Thread " + num + " is working");15 Thread.sleep(1000); // 模拟工作16 System.out.println("Thread " + num + " reached the barrier");17 barrier.await(); // 等待其他线程到达18 System.out.println("Thread " + num + " continues after barrier");19 } catch (Exception e) {20 e.printStackTrace();21 }22 }).start();23 }24 }25}4.2.5 Semaphore
1import java.util.concurrent.Semaphore;23public class SemaphoreExample {4 public static void main(String[] args) {5 // 创建信号量,只允许3个线程同时访问资源6 Semaphore semaphore = new Semaphore(3);7 8 for (int i = 0; i < 6; i++) {9 final int num = i;10 new Thread(() -> {11 try {12 System.out.println("Thread " + num + " is waiting for permit");13 semaphore.acquire(); // 获取许可14 System.out.println("Thread " + num + " got permit");15 Thread.sleep(1000); // 使用资源16 } catch (InterruptedException e) {17 Thread.currentThread().interrupt();18 } finally {19 System.out.println("Thread " + num + " releases permit");20 semaphore.release(); // 释放许可21 }22 }).start();23 }24 }25}4.2.6 Phaser
1import java.util.concurrent.Phaser;23public class PhaserExample {4 public static void main(String[] args) {5 Phaser phaser = new Phaser(3); // 注册3个参与者6 7 for (int i = 0; i < 3; i++) {8 final int num = i;9 new Thread(() -> {10 // 阶段111 System.out.println("Thread " + num + " completing phase 1");12 phaser.arriveAndAwaitAdvance(); // 等待所有线程完成阶段113 14 // 阶段215 System.out.println("Thread " + num + " completing phase 2");16 phaser.arriveAndAwaitAdvance(); // 等待所有线程完成阶段217 18 // 阶段319 System.out.println("Thread " + num + " completing phase 3");20 phaser.arriveAndDeregister(); // 完成并退出Phaser21 }).start();22 }23 }24}5. 线程安全的集合类
Java提供了多种线程安全的集合类,适用于不同场景:
5.1 同步集合类
- Vector:线程安全的ArrayList
- Hashtable:线程安全的HashMap
- Collections.synchronizedXxx():将普通集合包装为同步集合
1List<String> syncList = Collections.synchronizedList(new ArrayList<>());2Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());5.2 并发集合类
- ConcurrentHashMap:分段锁实现的线程安全HashMap
- CopyOnWriteArrayList:写时复制的ArrayList
- CopyOnWriteArraySet:基于CopyOnWriteArrayList的Set实现
- ConcurrentSkipListMap:基于跳表的排序Map
- ConcurrentSkipListSet:基于ConcurrentSkipListMap的排序Set
1Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();2List<String> concurrentList = new CopyOnWriteArrayList<>();5.3 阻塞队列
- ArrayBlockingQueue:基于数组的有界阻塞队列
- LinkedBlockingQueue:基于链表的可选有界阻塞队列
- PriorityBlockingQueue:基于优先级堆的无界阻塞队列
- DelayQueue:延迟元素队列
- SynchronousQueue:无缓冲的阻塞队列,直接交付
1BlockingQueue<Task> taskQueue = new ArrayBlockingQueue<>(100);23// 生产者4void producer() throws InterruptedException {5 Task task = createTask();6 taskQueue.put(task); // 如果队列满,会阻塞7}89// 消费者10void consumer() throws InterruptedException {11 Task task = taskQueue.take(); // 如果队列空,会阻塞12 processTask(task);13}6. ThreadLocal
ThreadLocal提供线程局部变量,每个线程都有自己独立的副本,实现线程隔离。
1public class ThreadLocalExample {2 // 创建ThreadLocal变量3 private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = 4 ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));5 6 public static String formatDate(Date date) {7 // 获取当前线程的SimpleDateFormat8 return dateFormatThreadLocal.get().format(date);9 }10 11 public static void main(String[] args) {12 // 多个线程使用同一个方法,但每个线程都有自己的SimpleDateFormat实例13 for (int i = 0; i < 10; i++) {14 new Thread(() -> {15 System.out.println(formatDate(new Date()));16 // 使用完毕后清除,防止内存泄漏17 dateFormatThreadLocal.remove();18 }).start();19 }20 }21}注意:使用ThreadLocal时要注意及时调用remove()方法,防止内存泄漏。
7. 线程安全性
7.1 线程安全的定义
线程安全指的是在多线程环境下,对共享资源的操作不会产生不可预期的结果。
7.2 实现线程安全的方法
- 不可变性:创建不可变对象,如String、Integer等
- 互斥同步:使用synchronized、Lock等同步访问共享资源
- 非阻塞同步:使用CAS(Compare And Swap)等原子操作
- 避免共享:使用ThreadLocal等确保资源不共享
- 线程安全类:使用现成的线程安全集合和工具类
7.3 线程安全的实例设计
1// 不可变对象2public final class ImmutableValue {3 private final int value;4 5 public ImmutableValue(int value) {6 this.value = value;7 }8 9 public int getValue() {10 return value;11 }12 13 public ImmutableValue add(int valueToAdd) {14 return new ImmutableValue(this.value + valueToAdd);15 }16}1718// 使用原子类19import java.util.concurrent.atomic.AtomicInteger;2021public class AtomicCounter {22 private AtomicInteger count = new AtomicInteger(0);23 24 public void increment() {25 count.incrementAndGet(); // 原子操作,无需额外同步26 }27 28 public int getCount() {29 return count.get();30 }31}8. 并发编程的最佳实践
- 优先使用不可变对象:避免同步问题
- 减少锁的粒度:只锁定必要的代码块
- 避免在循环中加锁:尽量将锁移到循环外
- 避免过度同步:会降低并发性能
- 优先使用并发集合:而非同步包装器
- 正确使用wait/notify:总是在循环中检查等待条件
- 避免嵌套锁:降低死锁风险
- 设置合理的线程池大小:考虑CPU核心数和任务类型
- 使用线程池执行异步任务:优于手动创建线程
- 使用非阻塞算法:提高并发性能
9. Java 8+中的并发增强
9.1 CompletableFuture
1import java.util.concurrent.CompletableFuture;23public class CompletableFutureExample {4 public static void main(String[] args) throws Exception {5 // 异步执行任务6 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {7 try {8 Thread.sleep(1000);9 } catch (InterruptedException e) {10 Thread.currentThread().interrupt();11 }12 return "Hello";13 });14 15 // 对结果进行转换16 CompletableFuture<String> greetingFuture = future.thenApply(s -> s + " World");17 18 // 消费结果19 greetingFuture.thenAccept(System.out::println);20 21 // 组合两个Future22 CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");23 CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");24 25 CompletableFuture<String> combined = future1.thenCombine(future2, (s1, s2) -> s1 + " " + s2);26 System.out.println(combined.get()); // Hello World27 }28}9.2 并行流(Parallel Streams)
1import java.util.Arrays;2import java.util.List;34public class ParallelStreamExample {5 public static void main(String[] args) {6 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);7 8 // 串行流9 long count1 = numbers.stream()10 .filter(n -> n % 2 == 0)11 .count();12 13 // 并行流14 long count2 = numbers.parallelStream()15 .filter(n -> n % 2 == 0)16 .count();17 }18}9.3 StampedLock
1import java.util.concurrent.locks.StampedLock;23public class StampedLockExample {4 private double x, y;5 private final StampedLock lock = new StampedLock();6 7 // 写锁8 public void move(double deltaX, double deltaY) {9 long stamp = lock.writeLock();10 try {11 x += deltaX;12 y += deltaY;13 } finally {14 lock.unlockWrite(stamp);15 }16 }17 18 // 乐观读19 public double distanceFromOrigin() {20 // 乐观读取(无锁)21 long stamp = lock.tryOptimisticRead();22 double currentX = x, currentY = y;23 24 // 检查读期间是否有修改25 if (!lock.validate(stamp)) {26 // 悲观读取(获取读锁)27 stamp = lock.readLock();28 try {29 currentX = x;30 currentY = y;31 } finally {32 lock.unlockRead(stamp);33 }34 }35 36 return Math.sqrt(currentX * currentX + currentY * currentY);37 }38}9.4 CountedCompleter
1import java.util.concurrent.CountedCompleter;2import java.util.concurrent.ForkJoinPool;34public class CountedCompleterExample {5 public static void main(String[] args) {6 int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};7 Sum sum = new Sum(null, array, 0, array.length);8 ForkJoinPool.commonPool().invoke(sum);9 System.out.println("Sum: " + sum.result);10 }11 12 static class Sum extends CountedCompleter<Void> {13 final int[] array;14 final int lo, hi;15 int result;16 17 Sum(Sum parent, int[] array, int lo, int hi) {18 super(parent);19 this.array = array;20 this.lo = lo;21 this.hi = hi;22 }23 24 @Override25 public void compute() {26 if (hi - lo > 1) {27 int mid = (lo + hi) >>> 1;28 // 创建子任务29 Sum left = new Sum(this, array, lo, mid);30 Sum right = new Sum(this, array, mid, hi);31 // 设置待完成的子任务数量32 setPendingCount(2);33 // 启动子任务34 left.fork();35 right.fork();36 } else {37 if (hi > lo)38 result = array[lo];39 tryComplete(); // 尝试完成当前任务40 }41 }42 43 @Override44 public void onCompletion(CountedCompleter<?> caller) {45 if (caller != this) {46 Sum child = (Sum)caller;47 result += child.result;48 }49 }50 }51}10. 多线程调试与故障排除
10.1 常见并发问题
- 竞态条件:多线程访问共享资源时结果依赖于线程执行顺序
- 死锁:两个或多个线程互相等待对方持有的锁
- 活锁:线程不断重试失败的操作,但互相让步而无法前进
- 线程饥饿:线程无法获取所需资源而无法继续执行
- 过度同步:同步过多导致性能下降
10.2 死锁检测与解决
死锁发生的四个必要条件:
- 互斥:资源不能被共享
- 持有并等待:线程持有资源同时等待其他资源
- 不可剥夺:资源只能由持有者自愿释放
- 循环等待:存在循环的资源依赖链
解决死锁的方法:
- 避免嵌套锁:按固定顺序获取锁
- 使用tryLock():带超时的锁获取
- 使用无锁数据结构:避免使用显式锁
- 检测与恢复:定期检查死锁并回滚操作
死锁示例及修复:
1// 死锁示例2public class DeadlockExample {3 private final Object resource1 = new Object();4 private final Object resource2 = new Object();5 6 public void method1() {7 synchronized(resource1) {8 System.out.println("Thread 1: Holding resource 1...");9 try { Thread.sleep(100); } catch (InterruptedException e) {}10 11 synchronized(resource2) {12 System.out.println("Thread 1: Holding resource 1 & 2...");13 }14 }15 }16 17 public void method2() {18 synchronized(resource2) { // 锁获取顺序与method1相反,可能导致死锁19 System.out.println("Thread 2: Holding resource 2...");20 try { Thread.sleep(100); } catch (InterruptedException e) {}21 22 synchronized(resource1) {23 System.out.println("Thread 2: Holding resource 2 & 1...");24 }25 }26 }27 28 // 修复:保持一致的锁获取顺序29 public void method2Fixed() {30 synchronized(resource1) { // 与method1相同的锁获取顺序31 System.out.println("Thread 2: Holding resource 1...");32 try { Thread.sleep(100); } catch (InterruptedException e) {}33 34 synchronized(resource2) {35 System.out.println("Thread 2: Holding resource 1 & 2...");36 }37 }38 }39}10.3 线程转储(Thread Dump)分析
线程转储包含所有线程的状态信息,是诊断线程问题的重要工具。
获取线程转储的方法:
jstack <pid>命令- 在JVM中发送SIGQUIT信号(Linux/Unix)
- 在JVisualVM、JConsole等工具中查看
线程状态分析:
- RUNNABLE:正在运行或等待系统资源
- BLOCKED:等待进入同步块
- WAITING/TIMED_WAITING:等待其他线程操作
- PARKED:通过LockSupport.park()被挂起
42. 设计模式在Java中的应用有哪些?
设计模式是软件开发中解决常见问题的可重用解决方案,是经过实践检验的最佳实践。Java作为一种面向对象编程语言,广泛应用了各类设计模式。理解这些模式能帮助开发者编写更灵活、可维护的代码。
1. 创建型模式(Creational Patterns)
创建型模式关注对象的创建机制,通过控制对象创建过程来减少系统的复杂性。
1.1 单例模式(Singleton Pattern)
确保一个类只有一个实例,并提供一个全局访问点。
实现方式:
- 饿汉式:类加载时创建实例
1public class EagerSingleton {2 // 在类加载时就创建实例3 private static final EagerSingleton INSTANCE = new EagerSingleton();4 5 // 私有构造函数,防止外部实例化6 private EagerSingleton() {}7 8 public static EagerSingleton getInstance() {9 return INSTANCE;10 }11}- 懒汉式:延迟创建实例,线程安全版本
1public class LazySingleton {2 // 声明但不初始化实例3 private static volatile LazySingleton instance;4 5 private LazySingleton() {}6 7 // 双重检查锁定8 public static LazySingleton getInstance() {9 if (instance == null) {10 synchronized (LazySingleton.class) {11 if (instance == null) {12 instance = new LazySingleton();13 }14 }15 }16 return instance;17 }18}- 静态内部类:延迟加载且线程安全
1public class StaticInnerClassSingleton {2 private StaticInnerClassSingleton() {}3 4 private static class SingletonHolder {5 private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();6 }7 8 public static StaticInnerClassSingleton getInstance() {9 return SingletonHolder.INSTANCE;10 }11}- 枚举实现:最简洁的实现方式,自动线程安全且防止序列化问题
1public enum EnumSingleton {2 INSTANCE;3 4 // 可以添加方法和属性5 public void doSomething() {6 // 方法实现7 }8}应用:Spring中的Bean默认是单例的,Java运行时中的Runtime类等。
1.2 工厂模式(Factory Pattern)
简单工厂模式:由一个工厂类根据传入的参数决定创建哪一种产品类的实例。
1public class ShapeFactory {2 public Shape createShape(String type) {3 if (type == null) {4 return null;5 }6 if (type.equalsIgnoreCase("CIRCLE")) {7 return new Circle();8 } else if (type.equalsIgnoreCase("RECTANGLE")) {9 return new Rectangle();10 } else if (type.equalsIgnoreCase("SQUARE")) {11 return new Square();12 }13 return null;14 }15}工厂方法模式:定义一个创建对象的接口,让子类决定实例化哪个类。
1// 抽象工厂2public abstract class ShapeFactory {3 public abstract Shape createShape();4}56// 具体工厂7public class CircleFactory extends ShapeFactory {8 @Override9 public Shape createShape() {10 return new Circle();11 }12}1314public class RectangleFactory extends ShapeFactory {15 @Override16 public Shape createShape() {17 return new Rectangle();18 }19}抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。
1// 抽象工厂2public interface GUIFactory {3 Button createButton();4 Checkbox createCheckbox();5}67// 具体工厂8public class WindowsFactory implements GUIFactory {9 @Override10 public Button createButton() {11 return new WindowsButton();12 }13 14 @Override15 public Checkbox createCheckbox() {16 return new WindowsCheckbox();17 }18}1920public class MacFactory implements GUIFactory {21 @Override22 public Button createButton() {23 return new MacButton();24 }25 26 @Override27 public Checkbox createCheckbox() {28 return new MacCheckbox();29 }30}应用:Java Collections中的集合工厂方法,JDBC中获取连接的DriverManager等。
1.3 建造者模式(Builder Pattern)
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
1public class Computer {2 // 必要参数3 private final String CPU;4 private final String RAM;5 // 可选参数6 private final String storage;7 private final String graphicsCard;8 9 private Computer(Builder builder) {10 this.CPU = builder.CPU;11 this.RAM = builder.RAM;12 this.storage = builder.storage;13 this.graphicsCard = builder.graphicsCard;14 }15 16 public static class Builder {17 // 必要参数18 private final String CPU;19 private final String RAM;20 // 可选参数21 private String storage;22 private String graphicsCard;23 24 public Builder(String CPU, String RAM) {25 this.CPU = CPU;26 this.RAM = RAM;27 }28 29 public Builder storage(String storage) {30 this.storage = storage;31 return this;32 }33 34 public Builder graphicsCard(String graphicsCard) {35 this.graphicsCard = graphicsCard;36 return this;37 }38 39 public Computer build() {40 return new Computer(this);41 }42 }43}4445// 使用建造者模式46Computer computer = new Computer.Builder("Intel i7", "16GB")47 .storage("1TB SSD")48 .graphicsCard("NVIDIA RTX 3080")49 .build();应用:Java中的StringBuilder、StringBuffer、Lombok的@Builder注解、Spring中的BeanDefinitionBuilder等。
1.4 原型模式(Prototype Pattern)
通过复制现有实例来创建新的实例,而不是通过构造函数。
1// Cloneable接口实现原型模式2public class Shape implements Cloneable {3 private String type;4 5 public String getType() {6 return type;7 }8 9 public void setType(String type) {10 this.type = type;11 }12 13 @Override14 public Shape clone() {15 try {16 return (Shape) super.clone();17 } catch (CloneNotSupportedException e) {18 return null;19 }20 }21}2223// 使用原型模式24Shape circle = new Shape();25circle.setType("Circle");26Shape clonedCircle = circle.clone();应用:Java中的Object.clone()方法、Spring中的Bean作用域prototype等。
2. 结构型模式(Structural Patterns)
结构型模式关注如何组合类和对象以形成更大的结构,同时保持结构的灵活性和高效性。
2.1 适配器模式(Adapter Pattern)
将一个类的接口转换成客户希望的另一个接口,使原本由于接口不兼容而不能一起工作的类能一起工作。
1// 目标接口2public interface MediaPlayer {3 void play(String audioType, String fileName);4}56// 被适配的接口7public interface AdvancedMediaPlayer {8 void playVlc(String fileName);9 void playMp4(String fileName);10}1112// 被适配的类13public class VlcPlayer implements AdvancedMediaPlayer {14 @Override15 public void playVlc(String fileName) {16 System.out.println("Playing vlc file: " + fileName);17 }18 19 @Override20 public void playMp4(String fileName) {21 // 什么都不做22 }23}2425// 适配器26public class MediaAdapter implements MediaPlayer {27 private AdvancedMediaPlayer advancedMusicPlayer;28 29 public MediaAdapter(String audioType) {30 if (audioType.equalsIgnoreCase("vlc")) {31 advancedMusicPlayer = new VlcPlayer();32 } else if (audioType.equalsIgnoreCase("mp4")) {33 advancedMusicPlayer = new Mp4Player();34 }35 }36 37 @Override38 public void play(String audioType, String fileName) {39 if (audioType.equalsIgnoreCase("vlc")) {40 advancedMusicPlayer.playVlc(fileName);41 } else if (audioType.equalsIgnoreCase("mp4")) {42 advancedMusicPlayer.playMp4(fileName);43 }44 }45}应用:Java I/O中的InputStreamReader和OutputStreamWriter、JDBC中的适配器等。
2.2 装饰器模式(Decorator Pattern)
动态地给一个对象添加一些额外的职责,比生成子类更加灵活。
1// 组件接口2public interface Component {3 void operation();4}56// 具体组件7public class ConcreteComponent implements Component {8 @Override9 public void operation() {10 System.out.println("ConcreteComponent operation");11 }12}1314// 装饰器15public abstract class Decorator implements Component {16 protected Component component;17 18 public Decorator(Component component) {19 this.component = component;20 }21 22 @Override23 public void operation() {24 component.operation();25 }26}2728// 具体装饰器29public class ConcreteDecoratorA extends Decorator {30 public ConcreteDecoratorA(Component component) {31 super(component);32 }33 34 @Override35 public void operation() {36 super.operation();37 addedBehavior();38 }39 40 private void addedBehavior() {41 System.out.println("Added behavior from ConcreteDecoratorA");42 }43}应用:Java I/O中的装饰器模式(如InputStream、BufferedInputStream等)、Swing中的BorderDecorator等。
2.3 代理模式(Proxy Pattern)
为其他对象提供一种代理以控制对这个对象的访问。
1// 接口2public interface Image {3 void display();4}56// 实际类7public class RealImage implements Image {8 private String fileName;9 10 public RealImage(String fileName) {11 this.fileName = fileName;12 loadFromDisk();13 }14 15 private void loadFromDisk() {16 System.out.println("Loading " + fileName);17 }18 19 @Override20 public void display() {21 System.out.println("Displaying " + fileName);22 }23}2425// 代理类26public class ProxyImage implements Image {27 private RealImage realImage;28 private String fileName;29 30 public ProxyImage(String fileName) {31 this.fileName = fileName;32 }33 34 @Override35 public void display() {36 if (realImage == null) {37 realImage = new RealImage(fileName);38 }39 realImage.display();40 }41}4243// 使用代理44Image image = new ProxyImage("test.jpg");45// 第一次调用display()时才加载图片46image.display();应用:Spring中的AOP和依赖注入、Hibernate中的延迟加载、Java RMI等。
2.4 组合模式(Composite Pattern)
将对象组合成树形结构以表示"部分-整体"的层次结构,使客户端可以统一对待单个对象和组合对象。
1// 组件接口2public interface Component {3 void operation();4}56// 叶子组件7public class Leaf implements Component {8 private String name;9 10 public Leaf(String name) {11 this.name = name;12 }13 14 @Override15 public void operation() {16 System.out.println("Leaf " + name + " operation");17 }18}1920// 复合组件21public class Composite implements Component {22 private List<Component> children = new ArrayList<>();23 private String name;24 25 public Composite(String name) {26 this.name = name;27 }28 29 public void add(Component component) {30 children.add(component);31 }32 33 public void remove(Component component) {34 children.remove(component);35 }36 37 @Override38 public void operation() {39 System.out.println("Composite " + name + " operation");40 for (Component child : children) {41 child.operation();42 }43 }44}应用:Swing中的组件层次结构、文件系统的表示等。
2.5 外观模式(Facade Pattern)
为子系统中的一组接口提供一个一致的界面,使子系统更容易使用。
1// 子系统类2public class CPU {3 public void freeze() { System.out.println("CPU freeze"); }4 public void jump(long position) { System.out.println("CPU jump to position " + position); }5 public void execute() { System.out.println("CPU execute"); }6}78public class Memory {9 public void load(long position, byte[] data) {10 System.out.println("Memory load from position " + position);11 }12}1314public class HardDrive {15 public byte[] read(long lba, int size) {16 System.out.println("HardDrive read sector " + lba);17 return new byte[size];18 }19}2021// 外观类22public class ComputerFacade {23 private CPU cpu;24 private Memory memory;25 private HardDrive hardDrive;26 27 public ComputerFacade() {28 this.cpu = new CPU();29 this.memory = new Memory();30 this.hardDrive = new HardDrive();31 }32 33 public void start() {34 cpu.freeze();35 memory.load(0, hardDrive.read(0, 1024));36 cpu.jump(0);37 cpu.execute();38 }39}4041// 使用外观模式42ComputerFacade computer = new ComputerFacade();43computer.start();应用:SLF4J对不同日志框架的抽象、Spring中的JdbcTemplate等。
2.6 桥接模式(Bridge Pattern)
将抽象部分与其实现部分分离,使它们都可以独立地变化。
1// 实现接口2public interface DrawAPI {3 void drawCircle(int radius, int x, int y);4}56// 具体实现7public class RedCircle implements DrawAPI {8 @Override9 public void drawCircle(int radius, int x, int y) {10 System.out.println("Drawing Circle[ color: red, radius: " + radius + ", x: " + x + ", y: " + y + "]");11 }12}1314public class GreenCircle implements DrawAPI {15 @Override16 public void drawCircle(int radius, int x, int y) {17 System.out.println("Drawing Circle[ color: green, radius: " + radius + ", x: " + x + ", y: " + y + "]");18 }19}2021// 抽象类22public abstract class Shape {23 protected DrawAPI drawAPI;24 25 protected Shape(DrawAPI drawAPI) {26 this.drawAPI = drawAPI;27 }28 29 public abstract void draw();30}3132// 扩展抽象类33public class Circle extends Shape {34 private int x, y, radius;35 36 public Circle(int x, int y, int radius, DrawAPI drawAPI) {37 super(drawAPI);38 this.x = x;39 this.y = y;40 this.radius = radius;41 }42 43 @Override44 public void draw() {45 drawAPI.drawCircle(radius, x, y);46 }47}4849// 使用桥接模式50Shape redCircle = new Circle(100, 100, 10, new RedCircle());51Shape greenCircle = new Circle(100, 100, 10, new GreenCircle());52redCircle.draw();53greenCircle.draw();应用:JDBC中的Driver和DriverManager、AWT中的平台相关实现等。
3. 行为型模式(Behavioral Patterns)
行为型模式关注对象间的通信,描述如何分配职责和算法。
3.1 策略模式(Strategy Pattern)
定义一系列算法,封装每个算法,并使它们可以互换。策略模式让算法独立于使用它的客户端。
1// 策略接口2public interface PaymentStrategy {3 void pay(int amount);4}56// 具体策略7public class CreditCardStrategy implements PaymentStrategy {8 private String cardNumber;9 private String cvv;10 private String expiryDate;11 12 public CreditCardStrategy(String cardNumber, String cvv, String expiryDate) {13 this.cardNumber = cardNumber;14 this.cvv = cvv;15 this.expiryDate = expiryDate;16 }17 18 @Override19 public void pay(int amount) {20 System.out.println(amount + " paid with credit card");21 }22}2324public class PayPalStrategy implements PaymentStrategy {25 private String email;26 private String password;27 28 public PayPalStrategy(String email, String password) {29 this.email = email;30 this.password = password;31 }32 33 @Override34 public void pay(int amount) {35 System.out.println(amount + " paid using PayPal");36 }37}3839// 上下文40public class ShoppingCart {41 private PaymentStrategy paymentStrategy;42 43 public void setPaymentStrategy(PaymentStrategy paymentStrategy) {44 this.paymentStrategy = paymentStrategy;45 }46 47 public void checkout(int amount) {48 paymentStrategy.pay(amount);49 }50}5152// 使用策略模式53ShoppingCart cart = new ShoppingCart();54cart.setPaymentStrategy(new CreditCardStrategy("1234-5678-9012-3456", "123", "12/24"));55cart.checkout(100);5657cart.setPaymentStrategy(new PayPalStrategy("example@example.com", "password"));58cart.checkout(200);应用:Java中的Comparator接口、Spring中的Resource接口等。
3.2 观察者模式(Observer Pattern)
定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会自动收到通知并更新。
1// 观察者接口2public interface Observer {3 void update(String message);4}56// 具体观察者7public class User implements Observer {8 private String name;9 10 public User(String name) {11 this.name = name;12 }13 14 @Override15 public void update(String message) {16 System.out.println(name + " received: " + message);17 }18}1920// 被观察者/主题21public class Topic {22 private List<Observer> observers = new ArrayList<>();23 private String message;24 25 public void register(Observer observer) {26 observers.add(observer);27 }28 29 public void unregister(Observer observer) {30 observers.remove(observer);31 }32 33 public void postMessage(String message) {34 this.message = message;35 notifyObservers();36 }37 38 public void notifyObservers() {39 for (Observer observer : observers) {40 observer.update(message);41 }42 }43}4445// 使用观察者模式46Topic topic = new Topic();47Observer user1 = new User("User 1");48Observer user2 = new User("User 2");4950topic.register(user1);51topic.register(user2);52topic.postMessage("Hello World!");应用:Java中的事件监听机制、JavaBeans中的属性变更通知机制、Spring中的ApplicationEvent等。
3.3 命令模式(Command Pattern)
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
1// 命令接口2public interface Command {3 void execute();4}56// 具体命令7public class LightOnCommand implements Command {8 private Light light;9 10 public LightOnCommand(Light light) {11 this.light = light;12 }13 14 @Override15 public void execute() {16 light.turnOn();17 }18}1920public class LightOffCommand implements Command {21 private Light light;22 23 public LightOffCommand(Light light) {24 this.light = light;25 }26 27 @Override28 public void execute() {29 light.turnOff();30 }31}3233// 接收者34public class Light {35 public void turnOn() {36 System.out.println("Light is on");37 }38 39 public void turnOff() {40 System.out.println("Light is off");41 }42}4344// 调用者45public class RemoteControl {46 private Command command;47 48 public void setCommand(Command command) {49 this.command = command;50 }51 52 public void pressButton() {53 command.execute();54 }55}5657// 使用命令模式58Light light = new Light();59Command lightOn = new LightOnCommand(light);60Command lightOff = new LightOffCommand(light);6162RemoteControl remote = new RemoteControl();63remote.setCommand(lightOn);64remote.pressButton();6566remote.setCommand(lightOff);67remote.pressButton();应用:Java中的Runnable接口、Swing中的Action接口、Spring中的JdbcTemplate中的回调等。
3.4 模板方法模式(Template Method Pattern)
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变算法的结构即可重定义算法的某些特定步骤。
1// 抽象类定义模板方法2public abstract class Game {3 // 模板方法,定义算法骨架4 public final void play() {5 initialize();6 startPlay();7 endPlay();8 }9 10 // 子类必须实现的抽象方法11 protected abstract void initialize();12 protected abstract void startPlay();13 protected abstract void endPlay();14}1516// 具体子类17public class Cricket extends Game {18 @Override19 protected void initialize() {20 System.out.println("Cricket Game Initialized!");21 }22 23 @Override24 protected void startPlay() {25 System.out.println("Cricket Game Started!");26 }27 28 @Override29 protected void endPlay() {30 System.out.println("Cricket Game Finished!");31 }32}3334public class Football extends Game {35 @Override36 protected void initialize() {37 System.out.println("Football Game Initialized!");38 }39 40 @Override41 protected void startPlay() {42 System.out.println("Football Game Started!");43 }44 45 @Override46 protected void endPlay() {47 System.out.println("Football Game Finished!");48 }49}5051// 使用模板方法模式52Game cricket = new Cricket();53cricket.play();5455Game football = new Football();56football.play();应用:Java中的各种抽象类、Spring中的JdbcTemplate、Hibernate中的各种回调等。
3.5 迭代器模式(Iterator Pattern)
提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
1// 迭代器接口2public interface Iterator<T> {3 boolean hasNext();4 T next();5}67// 容器接口8public interface Container<T> {9 Iterator<T> getIterator();10}1112// 具体容器13public class NameRepository implements Container<String> {14 private String[] names = {"Robert", "John", "Julie", "Lora"};15 16 @Override17 public Iterator<String> getIterator() {18 return new NameIterator();19 }20 21 private class NameIterator implements Iterator<String> {22 private int index;23 24 @Override25 public boolean hasNext() {26 return index < names.length;27 }28 29 @Override30 public String next() {31 if (hasNext()) {32 return names[index++];33 }34 return null;35 }36 }37}3839// 使用迭代器模式40Container<String> namesContainer = new NameRepository();41Iterator<String> iterator = namesContainer.getIterator();4243while (iterator.hasNext()) {44 String name = iterator.next();45 System.out.println("Name: " + name);46}应用:Java集合框架中的Iterator接口等。
4. 其他重要设计模式
4.1 状态模式(State Pattern)
允许对象在内部状态改变时改变它的行为,使对象看起来好像修改了它的类。
1// 状态接口2public interface State {3 void doAction(Context context);4}56// 具体状态7public class StartState implements State {8 @Override9 public void doAction(Context context) {10 System.out.println("Player is in start state");11 context.setState(this);12 }13 14 @Override15 public String toString() {16 return "Start State";17 }18}1920public class StopState implements State {21 @Override22 public void doAction(Context context) {23 System.out.println("Player is in stop state");24 context.setState(this);25 }26 27 @Override28 public String toString() {29 return "Stop State";30 }31}3233// 上下文34public class Context {35 private State state;36 37 public Context() {38 state = null;39 }40 41 public void setState(State state) {42 this.state = state;43 }44 45 public State getState() {46 return state;47 }48}4950// 使用状态模式51Context context = new Context();5253StartState startState = new StartState();54startState.doAction(context);55System.out.println(context.getState().toString());5657StopState stopState = new StopState();58stopState.doAction(context);59System.out.println(context.getState().toString());应用:Java线程的状态变化、工作流引擎中的状态迁移等。
4.2 责任链模式(Chain of Responsibility Pattern)
为请求创建一个接收者对象的链,每个接收者都包含对另一个接收者的引用。沿着这条链传递请求,直到有一个对象处理它。
1// 处理者抽象类2public abstract class AbstractLogger {3 public static final int INFO = 1;4 public static final int DEBUG = 2;5 public static final int ERROR = 3;6 7 protected int level;8 protected AbstractLogger nextLogger;9 10 public void setNextLogger(AbstractLogger nextLogger) {11 this.nextLogger = nextLogger;12 }13 14 public void logMessage(int level, String message) {15 if (this.level <= level) {16 write(message);17 }18 if (nextLogger != null) {19 nextLogger.logMessage(level, message);20 }21 }22 23 protected abstract void write(String message);24}2526// 具体处理者27public class ConsoleLogger extends AbstractLogger {28 public ConsoleLogger(int level) {29 this.level = level;30 }31 32 @Override33 protected void write(String message) {34 System.out.println("Standard Console::Logger: " + message);35 }36}3738public class FileLogger extends AbstractLogger {39 public FileLogger(int level) {40 this.level = level;41 }42 43 @Override44 protected void write(String message) {45 System.out.println("File::Logger: " + message);46 }47}4849// 使用责任链模式50AbstractLogger loggerChain = getChainOfLoggers();51loggerChain.logMessage(AbstractLogger.INFO, "This is an information.");52loggerChain.logMessage(AbstractLogger.DEBUG, "This is a debug level information.");53loggerChain.logMessage(AbstractLogger.ERROR, "This is an error information.");5455// 创建责任链56private static AbstractLogger getChainOfLoggers() {57 AbstractLogger errorLogger = new ErrorLogger(AbstractLogger.ERROR);58 AbstractLogger fileLogger = new FileLogger(AbstractLogger.DEBUG);59 AbstractLogger consoleLogger = new ConsoleLogger(AbstractLogger.INFO);60 61 errorLogger.setNextLogger(fileLogger);62 fileLogger.setNextLogger(consoleLogger);63 64 return errorLogger;65}应用:Java中的异常处理机制、Servlet中的过滤器链、Spring中的拦截器链等。
5. 设计模式在Java实际应用中的最佳实践
5.1 选择合适的设计模式
- 分析问题的具体需求和约束条件
- 考虑模式的优缺点和适用场景
- 选择最简单、最适合的设计模式
- 避免过度设计和滥用设计模式
5.2 设计模式的组合使用
实际应用中,通常需要组合多种设计模式来解决复杂问题。例如:
- Spring框架同时使用了工厂模式、单例模式、代理模式、观察者模式等
- Java集合框架中使用了迭代器模式、工厂模式、装饰器模式等
- Java I/O中使用了装饰器模式、工厂模式、适配器模式等
5.3 设计模式的实际案例
Spring框架中的设计模式应用:
- IOC容器:工厂模式、单例模式
- AOP:代理模式、装饰器模式
- MVC:复合模式(前端控制器、视图助手、模板视图)
- 事务管理:模板方法模式、代理模式
Java集合框架中的设计模式应用:
- ArrayList/LinkedList等:迭代器模式
- Collections.unmodifiableXxx():装饰器模式
- Collections.synchronizedXxx():装饰器模式
- Arrays.asList():适配器模式
Java并发包中的设计模式应用:
- Executor框架:命令模式
- Future/Callable:命令模式
- CompletableFuture:观察者模式
- ThreadPoolExecutor:工厂方法模式、模板方法模式
43. Java反射机制的原理和应用有哪些?
**反射(Reflection)**是Java的一个强大特性,允许程序在运行时检查和操作类、接口、方法和字段。通过反射,程序可以获取到在编译时无法获取的类型信息,并能够实例化对象、调用方法、访问和修改字段,即使它们是私有的。
1. 反射的基本概念
反射允许程序:
- 在运行时检查类、接口、字段和方法
- 在运行时创建类的实例
- 在运行时获取和设置对象的字段值
- 在运行时调用对象的方法
- 在运行时检查注解
反射的核心API位于java.lang.reflect包中,主要包括:
Class<?>: 类的元信息Field: 表示类的成员变量Method: 表示类的方法Constructor: 表示类的构造方法Modifier: 提供访问修饰符信息的工具类Array: 提供动态创建和访问数组的静态方法Parameter: Java 8后添加,提供方法参数信息
2. 获取Class对象的方式
反射的起点是获取一个类的Class对象,有以下几种方式:
1// 1. 通过类名.class2Class<?> clazz1 = String.class;34// 2. 通过对象的getClass()方法5String str = "Hello";6Class<?> clazz2 = str.getClass();78// 3. 通过Class.forName()方法9try {10 Class<?> clazz3 = Class.forName("java.lang.String");11} catch (ClassNotFoundException e) {12 e.printStackTrace();13}1415// 4. 通过ClassLoader16ClassLoader classLoader = Thread.currentThread().getContextClassLoader();17try {18 Class<?> clazz4 = classLoader.loadClass("java.lang.String");19} catch (ClassNotFoundException e) {20 e.printStackTrace();21}2223// 5. 基本类型的Class对象24Class<?> intClass = int.class;2526// 6. 基本类型包装类的TYPE字段27Class<?> integerClass = Integer.TYPE; // 等同于int.class3. 反射的主要应用场景
3.1 获取类的结构信息
1// 获取类的基本信息2Class<?> clazz = MyClass.class;3String className = clazz.getName(); // 获取完全限定名4String simpleName = clazz.getSimpleName(); // 获取简单类名5Package pkg = clazz.getPackage(); // 获取包信息6Class<?> superClass = clazz.getSuperclass(); // 获取父类7Class<?>[] interfaces = clazz.getInterfaces(); // 获取实现的接口8int modifiers = clazz.getModifiers(); // 获取修饰符9boolean isPublic = Modifier.isPublic(modifiers); // 检查修饰符1011// 获取字段信息12Field[] fields = clazz.getDeclaredFields(); // 获取所有声明的字段13Field[] publicFields = clazz.getFields(); // 获取所有公共字段(包括继承的)1415for (Field field : fields) {16 String fieldName = field.getName(); // 字段名17 Class<?> fieldType = field.getType(); // 字段类型18 int fieldModifiers = field.getModifiers(); // 字段修饰符19}2021// 获取方法信息22Method[] methods = clazz.getDeclaredMethods(); // 获取所有声明的方法23Method[] publicMethods = clazz.getMethods(); // 获取所有公共方法(包括继承的)2425for (Method method : methods) {26 String methodName = method.getName(); // 方法名27 Class<?> returnType = method.getReturnType(); // 返回类型28 Class<?>[] paramTypes = method.getParameterTypes(); // 参数类型29 Class<?>[] exceptionTypes = method.getExceptionTypes(); // 异常类型30}3132// 获取构造方法信息33Constructor<?>[] constructors = clazz.getDeclaredConstructors(); // 获取所有声明的构造方法34Constructor<?>[] publicConstructors = clazz.getConstructors(); // 获取所有公共构造方法3.2 创建类的实例
1// 使用默认构造方法创建实例2Class<?> clazz = MyClass.class;3try {4 Object obj = clazz.newInstance(); // 已过时,Java 9开始不推荐使用5} catch (InstantiationException | IllegalAccessException e) {6 e.printStackTrace();7}89// 使用Constructor创建实例(推荐)10try {11 // 获取默认构造方法12 Constructor<?> constructor = clazz.getDeclaredConstructor();13 Object obj = constructor.newInstance();14 15 // 获取带参数的构造方法16 Constructor<?> paramConstructor = clazz.getDeclaredConstructor(String.class, int.class);17 Object paramObj = paramConstructor.newInstance("name", 25);18} catch (Exception e) {19 e.printStackTrace();20}3.3 访问和修改字段
1// 访问公共字段2Class<?> clazz = MyClass.class;3Object obj = clazz.getDeclaredConstructor().newInstance();4try {5 Field field = clazz.getField("publicField"); // 只能获取公共字段6 Object value = field.get(obj); // 获取字段值7 field.set(obj, "new value"); // 设置字段值8} catch (Exception e) {9 e.printStackTrace();10}1112// 访问私有字段13try {14 Field privateField = clazz.getDeclaredField("privateField");15 privateField.setAccessible(true); // 设置访问性,绕过访问检查16 Object value = privateField.get(obj); // 获取私有字段值17 privateField.set(obj, "new private value"); // 修改私有字段值18} catch (Exception e) {19 e.printStackTrace();20}2122// 访问静态字段23try {24 Field staticField = clazz.getDeclaredField("CONSTANT");25 staticField.setAccessible(true);26 Object value = staticField.get(null); // 静态字段不需要实例,传null27} catch (Exception e) {28 e.printStackTrace();29}3.4 调用方法
1// 调用公共方法2Class<?> clazz = MyClass.class;3Object obj = clazz.getDeclaredConstructor().newInstance();4try {5 // 无参方法6 Method method = clazz.getMethod("publicMethod");7 Object result = method.invoke(obj);8 9 // 有参方法10 Method paramMethod = clazz.getMethod("publicParamMethod", String.class, int.class);11 Object paramResult = paramMethod.invoke(obj, "hello", 42);12} catch (Exception e) {13 e.printStackTrace();14}1516// 调用私有方法17try {18 Method privateMethod = clazz.getDeclaredMethod("privateMethod");19 privateMethod.setAccessible(true); // 绕过访问检查20 Object result = privateMethod.invoke(obj);21} catch (Exception e) {22 e.printStackTrace();23}2425// 调用静态方法26try {27 Method staticMethod = clazz.getDeclaredMethod("staticMethod");28 staticMethod.setAccessible(true);29 Object result = staticMethod.invoke(null); // 静态方法不需要实例,传null30} catch (Exception e) {31 e.printStackTrace();32}3.5 访问注解
1// 获取类上的注解2Class<?> clazz = MyClass.class;3if (clazz.isAnnotationPresent(MyAnnotation.class)) {4 MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);5 String value = annotation.value();6}78// 获取方法上的注解9Method method = clazz.getDeclaredMethod("myMethod");10if (method.isAnnotationPresent(MyMethodAnnotation.class)) {11 MyMethodAnnotation annotation = method.getAnnotation(MyMethodAnnotation.class);12}1314// 获取字段上的注解15Field field = clazz.getDeclaredField("myField");16if (field.isAnnotationPresent(MyFieldAnnotation.class)) {17 MyFieldAnnotation annotation = field.getAnnotation(MyFieldAnnotation.class);18}1920// 获取参数上的注解(Java 8+)21Method method2 = clazz.getDeclaredMethod("myMethod", String.class);22Parameter[] parameters = method2.getParameters();23for (Parameter parameter : parameters) {24 if (parameter.isAnnotationPresent(MyParamAnnotation.class)) {25 MyParamAnnotation annotation = parameter.getAnnotation(MyParamAnnotation.class);26 String value = annotation.value();27 }28}2930// 获取所有注解31Annotation[] annotations = clazz.getAnnotations(); // 包括继承的注解32Annotation[] declaredAnnotations = clazz.getDeclaredAnnotations(); // 仅直接声明的注解3.6 数组操作
1// 创建数组2int[] intArray = (int[]) Array.newInstance(int.class, 5);34// 设置数组元素5Array.setInt(intArray, 0, 10);6Array.setInt(intArray, 1, 20);78// 获取数组元素9int value = Array.getInt(intArray, 0);1011// 创建多维数组12int[][] matrix = (int[][]) Array.newInstance(int.class, 3, 3);1314// 获取数组类型信息15Class<?> clazz = intArray.getClass();16boolean isArray = clazz.isArray();17Class<?> componentType = clazz.getComponentType();4. 反射的实际应用
4.1 框架和库中的应用
反射在许多Java框架和库中得到广泛应用:
-
Spring: 依赖注入、AOP等核心功能
- 通过反射创建bean
- 自动装配依赖
- 使用注解配置
-
Hibernate/JPA: ORM映射
- 将实体类映射到数据库表
- 通过注解或XML配置访问字段
-
Jackson/Gson: JSON序列化和反序列化
- 将JSON字符串转换为Java对象
- 将Java对象转换为JSON字符串
-
JUnit: 测试框架
- 使用注解标记测试方法
- 自动运行测试案例
-
Android: 视图绑定等
- 利用注解绑定视图和控件
4.2 插件和扩展系统
许多应用程序使用反射来实现插件或扩展系统:
1// 动态加载和实例化插件2public Plugin loadPlugin(String className) throws Exception {3 Class<?> pluginClass = Class.forName(className);4 if (Plugin.class.isAssignableFrom(pluginClass)) {5 return (Plugin) pluginClass.getDeclaredConstructor().newInstance();6 }7 throw new IllegalArgumentException("Not a valid plugin: " + className);8}910// 扫描包中的所有插件11public List<Plugin> scanPlugins(String packageName) {12 // 获取包下所有类13 List<Class<?>> classes = getClassesInPackage(packageName);14 List<Plugin> plugins = new ArrayList<>();15 16 for (Class<?> clazz : classes) {17 // 检查是否是插件类型且有@PluginAnnotation18 if (Plugin.class.isAssignableFrom(clazz) && 19 clazz.isAnnotationPresent(PluginAnnotation.class)) {20 try {21 Plugin plugin = (Plugin) clazz.getDeclaredConstructor().newInstance();22 plugins.add(plugin);23 } catch (Exception e) {24 // 处理异常25 }26 }27 }28 29 return plugins;30}4.3 动态代理
反射可以用来创建动态代理,这是很多框架(如Spring AOP)的基础:
1// 定义接口2interface UserService {3 void addUser(String username);4 String getUser(int id);5}67// 定义InvocationHandler8class LoggingHandler implements InvocationHandler {9 private final Object target;10 11 public LoggingHandler(Object target) {12 this.target = target;13 }14 15 @Override16 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {17 System.out.println("Before method: " + method.getName());18 try {19 Object result = method.invoke(target, args);20 System.out.println("After method: " + method.getName());21 return result;22 } catch (Exception e) {23 System.out.println("Exception in method: " + method.getName());24 throw e;25 }26 }27}2829// 创建代理30UserService userService = new UserServiceImpl();31UserService proxy = (UserService) Proxy.newProxyInstance(32 UserService.class.getClassLoader(),33 new Class<?>[] { UserService.class },34 new LoggingHandler(userService)35);3637// 调用代理方法38proxy.addUser("user1"); // 会触发日志记录4.4 依赖注入容器
简单的依赖注入容器实现:
1public class DIContainer {2 private Map<Class<?>, Object> instances = new HashMap<>();3 4 public <T> void register(Class<T> clazz) throws Exception {5 Constructor<T> constructor = clazz.getDeclaredConstructor();6 T instance = constructor.newInstance();7 8 // 查找@Inject注解的字段9 for (Field field : clazz.getDeclaredFields()) {10 if (field.isAnnotationPresent(Inject.class)) {11 Class<?> fieldType = field.getType();12 field.setAccessible(true);13 14 // 递归创建依赖15 if (!instances.containsKey(fieldType)) {16 register(fieldType);17 }18 19 field.set(instance, instances.get(fieldType));20 }21 }22 23 instances.put(clazz, instance);24 }25 26 @SuppressWarnings("unchecked")27 public <T> T get(Class<T> clazz) {28 return (T) instances.get(clazz);29 }30}3132// 使用依赖注入容器33DIContainer container = new DIContainer();34container.register(UserService.class);35UserService service = container.get(UserService.class);5. 反射的性能考虑
反射虽然强大,但也有性能开销:
- 访问检查:特别是访问私有成员时需要绕过访问检查
- 类型安全检查:运行时进行而非编译时
- 缺少编译时优化:无法进行内联等优化
性能优化策略:
- 缓存反射对象:重复使用Class、Method、Field等对象
- 减少反射调用:仅在必要时使用反射
- 使用setAccessible(true):减少访问检查的开销
- 使用方法句柄(MethodHandle):Java 7引入的更高效替代方案
1// 缓存反射对象示例2public class ReflectionCache {3 private static final Map<String, Method> methodCache = new ConcurrentHashMap<>();4 5 public static Method getMethod(Class clazz, String methodName, Class... paramTypes) 6 throws NoSuchMethodException {7 String key = clazz.getName() + "#" + methodName + Arrays.toString(paramTypes);8 return methodCache.computeIfAbsent(key, k -> {9 try {10 return clazz.getDeclaredMethod(methodName, paramTypes);11 } catch (NoSuchMethodException e) {12 throw new RuntimeException(e);13 }14 });15 }16}6. 反射的安全性和限制
反射打破了Java的封装性,可能绕过访问控制。为了安全考虑:
- SecurityManager:可以控制反射操作
- 模块系统(Java 9+):可以限制反射访问
- 运行时权限:可以配置安全策略
1// 设置安全管理器2System.setSecurityManager(new SecurityManager());34// 安全策略示例(在策略文件中)5// permission java.lang.reflect.ReflectPermission "suppressAccessChecks";注意事项:
- 反射不能访问无法通过正常代码访问的成员(如父类的private字段)
- 某些JVM优化可能不适用于使用反射的代码
- 对于模块化应用(Java 9+),需要考虑
opens和exports声明
7. 方法句柄(MethodHandle)
Java 7引入的方法句柄提供了一种比反射更高效的动态方法调用机制:
1import java.lang.invoke.MethodHandle;2import java.lang.invoke.MethodHandles;3import java.lang.invoke.MethodType;45public class MethodHandleExample {6 public static void main(String[] args) throws Throwable {7 // 获取MethodHandles.Lookup对象8 MethodHandles.Lookup lookup = MethodHandles.lookup();9 10 // 查找方法11 MethodType methodType = MethodType.methodType(String.class, int.class);12 MethodHandle handle = lookup.findVirtual(String.class, "substring", methodType);13 14 // 调用方法15 String result = (String) handle.invokeExact("Hello, World", 0);16 17 // 绑定接收者18 MethodHandle boundHandle = handle.bindTo("Hello, World");19 String result2 = (String) boundHandle.invokeExact(0);20 }21}与反射的比较:
- 方法句柄性能通常更好
- 方法句柄类型安全性更强
- 反射更灵活,可以获取更多元信息
- 反射更容易使用和理解
8. 反射与注解处理
反射常用于运行时处理注解:
1// 自定义注解2@Retention(RetentionPolicy.RUNTIME)3@Target(ElementType.METHOD)4public @interface Transactional {5 boolean readOnly() default false;6}78// 使用反射处理注解9public class TransactionAspect {10 public Object invoke(Object target, Method method, Object[] args) throws Throwable {11 if (method.isAnnotationPresent(Transactional.class)) {12 Transactional tx = method.getAnnotation(Transactional.class);13 boolean readOnly = tx.readOnly();14 15 // 开始事务16 startTransaction(readOnly);17 18 try {19 Object result = method.invoke(target, args);20 commitTransaction();21 return result;22 } catch (Exception e) {23 rollbackTransaction();24 throw e;25 }26 } else {27 // 无事务注解,直接调用28 return method.invoke(target, args);29 }30 }31 32 // 事务操作实现...33}9. 反射的最佳实践
- 谨慎使用:仅在必要时使用反射,不要仅为方便而使用
- 异常处理:反射API会抛出多种异常,需要妥善处理
- 性能优化:缓存反射对象,避免重复获取
- 访问控制:使用
setAccessible(true)时考虑安全影响 - 类型安全:尽量使用泛型提高类型安全性
- 可读性:使用有意义的命名和注释
- 测试覆盖:反射代码需要更多测试覆盖
- 考虑替代方案:如工厂模式、依赖注入框架等
10. 反射的实际案例分析
案例:自定义ORM框架
实现一个简单的ORM映射,通过反射将Java对象映射到数据库表:
1// 表映射注解2@Retention(RetentionPolicy.RUNTIME)3@Target(ElementType.TYPE)4public @interface Table {5 String name();6}78// 列映射注解9@Retention(RetentionPolicy.RUNTIME)10@Target(ElementType.FIELD)11public @interface Column {12 String name();13 boolean primary() default false;14}1516// 实体类示例17@Table(name = "users")18public class User {19 @Column(name = "id", primary = true)20 private Long id;21 22 @Column(name = "username")23 private String username;24 25 @Column(name = "email")26 private String email;27 28 // 构造方法、getter和setter...29}3031// ORM工具类32public class OrmUtil {33 // 生成插入SQL34 public static String generateInsertSQL(Object entity) {35 Class<?> clazz = entity.getClass();36 if (!clazz.isAnnotationPresent(Table.class)) {37 throw new IllegalArgumentException("Class not annotated with @Table");38 }39 40 Table table = clazz.getAnnotation(Table.class);41 String tableName = table.name();42 43 List<String> columns = new ArrayList<>();44 List<String> values = new ArrayList<>();45 46 try {47 for (Field field : clazz.getDeclaredFields()) {48 if (field.isAnnotationPresent(Column.class)) {49 field.setAccessible(true);50 Column column = field.getAnnotation(Column.class);51 52 // 跳过自增主键53 if (column.primary() && field.get(entity) == null) {54 continue;55 }56 57 columns.add(column.name());58 59 Object value = field.get(entity);60 if (value instanceof String) {61 values.add("'" + value + "'");62 } else {63 values.add(String.valueOf(value));64 }65 }66 }67 } catch (Exception e) {68 throw new RuntimeException("Error generating SQL", e);69 }70 71 return String.format("INSERT INTO %s (%s) VALUES (%s)",72 tableName,73 String.join(", ", columns),74 String.join(", ", values));75 }76 77 // 生成查询SQL并映射结果78 public static <T> T findById(Class<T> clazz, Object id, Connection conn) {79 if (!clazz.isAnnotationPresent(Table.class)) {80 throw new IllegalArgumentException("Class not annotated with @Table");81 }82 83 Table table = clazz.getAnnotation(Table.class);84 String tableName = table.name();85 String primaryKeyColumn = "";86 87 // 查找主键列88 for (Field field : clazz.getDeclaredFields()) {89 if (field.isAnnotationPresent(Column.class)) {90 Column column = field.getAnnotation(Column.class);91 if (column.primary()) {92 primaryKeyColumn = column.name();93 break;94 }95 }96 }97 98 if (primaryKeyColumn.isEmpty()) {99 throw new IllegalArgumentException("No primary key found");100 }101 102 String sql = String.format("SELECT * FROM %s WHERE %s = ?", 103 tableName, primaryKeyColumn);104 105 try (PreparedStatement stmt = conn.prepareStatement(sql)) {106 stmt.setObject(1, id);107 108 try (ResultSet rs = stmt.executeQuery()) {109 if (rs.next()) {110 return mapResultSetToObject(rs, clazz);111 }112 }113 } catch (Exception e) {114 throw new RuntimeException("Error executing query", e);115 }116 117 return null;118 }119 120 // 将结果集映射到对象121 private static <T> T mapResultSetToObject(ResultSet rs, Class<T> clazz) 122 throws Exception {123 T instance = clazz.getDeclaredConstructor().newInstance();124 125 for (Field field : clazz.getDeclaredFields()) {126 if (field.isAnnotationPresent(Column.class)) {127 field.setAccessible(true);128 Column column = field.getAnnotation(Column.class);129 String columnName = column.name();130 131 Object value = rs.getObject(columnName);132 133 if (value != null) {134 // 处理类型转换135 if (field.getType() == int.class || field.getType() == Integer.class) {136 field.set(instance, rs.getInt(columnName));137 } else if (field.getType() == long.class || field.getType() == Long.class) {138 field.set(instance, rs.getLong(columnName));139 } else if (field.getType() == double.class || field.getType() == Double.class) {140 field.set(instance, rs.getDouble(columnName));141 } else if (field.getType() == boolean.class || field.getType() == Boolean.class) {142 field.set(instance, rs.getBoolean(columnName));143 } else if (field.getType() == Date.class) {144 field.set(instance, rs.getDate(columnName));145 } else {146 field.set(instance, value);147 }148 }149 }150 }151 152 return instance;153 }154}
评论区 / Comments