java教程笔记(九)-异常处理,枚举类,反射机制,注解
1.java的异常处理
1.什么是异常(Exception)?
异常指的并不是语法错误和逻辑错误。语法错了,编译不通过,不会产生字节码文件,根本不能运行。
代码逻辑错误,只是没有得到想要的结果,例如:求a与b的和,你写成了a-b
在 Java 中,异常是指程序在运行过程中发生的不正常事件,例如:
- 文件找不到(
FileNotFoundException
) - 数组下标越界(
ArrayIndexOutOfBoundsException
) - 空指针访问(
NullPointerException
) - 类找不到(
ClassNotFoundException
)
Java 提供了内置的异常类体系来表示这些错误,并通过 try-catch-finally
和 throw-throws
来进行处理。
2.异常分类
Java 中的异常体系继承自 Throwable
类,主要分为两大类:
1. Error(错误)
- 表示 JVM 本身的问题,如内存溢出(
OutOfMemoryError
)、栈溢出(StackOverflowError
) - 通常应用程序无法处理,不需要捕获
2. Exception(异常)
1. Checked Exceptions(受检异常):在代码编译阶段,编译器就能明确 警示 当前代码 可能发生(不是一定发生) xx异常,并 明确督促 程序员提前编写处理它的代码。通常,这类异常的发生不是由程序员的代码引起的
- 编译器强制要求你必须处理的异常
- 例如:
IOException
、SQLException
- 必须使用
try-catch
捕获或用throws
声明抛出
2. Unchecked Exceptions(非受检异常 / 运行时异常):在代码编译阶段,编译器完全不做任何检查,无论该异常是否会发生,编译器都不给出任何提示。只有等代码运行起来并确实发生了xx异常,它才能被发现。通常,这类异常是由程序员的代码编写不当引起的
- 继承自
RuntimeException
- 编译器不要求必须处理
- 例如:
NullPointerException
、ArrayIndexOutOfBoundsException
// 示例:运行时异常int[] arr = new int[5];
System.out.println(arr[10]); // 抛出 ArrayIndexOutOfBoundsException
3.异常处理语法结构
程序在执行的过程当中,一旦出现异常,就会在出现异常的代码处,生成对应的异常类对象,并将此对象抛出。
一旦抛出此程序不执行其后面的代码。
针对过程1中抛出的对象,进行捕获处理。此捕获处理的过程,就成为抓
一旦将异常进行处理,代码就可以急促执行。
1. try-catch-finally
-
try 块:
- 包含可能会引发异常的代码。
- 如果异常发生,则会跳转到对应的
catch
块。
-
catch 块:
- 捕获特定类型的异常(如
IOException
,NullPointerException
等)。 - 支持多个
catch
块来处理不同类型的异常。
- 捕获特定类型的异常(如
-
finally 块:
- 不论是否发生异常,都会执行该块中的代码。
- 常用于关闭文件流、数据库连接等资源释放操作。
若 catch
块未抛出新异常:
异常被捕获后,try
块中异常行之后的代码不执行,但 catch
块后的代码会继续执行。
try {
// 可能发生异常的代码}catch (异常类型1 e) {
// 处理异常
}
catch (异常类型2 e) {
e.printStackTrace//打印异常的详细信息;(推荐)
// 多个 catch 分别处理不同异常
}
catch(IOException | SQLException ex){//同时捕获多个异常
}
finally {
// 不管是否发生异常都会执行(可选)}
/*
针对于try中抛出的异常类的对象,使用之后的catch语句进行匹配,一旦匹配上,就进入catch语句块进行处理。
3、一旦处理结束,代码就可以继续向下执行。
4、如果声明了多个catch结构,不同的异常类型在子父关系的情况下,谁声明在上面,谁声明在下面都可以。如果多个异常类型满足子父类的关系,必须将子类声明在父类结构的上面。否则报错。
示例:
try {int result = 10 / 0;}
catch (ArithmeticException e) {
System.out.println("除数不能为0");
}
finally {
System.out.println("finally 总会执行");
}
2. throw:手动抛出异常
当程序运行过程中遇到错误或不符合预期的情况时,可以使用 throw
抛出自定义异常或者系统异常。
throw
后面必须是一个 Throwable
或其子类的实例(如 Exception、RuntimeException
等)。
抛出异常后,程序会停止当前代码块的执行,并将异常传递给调用栈。
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数");}
3. throws:声明方法可能抛出的异常
用于声明方法可能会抛出的异常类型。
如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉或并不能确定如何处理这种异常,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理,否则编译不通过。
在方法签名中声明该方法可能抛出的异常,让调用者知道需要处理这些异常,强制调用者处理。
public void readFile() throws IOException {
FileReader reader = new FileReader("file.txt");}
4.try-with-resources
try-with-resources
是 Java 7 引入的一项语法特性,用于自动管理资源的关闭,避免因忘记手动关闭资源而导致资源泄漏(如文件流、网络连接、数据库连接等未关闭)。
try (声明或初始化资源) {// 使用资源的代码
} catch (异常类型 e) {// 异常处理
} finally {// 可选:最终执行的代码块
}
资源必须实现 AutoCloseable
接口(或其子接口 Closeable
),该接口定义了 close()
方法。
import java.io.*;public class TryWithResourcesExample {public static void main(String[] args) {try (FileInputStream fis = new FileInputStream("example.txt")) {int data;while ((data = fis.read()) != -1) {System.out.print((char) data);}} catch (IOException e) {e.printStackTrace();}}
}
/*
FileInputStream 实现了 AutoCloseable。
即使在读取过程中抛出异常,fis 也会被自动关闭。
*/
特点与优势
特性 | 描述 |
---|---|
自动关闭资源 | 在 try 块结束后,无论是否发生异常,都会自动调用 close() 方法。 |
多资源支持 | 可以在 try() 中声明多个资源,用分号 ; 分隔。 |
简化代码 | 避免冗长的 finally 块手动关闭资源逻辑。 |
增强可读性 | 资源声明集中清晰,便于维护。 |
注意事项
-
资源必须实现 AutoCloseable 或 Closeable
AutoCloseable
是 Java 7 的标准接口。Closeable
是AutoCloseable
的子接口,通常用于 I/O 类库。
-
资源按声明逆序关闭
- 最后一个声明的资源最先被关闭。
-
catch/finally 中可以访问资源吗?
- 不行!资源的作用域仅限于
try
块内。
- 不行!资源的作用域仅限于
-
异常抑制(Suppressed Exceptions)
- 如果
try
块和close()
方法都抛出异常,try
块中的异常为主异常,close()
中的异常会被“抑制”,但可以通过Throwable.getSuppressed()
获取。
- 如果
与传统 try-catch-finally 对比
传统方式(Java 7 之前)
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt"); // 使用 fis
}
catch (IOException e) {
e.printStackTrace();
}
finally {
if (fis != null) {
try {
fis.close();
}
catch (IOException e) {
e.printStackTrace();}}}
使用 try-with-resources 后(更简洁)
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 使用 fis
}
catch (IOException e) {
e.printStackTrace();
}
5. 高级技巧:异常链(Exception Chaining)
当你抛出一个新的异常但希望保留原始异常的信息时,可以使用异常链:
try {
// 可能抛出 IOException 的操作}
catch (IOException e) {throw new CustomException("读取文件失败", e); // 将原始异常作为 cause
}
这样可以通过 e.getCause()
获取原始异常信息,有助于排查问题。
4.自定义异常
你可以创建自己的异常类来表示特定业务逻辑中的错误。
1. 为什么需要自定义异常?
- 增强语义:标准异常类如
IllegalArgumentException
或IOException
可能无法准确描述你的业务逻辑错误。 - 统一处理:可以在项目中定义一套统一的异常体系,便于集中捕获和处理。
- 分离业务逻辑与异常处理:将异常处理从主流程中解耦,提升代码结构
2. 自定义异常的基本步骤
步骤 1:继承合适的异常类
Java 中所有异常都继承自 Throwable
类。通常我们选择以下两个基类之一:
- Exception:受检异常(Checked Exception),调用者必须处理或声明。
RuntimeException
:非受检异常(Unchecked Exception),不需要强制处理。
// 示例:一个受检异常
public class InvalidUserInputException extends Exception {public InvalidUserInputException(String message) {super(message);}
}// 示例:一个非受检异常
public class DataNotFoundException extends RuntimeException {public DataNotFoundException(String message) {super(message);}
}
步骤 2:抛出自定义异常
在业务逻辑中使用 throw
抛出自定义异常:
public void validateAge(int age) throws InvalidUserInputException {if (age < 0 || age > 150) {throw new InvalidUserInputException("年龄必须在 0 到 150 之间");}
}
class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
public class TestCustomException {
static void validateAge(int age) throws InvalidAgeException {if (age < 0) {throw new InvalidAgeException("年龄不能为负数");
}
}
public static void main(String[] args) {
try {
validateAge(-5);}
catch (InvalidAgeException e) {
System.out.println("捕获到异常:" + e.getMessage());
}
}}
步骤 3:捕获并处理自定义异常
public static void main(String[] args) {try {validateAge(-10);} catch (InvalidUserInputException e) {System.out.println("捕获到自定义异常: " + e.getMessage());}
}
3. 自定义异常的最佳实践
实践建议 | 描述 |
---|---|
异常命名 | 使用有意义的名称,以 Exception 结尾,如 InvalidTokenException 、ResourceNotFoundException 。 |
构造方法 | 提供多个构造方法,支持传入消息、异常原因等。 |
错误码 | 可为异常添加错误码字段,方便日志记录和前端识别。 |
日志记录 | 在抛出异常前打印日志,便于调试。 |
不滥用异常 | 异常应表示真正的错误状态,而不是控制流程的手段 |
4. 完整示例
定义自定义异常类
// 受检异常
public class AccountLockedException extends Exception {private final String errorCode;public AccountLockedException(String message, String errorCode) {super(message);this.errorCode = errorCode;}public String getErrorCode() {return errorCode;}
}
使用自定义异常
public class UserService {public void login(String username, String password) throws AccountLockedException {if ("locked_user".equals(username)) {throw new AccountLockedException("账户已被锁定", "ACCOUNT_LOCKED");}// 登录逻辑...}
}
捕获和处理
public class Main {
public static void main(String[] args) {UserService service = new UserService();
try {
service.login("locked_user", "123456");
}
catch (AccountLockedException e) {
System.err.println("错误码:" + e.getErrorCode() + ", 消息:" + e.getMessage());
}
}
}
5.异常处理最佳实践
实践 | 建议 |
---|---|
避免空 catch 块 | 不要写 catch (Exception e) {} ,这会导致错误被忽略 |
异常信息要清晰 | 使用有意义的异常信息帮助调试 |
合理使用 finally | 用于关闭资源(如文件流、数据库连接) |
尽量捕获具体异常 | 不要用 catch (Exception e) 捕获所有异常 |
不要滥用异常控制流程 | 异常不应该作为正常业务流程的一部分 |
包装并重新抛出异常 | 在适当的地方包装原始异常后重新抛出 |
6.常见异常类型
异常类名 | 描述 |
---|---|
NullPointerException | 访问一个 null 对象的方法或属性 |
ArrayIndexOutOfBoundsException | 数组索引超出范围 |
ClassCastException | 类型转换失败 |
IllegalArgumentException | 方法接收非法参数 |
IOException | 输入输出操作失败 |
SQLException | 数据库操作失败 |
ClassNotFoundException | 找不到指定的类 |
NoSuchMethodException | 找不到指定的方法 |
InterruptedException | 线程被中断 |
7.总结
要点 | 内容 |
---|---|
异常机制 | Java 提供了强大的异常处理机制,使程序更健壮 |
异常分类 | Error(错误)、Exception(异常),其中 Exception 又分为 checked 和 unchecked |
关键字 | try , catch , finally , throw , throws |
自定义异常 | 可以继承 Exception 或 RuntimeException 创建自己的异常类 |
最佳实践 | 捕获具体异常、避免空 catch、合理使用 finally、不要滥用异常控制流程 |
如果你看到类似代码:
java
try { // some code } catch (IOException e) { e.printStackTrace(); }
说明你正在使用标准的异常处理方式,对可能发生的 IO 错误进行捕获和处理。
2.枚举类
枚举类(enum
)是 Java 中一种特殊的类,用于定义一组常量。每个常量代表一个具体的值,并且可以包含字段、方法以及构造函数。
1.定义枚举类
用 Class 实现一个简单的枚举类
public final class Color {// 定义常量实例(静态最终字段)public static final Color RED = new Color("RED");public static final Color GREEN = new Color("GREEN");public static final Color BLUE = new Color("BLUE");// 存储所有枚举值private static final Color[] VALUES = {RED, GREEN, BLUE};// 枚举名称private final String name;// 私有构造器,防止外部创建新实例private Color(String name) {this.name = name;}// 获取名称public String name() {return name;}// 获取所有枚举值public static Color[] values() {return VALUES;}// 根据名称获取枚举实例public static Color valueOf(String name) {for (Color color : VALUES) {if (color.name.equals(name)) {return color;}}throw new IllegalArgumentException("No enum constant with name: " + name);}// 重写 toString 方法@Overridepublic String toString() {return "Color." + name;}
}
public class Main {public static void main(String[] args) {Color c1 = Color.RED;Color c2 = Color.valueOf("GREEN");System.out.println(c1); // 输出:Color.REDSystem.out.println(c2); // 输出:Color.GREENfor (Color color : Color.values()) {System.out.println(color.name());}if (c1 == Color.RED) {System.out.println("是红色");}}
}
枚举类通过enum
关键字定义
//基本结构 默认调用无参构造函数,常量前面省略了public static final
public enum Day {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}
//添加属性和构造方法,你可以为枚举添加字段,并通过构造方法进行初始化
public enum Color {RED("红色"),GREEN("绿色"),BLUE("蓝色");private String description;Color(String description) {this.description = description;}public String getDescription() {return description;}
}
//添加方法 你可以在枚举中添加任意的方法,比如静态方法用于查找对应的枚举值
public enum Color {RED("红色"),GREEN("绿色"),BLUE("蓝色");private String description;Color(String description) {this.description = description;}public String getDescription() {return description;}// 根据描述获取对应的枚举值public static Color fromDescription(String desc) {for (Color color : values()) {if (color.getDescription().equals(desc)) {return color;}}throw new IllegalArgumentException("No such color: " + desc);}
}
//实现接口 枚举类可以实现接口,每个枚举实例都可以有不同的行为:
//匿名内部类
public interface Behavior {void perform();
}public enum Operation implements Behavior {ADD {@Overridepublic void perform() {System.out.println("执行加法操作");}},SUBTRACT {@Overridepublic void perform() {System.out.println("执行减法操作");}};
}Operation.ADD.perform(); // 输出:执行加法操作
//抽象方法 你也可以在枚举中定义抽象方法,然后在每个枚举常量中实现它:
//匿名内部类
public enum Shape {CIRCLE {@Overridepublic double area(double... params) {return Math.PI * params[0] * params[0];}},SQUARE {@Overridepublic double area(double... params) {return params[0] * params[0];}};public abstract double area(double... params);
}
System.out.println(Shape.CIRCLE.area(5)); // 输出圆面积
//使用 switch 判断枚举值Color color = Color.RED;switch (color) {case RED:System.out.println("红色");break;case GREEN:System.out.println("绿色");break;case BLUE:System.out.println("蓝色");break;
}
2. 枚举类的特性
- 继承
java.lang.Enum
:所有枚举类都隐式继承自Enum
类。 - 不可被继承:Java 的枚举类不能被其他类继承,但可以实现接口。
-
枚举常量是
public static final
的 -
枚举构造器必须是
private
或默认包访问权限 -
枚举值必须放在最前面,后面可加方法或字段
- 不需要提供set方法:因为枚举对象的值为只读
-
使用
==
比较枚举值更高效:不要使用.equals()
,直接使用==
比较即可,因为枚举是单例的。 - 枚举对象名通常使用大写
- 线程安全:枚举类天然支持线程安全。
- 序列化安全:枚举类默认保证单例,避免反序列化破坏单例模式。
- 可定义属性和方法:
package com.enum_;public class Enumeration02 {public static void main(String[] args) {System.out.println(Season.SPRING);System.out.println(Season.SUMMER);System.out.println(Season.AUTUMN);System.out.println(Season.WINTER);} }enum Season{SPRING("春天","温暖"),SUMMER("夏天","炎热"),AUTUMN("秋天","凉爽"),WINTER("冬天","寒冷");private String name;private String describe;private Season(String name, String describe) {this.name = name;this.describe = describe;}public String getName() {return name;}public String getDescribe() {return describe;}@Overridepublic String toString() {return "Season2{" +"name='" + name + '\'' +", describe='" + describe + '\'' +'}';} }构造器只能用private修饰;枚举类所有实例必须在枚举类中显式列出(, 分隔 ; 结尾)。 列出实例系统会自动添加 public static final 修饰;必须在枚举类的第一行声明枚举类对象 JDK 1.5可在switch表达式中使用Enum定义的枚举类的对象作为表达式, case 子句可以直接使用枚举值的名字, 无需添加枚举类作为限定。*/
switch (season) {case SPRING: ... break;// ...其他case }
3. 常用方法
values()
:返回枚举数组。valueOf(String name)
:根据名称获取枚举实例。name()
:获取枚举常量的名称。ordinal()
:获取枚举常量的位置索引,默认从0开始。
Season summer = Season.SUMMER;
//toString():返回枚举类对象的名称
System.out.println(summer.toString());// System.out.println(Season1.class.getSuperclass());
System.out.println("****************");
//values():返回所的枚举类对象构成的数组
Season[] values = Season.values();
for(int i = 0;i < values.length;i++){System.out.println(values[i]);
}
System.out.println("****************");
Thread.State[] values1 = Thread.State.values();
for (int i = 0; i < values1.length; i++) {System.out.println(values1[i]);
}//valueOf(String objName):返回枚举类中对象名是objName的对象。
Season winter = Season.valueOf("WINTER");
//如果没objName的枚举类对象,则抛异常:IllegalArgumentException
// Season winter = Season.valueOf("WINTER1");
System.out.println(winter);
4. 使用场景
- 表示状态(如订单状态:
PENDING
,PAID
,SHIPPED
,DELIVERED
)。 - 表示固定选项(如性别:
MALE
,FEMALE
)。 - 作为策略模式的一部分(通过枚举实现接口,定义不同的行为)。
示例代码:
public enum OrderStatus {PENDING("待支付"),PAID("已支付"),SHIPPED("已发货"),DELIVERED("已送达");private String description;OrderStatus(String description) {this.description = description;}public String getDescription() {return description;}// 根据描述查找对应的枚举public static OrderStatus fromDescription(String description) {for (OrderStatus status : values()) {if (status.getDescription().equals(description)) {return status;}}throw new IllegalArgumentException("Invalid description: " + description);}
}// 使用示例
public class Main {public static void main(String[] args) {OrderStatus status = OrderStatus.PAID;System.out.println(status.getDescription()); // 输出:已支付OrderStatus found = OrderStatus.fromDescription("已发货");System.out.println(found); // 输出:SHIPPED}
}
3.java的反射
Java反射允许程序在运行时分析类、接口、方法和字段的结构,并能动态调用对象的方法或修改属性。例如,通过反射可以获取Person
类的所有方法并调用eat()
方法,即使该方法在编译时未被显式引用。
作用:
- 框架开发:如Spring通过反射实现依赖注入和AOP(动态代理)。
- 动态扩展:根据配置文件(如XML)动态加载类并实例化对象(如Struts框架的Action配置)。
- 调试与测试:JUnit通过反射调用带有
@Test
注解的方法 。 - 在日常的第三方应用开发过程中,经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法 。
1.反射和正常引入类的区别
在Java中,可以通过两种方式使用类:正常引入(静态加载) 和 反射(动态加载)。它们在编译时行为、性能、灵活性和安全性等方面存在显著差异。
特性 | 正常引入类(静态加载) | 反射(动态加载) |
---|---|---|
引入方式 | 在代码中通过import 引入类 | 在运行时通过Class.forName() 等方法动态获取类 |
编译时是否必须存在 | 是 | 否 |
是否需要硬编码类名 | 是 | 否(可以传字符串) |
//正常引入
// 静态导入
import java.util.ArrayList;public class Main {public static void main(String[] args) {ArrayList<String> list = new ArrayList<>();}
}
//反射引入
public class Main {public static void main(String[] args) throws Exception {// 动态加载类Class<?> clazz = Class.forName("java.util.ArrayList");Object list = clazz.getDeclaredConstructor().newInstance();}
}
对比维度 | 正常引入类 | 反射 |
---|---|---|
编译依赖 | 必须存在该类(否则编译失败) | 不需要在编译时存在(运行时决定) |
调用方式 | 直接调用构造器、方法 | 通过Method.invoke() 等方式间接调用 |
性能 | 快速,无额外开销 | 较慢,涉及安全检查、JNI调用等 |
封装性 | 尊重访问控制(如private不能访问) | 可以绕过访问控制(通过setAccessible(true) ) |
适用场景 | 常规开发、业务逻辑 | 框架开发、插件系统、通用工具等 |
可维护性 | 易于理解和调试 | 代码可读性差,难以维护 |
2.反射的实现原理
1.JVM如何支持反射
反射基于JVM的类加载机制,java虚拟机(JVM)为每个加载的类生成一个唯一的Class<T>
对象,该对象包含了类的所有元数据信息,如类名、父类、接口、构造函数、方法、字段等。这些信息由JVM在类加载阶段解析,并存储在方法区中(在JDK8之后是元空间Metaspace)。
核心组件:
Class<T>
:表示类或接口的类型信息。Method
:表示类的方法信息,包含方法名、参数类型、返回类型等。Field
:表示类的字段信息,包括名称、类型、修饰符等。Constructor<T>
:表示类的构造函数,用于实例化对象。Modifier
:用于判断成员的访问权限(public、private、static等)。
这些类构成了Java反射API的基础。
2. 反射的核心流程
1. 获取Class<T>
对象
Java通过以下几种方式获取类的Class
对象:
// 方式一:通过对象.getClass()
Object obj = new String("hello");
Class<?> clazz = obj.getClass();// 方式二:通过类.class语法
Class<String> clazz = String.class;// 方式三:通过Class.forName()加载类
Class<?> clazz = Class.forName("java.util.ArrayList");
其中,Class.forName()
会触发类的加载和初始化过程。
2. 类加载机制
当调用Class.forName()
或首次使用某个类时,JVM会通过类加载器(ClassLoader)**来查找并加载该类的字节码文件(.class
),然后将其转换为内部结构并存入方法区。
类加载过程包括:
- 加载(Loading):读取字节码,生成
Class
对象。 - 链接(Linking)
- 验证(Verification)
- 准备(Preparation):为静态变量分配内存并设置默认值。
- 解析(Resolution):符号引用转为直接引用。
- 初始化(Initialization):执行静态代码块和静态变量赋值。
这个过程确保了类的完整性和可用性。
3. 反射调用方法/访问字段
一旦获得了Class
对象,就可以进一步获取其Method
、Field
、Constructor
等对象,并进行操作。
例如调用方法:
Method method = clazz.getMethod("methodName", paramTypes); method.invoke(obj, args);
底层实现上,JVM会通过JNI(Java Native Interface)调用本地方法,最终通过C++实现对方法的调用。
关于setAccessible(true)
对于私有成员,Java默认不允许访问。但通过调用setAccessible(true)
可以绕过访问控制检查。这是通过修改Java安全管理策略实现的,可能会带来安全风险。
4. 性能影响
反射操作通常比直接调用慢,原因如下:
- 每次调用
getMethod()
、invoke()
都需要查找方法表。 - 安全检查:每次调用前都会检查访问权限。
- JNI调用开销大。
为了优化性能,可以:
- 缓存
Method
、Field
等对象。 - 使用
MethodHandle
(JDK7引入)或VarHandle
替代部分反射操作。
3.总结
阶段 | 描述 |
---|---|
类加载 | JVM通过类加载器加载类字节码,生成Class 对象 |
元数据访问 | Class 对象提供访问类结构的入口 |
动态调用 | 利用Method.invoke() 、Field.get/set() 等实现动态操作 |
安全控制 | 默认遵循访问控制规则,可通过setAccessible(true) 绕过 |
3.反射的实际用法
1.获取 Class 对象
反射的第一步是获取类的 Class
对象。有三种常见方式:
// 方式1:通过对象.getClass()
String str = "hello";
Class<?> clazz1 = str.getClass();// 方式2:通过类名.class语法
Class<String> clazz2 = String.class;// 方式3:通过 Class.forName("全限定类名")
Class<?> clazz3 = Class.forName("java.util.ArrayList");
2.获取类的信息
通过 Class
对象可以获取类的各种信息:
在 Java 反射(Reflection)机制中,clazz
是一个约定俗成的变量名,用来表示某个类的 Class
对象。
获取类名
System.out.println(clazz.getName()); // 完整类名(带包名)
System.out.println(clazz.getSimpleName()); // 简单类名
获取父类和接口
Class<?> superClass = clazz.getSuperclass(); // 获取父类
Class<?>[] interfaces = clazz.getInterfaces(); // 获取实现的接口
获取构造函数
Constructor<?>[] constructors = clazz.getConstructors(); // 所有 public 构造器
Constructor<?> constructor = clazz.getConstructor(String.class); // 指定参数类型的构造器
访问私有构造函数
同样需要设置为可访问:
getDeclaredConstructor
是 Java 反射 API 中的一个方法,用于获取类中声明的某个构造函数(Constructor),无论其访问权限是什么(public
、private
、protected
或默认包访问权限)。
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) throws NoSuchMethodException
//parameterTypes:构造函数的参数类型数组,用于定位具体构造函数
获取方法
Method[] methods = clazz.getMethods(); // 所有 public 方法(包括继承的)
Method method = clazz.getMethod("methodName", paramTypes); // 获取指定方法
获取字段
Field[] fields = clazz.getFields(); // 所有 public 字段
Field field = clazz.getField("fieldName"); // 获取指定字段
3.创建对象实例
使用默认构造函数创建对象
Object obj = clazz.newInstance(); // JDK8 及之前常用
使用指定构造函数创建对象
Constructor<SomeClass> constructor = clazz.getConstructor(String.class);
Object obj = constructor.newInstance("Hello Reflection");
4.调用方法
obj
是你要操作的类的实例对象,即你要设置字段值的那个对象。
Method method = clazz.getMethod("sayHello", String.class);
Object result = method.invoke(obj, "World"); // 调用方法
调用私有方法
需要设置访问权限为可访问:
Method privateMethod = clazz.getDeclaredMethod("privateMethodName", paramTypes);
privateMethod.setAccessible(true); // 绕过访问控制
privateMethod.invoke(obj, args);
5.访问字段
获取字段值
obj
是你要操作的类的实例对象,即你要设置字段值的那个对象。
getField只能获取 public
字段(包括继承来的)
Field field = clazz.getField("name");
String value = (String) field.get(obj);
设置字段值
field.set(obj, "newValue");
访问私有字段
同样需要设置为可访问:
getDeclaredField
是 Java 反射 API 中的一个方法,用于获取类中声明的指定字段(属性),无论该字段的访问权限是什么(public
、private
、protected
或默认包访问权限)。但不能获取继承来的字段
Field privateField = clazz.getDeclaredField("privateFieldName");
privateField.setAccessible(true);
privateField.set(obj, "newPrivateValue");
6.处理数组类型
反射也支持操作数组:
创建数组
Object array = Array.newInstance(String.class, 5); // 创建长度为5的字符串数组
修改数组元素
Array.set(array, 0, "value");
获取数组元素
String value = (String) Array.get(array, 0);
7.异常处理
反射操作可能抛出以下异常:
ClassNotFoundException
:类不存在IllegalAccessException
:无法访问类或成员InstantiationException
:类是抽象类或接口NoSuchMethodException
:找不到方法InvocationTargetException
:方法执行过程中抛出异常
建议统一捕获:
try {Method method = clazz.getMethod("someMethod");method.invoke(obj);
} catch (Exception e) {e.printStackTrace();
}
public class ReflectionExample {public static void main(String[] args) throws Exception {// 1. 获取 Class 对象Class<?> clazz = Class.forName("com.example.Person");// 2. 创建对象Object person = clazz.getConstructor().newInstance();// 3. 获取并调用方法Method sayHello = clazz.getMethod("sayHello", String.class);sayHello.invoke(person, "Reflection");// 4. 获取并设置字段Field nameField = clazz.getField("name");nameField.set(person, "Alice");System.out.println(nameField.get(person));}
}class Person {public String name;public void sayHello(String msg) {System.out.println("Hello, " + msg);}
}
4.JDK 内置的基本注解类型
Java 提供了一些内置的注解(Annotations),用于为编译器、框架或运行时提供元信息。这些注解可以增强代码可读性、优化编译行为、进行错误检查等。
注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段。
以下是 Java 标准库中常见的 JDK 内置基本注解类型及其用途详解:
1. @Override
用途:
标记一个方法是重写了父类的方法。
使用位置:
只能用于方法上。
示例:
@Override public String toString() {
return "MyClass";
}
注意:
- 如果使用了该注解但没有正确重写父类方法,编译器会报错。
- 提高代码可读性和安全性。
2. @Deprecated
用途:
表示某个类、方法或字段已过时,不推荐使用。
使用位置:
可用于类、方法、字段、构造函数等。
示例:
@Deprecated public void oldMethod() { // 已废弃的方法 }
编译器行为:
- 使用被
@Deprecated
注解标记的元素时,编译器会发出警告。 - 推荐开发者使用替代方法。
3. @SuppressWarnings
用途:
抑制编译器警告。
使用位置:
可用于类、方法、字段、局部变量等。
示例:
@SuppressWarnings("unused")
private int unusedVariable;
常见参数值:
参数值 | 含义 |
---|---|
"unchecked" | 忽略未检查的转换警告(泛型相关) |
"deprecation" | 忽略对废弃方法使用的警告 |
"unused" | 忽略未使用的代码警告 |
"all" | 抑制所有警告 |
4. @SafeVarargs
用途:
告知编译器,该方法的可变参数(varargs)不会造成堆污染(heap pollution)。
使用位置:
适用于 static
或 final
方法的可变参数方法。
示例:
@SafeVarargs
public static <T> void addAll(List<T> list, T... elements) {
for (T element : elements) {
list.add(element);}
}
背景说明:
- 可变参数在泛型中可能引发类型安全问题。
- 使用此注解后,编译器不会发出关于堆污染的警告。
5. @FunctionalInterface
(Java 8+)
用途:
标记一个接口为函数式接口(只有一个抽象方法的接口),可以作为 Lambda 表达式的目标类型。
使用位置:
用于接口定义上。
示例:
@FunctionalInterfacepublic interface MyFunction {void apply(); // 只能有一个抽象方法,否则编译报错}
特点:
- 可以有多个默认方法或静态方法。
- 若接口不符合函数式接口定义,使用该注解会导致编译错误。
6. @Native
用途:
表示一个常量字段可以通过本地代码访问(如 C/C++ 调用 Java 中的常量)。
使用位置:
用于字段(field)上。
示例:
@Native
public static final int MAX_VALUE = 100;
使用场景:
主要用于 JNI(Java Native Interface)开发中。
总结:JDK 内置注解一览表
注解名 | 使用位置 | 主要用途 |
---|---|---|
@Override | 方法 | 标记重写父类方法 |
@Deprecated | 类/方法/字段等 | 标记已废弃的内容 |
@SuppressWarnings | 多种 | 抑制编译器警告 |
@SafeVarargs | 方法 | 声明可变参数安全 |
@FunctionalInterface | 接口 | 声明函数式接口 |
@Native | 字段 | 标记可通过本地代码访问的常量 |
扩展建议
虽然上述注解是 JDK 自带的基础注解,但在实际开发中,你还可以通过以下方式扩展注解功能:
- 自定义注解 + 反射处理(如 Spring 框架中的
@Autowired
,@RequestMapping
) - 使用 APT(Annotation Processing Tool)生成代码(如 Lombok、Dagger 等工具)
Java 的异常处理机制是 Java 语言中用于处理程序运行时错误的重要特性。它允许程序在出现异常的情况下仍然保持正常的流程,并提供了一种结构化的方式来捕获和处理错误。
7.Java 元注解详解
在 Java 中,元注解(Meta-Annotations) 是用于注解其他注解的注解。它们定义了自定义注解的行为,比如该注解能在哪些元素上使用、生命周期有多长、是否可以被继承等。
Java 提供了 4 个标准的元注解:
元注解 | 说明 |
---|---|
@Retention | 注解的生命周期 |
@Target | 注解适用的目标类型 |
@Inherited | 子类是否继承父类的注解 |
@Documented | 注解是否包含在 Javadoc 中 |
1. @Retention
作用:
指定注解的生命周期,即注解信息保留在哪个阶段。
可选值():
值 | 描述 |
---|---|
SOURCE | 注解仅保留在源码中(编译时丢弃),如 @Override |
CLASS | 注解保留在 .class 文件中(默认),但运行时不可见 |
RUNTIME | 注解保留在运行时,可通过反射读取,适合框架处理 |
示例:
@Retention(RetentionPolicy.RUNTIME)public @interface MyAnnotation { }
如果你希望注解能在运行时通过反射获取,请使用 @Retention(RUNTIME)
。
2. @Target
作用:
指定注解可以应用在哪些程序元素上。
可选值(ElementType
枚举):
值 | 可应用位置 |
---|---|
TYPE | 类、接口、枚举 |
FIELD | 字段(属性) |
METHOD | 方法 |
PARAMETER | 方法参数 |
CONSTRUCTOR | 构造函数 |
LOCAL_VARIABLE | 局部变量 |
ANNOTATION_TYPE | 注解类型 |
PACKAGE | 包 |
TYPE_PARAMETER | 类型参数(Java 8+) |
TYPE_USE | 使用类型的任意地方(Java 8+) |
示例:
@Target(ElementType.METHOD)
public @interface MyMethodAnnotation { }
若不加 @Target
,则注解可以应用于所有支持注解的元素。
3. @Inherited
作用:
如果一个类使用了某个注解,并且该注解被 @Inherited
标记,那么它的子类会自动继承这个注解。
示例:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInheritedAnnotation { }
@MyInheritedAnnotation
public class ParentClass { }
public class ChildClass extends ParentClass { }
此时,ChildClass
也具有 @MyInheritedAnnotation
注解。
注意:@Inherited
仅对类有效,对接口或方法无效。
4. @Documented
作用:
标记该注解是否应被 Javadoc 工具记录。
示例:
@Documented@Retention(RetentionPolicy.RUNTIME)public @interface MyDocumentedAnnotation { String value(); }
当使用该注解时,生成的 Javadoc 将包含该注解信息。
总结:元注解对比表
元注解 | 用途 | 常用配置 |
---|---|---|
@Retention | 控制注解生命周期 | SOURCE , CLASS , RUNTIME |
@Target | 控制注解适用范围 | METHOD , FIELD , TYPE 等 |
@Inherited | 控制注解是否被继承 | 类继承时生效 |
@Documented | 控制是否生成文档 | 用于公共 API 文档 |
实际应用场景建议
场景 | 推荐使用的元注解组合 |
---|---|
框架拦截器(如权限控制) | @Retention(RUNTIME) + @Target(METHOD) |
配置类解析(如 Spring 的 @Component ) | @Retention(RUNTIME) + @Target(TYPE) |
日志记录 | @Retention(RUNTIME) + @Target(METHOD) |
编译期检查(如 Lombok) | @Retention(SOURCE) + @Target(TYPE) |
APT(注解处理器) | @Retention(SOURCE) + @Target(...) |
8.如何自定义 Java 注解
Java 注解是一种元数据机制,可以为代码提供额外信息。除了使用 JDK 提供的内置注解(如 @Override
、@Deprecated
等),你还可以自定义注解来满足特定业务需求或框架开发需要。
1.基本语法
定义一个简单注解:
使用 @interface
关键字声明注解类,内部可定义属性(无参数的方法)及默认值。
public @interface MyAnnotation { }
这个注解没有任何属性,仅用于标记作用。
2.添加注解属性(成员变量)
你可以为注解定义成员变量(也叫元素),这些成员变量在使用时必须赋值,除非提供了默认值。
属性声明类似方法,但无方法体,通过 default
指定默认值。
注解属性若无默认值,使用时必须显式赋值。
多属性赋值需显式指定键名:@MyAnnotation(value = "a", priority = 2)
示例:带属性的注解
public @interface MyAnnotation {
String name();
int value() default 0; // 带默认值
String[] tags() default {"default"};}
使用方式:
@MyAnnotation(name = "test", value = 10, tags = {"a", "b"})
public class MyClass { }
如果只有一个属性且名为 value
,则可以省略属性名写法:
@MyAnnotation(value = 10) // 或 @MyAnnotation(10)
3.使用元注解
元注解是“用于注解其他注解”的注解,用来定义自定义注解的行为。
常用的元注解包括:
元注解 | 说明 |
---|---|
@Retention | 注解的生命周期 |
@Target | 注解可以应用的目标类型 |
@Inherited | 子类是否继承父类的注解 |
@Documented | 是否包含在 Javadoc 中 |
示例:结合元注解定义注解
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) // 运行时保留@Target(ElementType.METHOD) // 只能用于方法@Documented // 包含在文档中public @interface MyRuntimeAnnotation {
String description() default "No description";
int order() default 0;}
使用示例:
@MyRuntimeAnnotation(description = "这是一个测试方法", order = 1)
public void testMethod() { }
4.运行时获取注解信息(反射)
要让注解在运行时起作用,必须使用 @Retention(RetentionPolicy.RUNTIME)
,然后通过反射读取。
示例:通过反射获取注解
public class AnnotationReader {public static void main(String[] args) throws Exception {//获取类的 Class 对象并查找方法Method method = MyClass.class.getMethod("testMethod");
/*
isAnnotationPresent(Class<? extends Annotation> annotationClass):
判断该方法上是否使用了 @MyRuntimeAnnotation 注解。
如果存在该注解,则返回 true,否则返回 false。
*/if (method.isAnnotationPresent(MyRuntimeAnnotation.class)) {
/*
getAnnotation():获取该方法上的指定注解实例。
返回的是 MyRuntimeAnnotation 类型的对象,可以通过它访问注解中的属性值。
*/MyRuntimeAnnotation annotation = method.getAnnotation(MyRuntimeAnnotation.class);
/*
description() 和 order() 是你在自定义注解中定义的方法,对应注解的属性。
这两个方法会返回在方法上使用注解时传入的值。
*/System.out.println("Description: " + annotation.description());System.out.println("Order: " + annotation.order());}}
}
5.自定义注解的典型应用场景
场景 | 描述 |
---|---|
日志记录/监控 | 标记方法需记录日志、性能监控等 |
权限控制 | 方法执行前检查用户权限 |
参数校验 | 在方法调用前验证输入参数 |
自动注册组件 | 框架扫描并注册带有特定注解的类 |
AOP 编程 | Spring AOP 利用注解进行切面编程 |
ORM 映射 | 如 Hibernate 使用注解映射数据库字段 |
6.完整示例汇总
1. 自定义注解定义
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Documented public@interface MyRuntimeAnnotation {String description() default "No description";int order() default 0;
}
2. 使用注解
public class MyClass {
@MyRuntimeAnnotation(description = "这是一个测试方法", order = 1)
public void testMethod() {
System.out.println("执行了 testMethod");}
}
3. 反射读取注解
public class AnnotationReader {public static void main(String[] args) throws Exception {Method method = MyClass.class.getMethod("testMethod");
if (method.isAnnotationPresent(MyRuntimeAnnotation.class)) {MyRuntimeAnnotation ann = method.getAnnotation(MyRuntimeAnnotation.class); System.out.println("描述:" + ann.description());
System.out.println("顺序:" + ann.order()); } } }
7.注意事项
注意事项 | 说明 |
---|---|
成员变量只能是基本类型、String、Class、enum、Annotation 或其数组形式 | 否则编译报错 |
不支持可变参数 | 注解不支持 ... 参数 |
属性值必须是常量表达式 | 不能动态计算 |
注解没有继承关系 | 注解之间不能继承 |
注解处理一般依赖框架 | 如 Spring、Hibernate 等框架会扫描注解并处理 |
总结
步骤 | 内容 |
---|---|
1. 定义注解 | 使用 @interface 关键字 |
2. 添加属性 | 类似接口方法,可设置默认值 |
3. 使用元注解 | 控制生命周期、适用范围等 |
4. 使用注解 | 应用于类、方法、字段等 |
5. 获取注解 | 使用反射获取并处理注解信息 |