文章目录
1、反射
1.1、类加载
类加载
当程序需要使用某个类时,如果该类还未被加载到内存中,则系统会通过类的加载、类的连接、类的初始化这三个步骤对该类进行初始化。如果不出意外,则 JVM 将会连续完成这三个步骤,所以有时也将这三个步骤统称为类加载或者类初始化。
- 步骤一、类的加载:当任何类在被使用时,都会将该类的 .class 文件读入内存,并为其创建一个 java.lang.Class 对象。
- 步骤二、类的连接
- 验证阶段:检验被加载的类是否有正确的内部结构,并与其他类协调一致。
- 准备阶段:负责为类的变量分配内存,并为其设置默认初始值。
- 解析阶段:将类的二进制数据中的符号引用替换为直接引用。
- 步骤三、类的初始化:首先该类需要完成类的加载、连接,如果该类存在直接父类,则需要父类依次完成类的加载、连接、初始化。
类加载时机
- 创建类的实例(new)。
- 使用反射主动加载 .class 文件(获取 Class 对象)。
- 调用类的静态方法、静态变量(包括为其静态变量赋值)。
- 加载该类的子类。
- 使用 java.exe 命令运行某个主类。
类加载器
类加载器的作用是将该类的 .class 文件加载到内存中,并为其创建一个 java.lang.Class 对象。虽然不用过分关心类的加载机制,但是可以更好地了解程序的运行。类的加载机制如下:
- 全盘负责:当一个类加载器负责加载某个 Class 时,该 Class 依赖与引用的其他 Class 也由该类加载器负责加载,除非显式地使用另一个类加载器。
- 父类委托:当一个类加载器负责加载某个 Class 时,首先会让父类加载器尝试加载,如果父类加载器无法加载,则尝试从自己的类路径下加载。
- 缓存机制:所有已加载过的 Class 都会缓存起来,当程序需要使用某个 Class 时,类加载器会先从缓存区中查找,如果没有找到,则系统才会读取对应的二进制数据,然后将其转换为 Class 对象并存储到缓存区。
1.2、反射机制
什么是反射
在程序的运行期获取一个类的变量与方法信息,然后使用这些信息创建该类的对象并调用其方法,这种动态特性称为反射机制(Reflect)。这种动态特性,可以让程序在编译期不用完成确定,在运行时仍然可以扩展,极大地增强了程序的灵活性。
反射的作用
可以为任意未知的类创建该类的对象并调用其方法。换句话说,就是为别人的类创建对象。
为什么说“反射就是 Java 框架的基石”
首先要理解框架的运行模式:框架是一套已经封装好的工具(半成品软件),然后用户基于框架完成开发,以便简化编码。因为用户编写的这些类,对框架来说事先是不知道的,但是框架必须创建这些类的对象并调用其方法,最后发挥作用。所以框架需要解决这个首要问题:如何为别人的类创建对象,正好可以使用反射。
反射的用途
- 需要为一个类创建对象,但是事先不知道该类的具体信息,可以使用反射(IOC)。
- 需要增强一个类的方法,但是事先不知道该类的具体信息,可以使用反射(AOP)。
反射的优缺点
- 优点:可以在程序运行期创建其对象,可以解耦并提高其可扩展性。
- 缺点:速度慢、可能不安全(访问 private 内容)、突破泛型约束等等。
1.3、获取 Class 对象
程序在计算机中的三个阶段
- Source 源码阶段:包含 .java、.class 文件,通过类加载到第二个阶段。
- Class 对象阶段:包含 Class 对象,具体分为成员变量、构造方法、成员方法、注解等等,通过创建对象到第三个阶段。
- Runtime 运行阶段:包含创建对象,可以使用 new 或者反射。
获取 Class 对象的三种方式
- Class.forName(“全类名”):通过 Class 类的静态方法获取。会同时将 .class 文件加载到内存中,通常用于从配置文件中获取类的信息。
- 任意类型.class:通过“任意类型.class”获取。包括基本类型、引用类型(类、接口、枚举、注解、数组等等),通常用于作为某个方法的参数。
- 对象.getClass():通过 Object 类的方法获取,通常用于对象已经存在。
注意:在程序的运行过程中,所有已加载过的 Class 对象都会缓存起来,同一个 .class 文件只会被加载到内存一次,因此所有方式获取到的 Class 对象都是同一个。
public class GetClass {
public static void main(String[] args) throws ClassNotFoundException {
String s = "ABC";
// 获取 Class 对象的三种方式
Class<?> cls1 = Class.forName("java.lang.String");// 泛型为 Object 类,可以强转为 String 类
Class<String> cls2 = String.class;// 泛型为 String 类
Class<? extends String> cls3 = s.getClass();// 泛型为 String 类或者其子类,这里实际为 String 类
// 所有方式获取到的 Class 对象都是同一个
System.out.println(cls1 == cls2 && cls2 == cls3);// true
}
}
不同 Class 对象的泛型
- 基本类型的泛型:对应的包装类。
- 引用类型的泛型(类、接口、枚举、注解、数组等等):原 Java 类。
- void 关键字:Void。
public class ClassType {
public static void main(String[] args) {
// 基本类型
Class<Integer> cls1 = int.class;
// 引用类型
Class<Integer> cls2 = Integer.class;
// 数组
Class<int[]> cls3 = int[].class;
Class<Integer[]> cls4 = Integer[].class;
// 接口
Class<List> cls5 = List.class;
// 枚举
Class<? extends Color> cls6 = Color.BLUE.getClass();
Class<Color> cls7 = Color.class;
// void
Class<Void> cls8 = void.class;
System.out.println(cls8);
}
}
1.4、Class 类
Class 类的常用方法
// 返回成员变量
Field[] getFields()// 所有 public 的成员变量
Field getField(String name)// 指定名称且是 public 的成员变量
Field[] getDeclaredFields()// 所有的成员变量
Field getDeclaredField(String name)// 指定名称的成员变量
// 返回构造方法
Constructor<?>[] getConstructors()
Constructor<T> getConstructor(Class<?>... parameterTypes)
Constructor<?>[] getDeclaredConstructors()
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
// 返回成员方法
Method[] getMethods()
Method getMethod(String name, Class<?>... parameterTypes)
Method[] getDeclaredMethods()
Method getDeclaredMethod(String name, Class<?>... parameterTypes)
String getName()// 返回全类名
T newInstance()// 返回类的实例,注意:这里默认调用无参构造,在 JDK1.9 中已被废弃,建议使用 Constructor
ClassLoader getClassLoader()// 返回类加载器
Class<? super T> getSuperclass()// 返回直接父类的 Class 对象
Field 类的常用方法
Object get(Object obj)// 返回指定对象的成员变量的值
void set(Object obj, Object value)// 设置指定对象的成员变量的值
void setAccessible(true)// 忽略安全检查,用于访问非 public 的成员变量,同时可以提高运行效率(构造方法、成员方法类似)
Constructor 类的常用方法
T newInstance(Object... initargs)// 返回类的实例,如果参数为空则作用同 Class 对象的 T newInstance() 方法
Method 类的常用方法
Object invoke(Object obj, Object... args)// 执行方法
1.5、【案例】编写一个“框架”
要求:编写一个"框架",在不改变 Java 代码的情况下,实现创建任意类的对象,然后执行目标方法。
实现:读取配置文件中的信息(包括目标类的类名、方法名、方法参数等等),然后使用反射执行目标方法。
className=Java05_ReflectAndAnnotation.T03_Framework.Student
methodName=study
public class Student {
public void study() {
System.out.println("好好学习");
}
}
// 编写一个“框架”
public class Demo {
public static void main(String[] args) throws Exception {
// 1、读取配置文件中的信息(包括目标类的类名、方法名、方法参数等等)
InputStream is = Demo.class.getClassLoader().getResourceAsStream("反射.txt");
Properties properties = new Properties();
properties.load(is);
is.close();
String className = properties.getProperty("className");
String methodName = properties.getProperty("methodName");
// 2、使用反射执行目标方法
Class<?> clazz = Class.forName(className);
Constructor<?> constructor = clazz.getConstructor();
Object o = constructor.newInstance();
Method method = clazz.getMethod(methodName);
method.invoke(o);
}
}
1.6、【案例】突破泛型约束
反射可以完成很多不可能的事情,例如:访问类的 private 内容,突破泛型约束等等。例如:可以添加 String 类型到 Integer 类型的集合中,说明 Java 泛型约束只存在于编译期,程序运行时会自动转换为 Object 类型,因此 Java 泛型俗称“伪泛型”。
// 突破泛型约束
public class Demo {
public static void main(String[] args) throws Exception {
// 1、定义 Integer 类型的集合,正常只能添加 Integer 类型
List<Integer> list = new ArrayList<>();
list.add(12);
list.add(18);
// 2、使用反射,add() 方法的参数类型为 Object 类型
list.getClass().getMethod("add", Object.class).invoke(list, "张三");
// 3、输出集合,结果正常
System.out.println(list);// [12, 18, 张三]
// 4、接收这个元素
// Integer integer = list.get(2);// 使用 Integer 类型接收,虽然编译期正常,但是运行时会抛出 ClassCastException 异常
// String string = list.get(2);// 使用 String 接收,编译期直接报错
Object object = list.get(2);// 使用 Object 接收,结果正常
}
}
2、注解
2.1、注解介绍
什么是注解
注解(Annotation)也叫元数据,一种代码级别的说明。从 JDK1.5 时引入,与类、接口、枚举是在同一个层次。可以声明在包、类、字段、方法、局部变量、方法形参列表等地方,用来对这些元素进行说明,注释。注解作用如下:
- 编写文档:用于生成 API 文档。例如:@Return。
- 编译检查:用于让编译器能实现基本的编译检查。例如:@Override。
- 代码分析:用于对代码进行分析,通常使用反射。
其他 Java 注解:重写检查 @Override、已过时的 @Deprecated、警告抑制 @SuppressWarnings(“all”)、函数式接口检查 @FunnctionalInterface 等等。
注解与注释的区别
- 注解:说明程序的,给计算机看。
- 注释:说明程序的,给程序员看。
2.2、元注解
元注解是描述注解的注解,用于定义注解。
- @Target:描述注解的作用位置。
- @Target(ElementType.TYPE):类、接口、枚举、注解。
- @Target(ElementType.FIELD):字段、枚举常量。
- @Target(ElementType.METHOD):方法。
- @Target(ElementType.PARAMETER):方法形参。
- @Target(ElementType.CONSTRUCTOR):构造方法。
- @Target(ElementType.LOCAL_VARIABLE):。
- @Target(ElementType.ANNOTATION_TYPE):注解。
- @Target(ElementType.PACKAGE):包。
- @Retention:描述注解的保留阶段。
- @Retention(RetentionPolicy.SOURCE):源码级别保留,编译时会被忽略,不会被写入 .class 文件。
- @Retention(RetentionPolicy.CLASS):编译期间保留,会被写入 .class 文件,但是会被 JVM 忽略,运行时无法获得。
- @Retention(RetentionPolicy.RUNTIME):运行期间保留,会被 JVM 保留,运行时会被 JVM 或者其他使用反射机制的程序读取与使用。
- @Documented:该注解会存在于 API 文档中。
- @Inherited:该注解会被子类继承。
2.3、自定义注解
语法
- default 默认值:如果注解属性已定义默认值,则使用时可以不赋值。
- 属性名称:如果注解属性有且只有一个 value 需要赋值,则使用时 value 可以省略。
- 数据类型:如果注解属性为数组,则使用时 {} 包围。如果数组中只有一个值,则使用时 {} 可以省略。
元注解
public @interface 注解名称 {
数据类型 属性名称 default 默认值;
}
自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Invocation {
String className() default "";
String methodName() default "";
}
2.4、注解的本质
使用 JDK 命令反编译 .class 文件
- 步骤一、创建自定义注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Invocation {
String className() default "";
String methodName() default "";
}
- 步骤二、使用 javac 命令编译源文件得到 .class 文件。
- 步骤三、使用 javap 命令反编译 .class 文件得到新的源文件。
public interface T15_Annotation.T01_OriginalAnnotation.Invocation extends java.lang.annotation.Annotation {
public abstract java.lang.String className();
public abstract java.lang.String methodName();
}
注解的本质
注解的本质是一个接口,该接口继承了 java.lang.annotation.Annotation 接口。
- 注解的属性名称:对应接口中的抽象方法名称。
- 注解属性的数据类型:对应接口中抽象方法的返回值类型。
2.5、注解与反射
使用反射获取注解中的信息
- 步骤一、创建自定义注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Invocation {
String className() default "";
String methodName() default "";
}
- 步骤二、使用自定义注解。
- 步骤三、使用反射,获取注解中定义的属性值信息。
@Invocation(className = "T15_Annotation.T02_GetAnnotationInformation.Student", methodName = "study")
public class InvocationTest {
public static void main(String[] args) throws Exception {
// 获取注解信息
Invocation invocation = InvocationTest.class.getAnnotation(Invocation.class);
// 注解的本质是在内存中生成了一个注解接口的实现类对象
/*
public class InvocationImpl implements Invocation {
public String className() {
return "T15_Annotation.T02_GetAnnotationInformation.Student";
}
public String methodName() {
return "study";
}
}
*/
String className = invocation.className();
String methodName = invocation.methodName();
// 使用反射获得目标类的对象
Class<?> clazz = Class.forName(className);
Object o = clazz.newInstance();
// 使用反射获得目标类的方法
Method method = clazz.getMethod(methodName);
// 执行目标方法
method.invoke(o);
}
}
注解对象的本质
注解的本质是在内存中生成了一个注解接口的实现类对象。
注解与反射的使用说明
- 反射:首先从文本文件中读取类的信息,然后使用反射执行方法。
- 注解:首先使用反射从 Java 类中读取注解的信息,然后使用反射执行方法。
2.6、【案例】编写一个“测试框架”
要求:编写一个“测试框架”,要求运行所有存在 @Check 注解的方法,并将结果输出到文件中。
实现:首先定义 @Check 注解,然后将其添加到测试方法上,使用反射获取目标类中的所有方法,遍历所有方法并运行存在 @Check 注解的方法,最后将结果输出到文件中。
总结:注解就是一个标记,注解具体有什么用,需要交给注解解析程序处理。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Check {
}
public class Operation {
@Check
public void test01() {
System.out.println("test01");
}
public void test02() {
System.out.println("test02");
}
@Check
public void test03() {
System.out.println("test03");
}
@Check
public void test04() throws Exception {
System.out.println("test04");
throw new Exception("出现异常");
}
}
public class Demo {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
Operation operation = new Operation();
Method[] methods = operation.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Check.class)) {// 如果该方法上存在该注解,则返回 true,否则返回 false
method.invoke(operation);
System.out.println(method.getName() + "测试完毕");
}
}
}
}