Java 05 - 反射与注解

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() + "测试完毕");
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值