Skip to content

反射、注解

  • 反射
    • 获取字节码方式 ❤️
    • 获取成员变量 🍐 成 员、构造、方法获取方式非常相似
    • 获取构造方法 🍐
    • 获取普通方法 🍐
  • 注解
    • 注解的作用 🍐
    • 解析注解 🚀

前置知识

  1. 知道.java文件通过javac命令 能编译成.class文件
  2. 知道类一般由成员变量、构造方法、普通方法组成
  3. 能理解配置文件的作用

一、反射 🚩 🍐 理解

学前交代

  1. 反射、动态代理、注解等知识点,在以后开发中极少用到。
  2. 这些技术都是以后学习框架、或者做框架的底层源码的。
  3. 讲解的目的是为了以后我们理解框架、或者自己开发框架架构师给别人用作铺垫的
  4. 接下来的学习的节奏是:先认识,在了解作用和场景

反射技术

反射技术 指的是加载类的字节码内存,并以编程的方法解刨出类中各个成分(成员变量、方法、构造器等)

  1. Java中反射技术用到的类在java.lang.reflect包中
  2. 用IDEA开发程序时,用对象调用方法,IDEA会有代码提示,idea会将这个对象能调用的方法都给你列举出来,供你选择。这个场景就用到了反射技术
  3. 学习反射,主要学习通过反射技术获得:

由于Java的设计原则是万物皆对象,获取到的类其实也是以对象的形式体现的,叫做字节码对象

  • 字节码对象,用Class类来表示
  • 构造器对象,用Constructor类的对象来表示
  • 成员变量对象,用Field类的对象来表示
  • 成员方法对象,用Method类的对象来表示

练习反射素材准备

准备案例代码Cat类,接下来通过方法获得他的成员变量,成员方法,以及构造方法对象

点击查看Cat类素材

java
public class Cat{
//    私有的成员变量
    private String name;
    private int age;

//    公开的成员变量
    public int sex;


//    公开的无参构造
    public Cat(){
        System.out.println("空参数构造方法执行了");
    }

//    私有的有参构造
    private Cat(String name, int age){
        System.out.println("有参数构造方法执行了");
        this.name=name;
        this.age=age;
    }

    //私有的无参普通方法
    private void run(){
        System.out.println("(>^ω^<)喵跑得贼快~~");
    }

    //公开的无参普通方法
    public void eat(){
        System.out.println("(>^ω^<)喵爱吃猫粮~");
    }

    //私有的有参普通方法
    private String eat(String name){
        return "(>^ω^<)喵爱吃:"+name;
    }

//  get 和set 以及toString方法省略...自行实现

}

1️⃣ 1.1 反射入门案例 🍐 ✏️

1.1 获取类的字节码

反射的第一步:是将字节码加载到内存,我们需要获取到的字节码对象。

比如有一个Cat类,获取Cat类的字节码代码有三种写法。不管用哪一种方式,获取到的字节码对象其实是同一个。

java
public class Test1Class{
    public static void main(String[] args){
        // 方式1:类名.class
        Class c1 = Cat.class;
        System.out.println(c1.getName()); //获取全类名
        System.out.println(c1.getSimpleName()); //获取简单类名

        // 方式2:Class.forName("全类名")
        // 全类名快捷键:选中类,按住ctrl+alt+shift+C 粘贴到 "这里"
        Class c2 = Class.forName("com.itheima.reflect.Cat");
        System.out.println(c1 == c2); //true

         // 方式3:对象.getClass()
        Cat s = new Cat();
        Class c3 = s.getClass();
        System.out.println(c2 == c3); //true
    }
}

1.2 获取类的构造器

反射的第二步:通过字节码对象获得获取构造器对象

获取构造器,需要用到Class类提供的几个方法,如下图所示:

方法记忆技巧

  • getXX:获取什么
  • Constructor: 构造方法的意思
  • Declared:获得任意的,包含private的,没有这个单词表示只能获取一个public修饰的
  • 后缀s: 表示可以获取多个,没有后缀s只能获取一个

点击查看获取类中构造器对象代码

    1. 接下来,我们写一个测试方法,来测试获取类中所有的构造器
java
public class Test2Constructor(){
    @Test
    public void testGetConstructors(){
        //1、反射第一步:必须先得到这个类的Class对象
        Class c = Cat.class;

        //2、获取类的全部构造器
        Constructor[] constructors = c.getDeclaredConstructors();
        //3、遍历数组中的每一个构造器对象。
        for(Constructor constructor: constructors){
            System.out.println(constructor.getName()+"---> 参数个数:"+constructor.getParameterCount());
        }
    }
}

运行测试方法打印结果如下

    1. 刚才演示的是获取Cat类中所有的构造器,接下来,我们演示单个构造器试一试
java
public class Test2Constructor(){
    @Test
    public void testGetConstructor(){
        //1、反射第一步:必须先得到这个类的Class对象
        Class c = Cat.class;

        //2、获取类public修饰的空参数构造器
        Constructor constructor1 = c.getConstructor();
        System.out.println(constructor1.getName()+"---> 参数个数:"+constructor1.getParameterCount());

        //3、获取private修饰的有两个参数的构造器,第一个参数String类型,第二个参数int类型
        Constructor constructor2 =
            c.getDeclaredConstructor(String.class,int.class);

        System.out.println(constructor2.getName()+"---> 参数个数:"+constructor1.getParameterCount());

    }
}

打印结果如下

点击查看构造器的作用

其实构造器的作用:初始化对象并返回

1.调用公开且无参的构造 示例代码 👇

java
public class Test1 {
    public static void main(String[] args) throws Exception {
//        获得无参的构造方法
        Constructor<Cat> constructor1 = Cat.class.getConstructor();
//        通过构造方法创建Cat对象(注意:不需要new哦)
        Cat cat = constructor1.newInstance();
//        打印对象
        System.out.println(cat);
    }
}

2.调用私有且有参的构造示例代码 👇

java
public class Test2 {
    public static void main(String[] args) throws Exception {
//        获得私有的有参构造方法
        Constructor<Cat> constructor1 = Cat.class.getDeclaredConstructor(String.class,int.class);
//        因为是私有的,所以要手动取消检查
        constructor1.setAccessible(true);
//        通过有参的构造方法创建Cat对象(注意:不需要new哦)
        Cat cat = constructor1.newInstance("韩立",1000);
//        打印对象
        System.out.println(cat);
    }
}

这里我们需要用到如下的两个方法,注意:这两个方法时属于Constructor的,需要用Constructor对象来调用。

如下图所示,constructor1和constructor2分别表示Cat类中的两个构造器。现在我要把这两个构造器执行起来

由于构造器是private修饰的,先需要调用setAccessible(true) 表示禁止检查访问控制,然后再调用newInstance(实参列表) 就可以执行构造器,完成对象的初始化了。

代码如下:为了看到构造器真的执行, 故意在两个构造器中分别加了两个打印语句

代码的执行结果如下图所示:

1.3 获取类的成员变量

反射的第三步:通过字节码文件对象获得成员变量对象

其实套路是一样的,在Class类中提供了获取成员变量的方法,如下图所示。

方法记忆技巧

  • getXX:获取什么
  • Constructor: 构造方法的意思
  • Declared:获得任意的,包含private的,没有这个单词表示只能获取一个public修饰的
  • 后缀s: 表示可以获取多个,没有后缀s只能获取一个

获得成员变量对象的示例代码

java
public class Test3 {
    public static void main(String[] args) throws Exception {
//获得字节码文件
        Class<Cat> catClass = Cat.class;
//        通过字节码文件 获得全部的成员变量
        Field[] declaredFields = catClass.getDeclaredFields();
        System.out.println(Arrays.toString(declaredFields));

//     通过字节码文件 获得public成员变量
        Field[] fields = catClass.getFields();
        System.out.println(Arrays.toString(fields));

    }
}

点击查看使用获取到成员变量的对象 代码

  • 获取到成员变量的对象之后该如何使用呢? 在Filed类中提供给给成员变量赋值和获取值的方法,如下图所示。

使用获得的成员变量的示例代码 👇

java
public class Test3 {
    public static void main(String[] args) throws Exception {
//获得字节码文件
        Class<Cat> catClass = Cat.class;
//        通过字节码文件 获得指定的成员变量
        Field nameField = catClass.getDeclaredField("name");
        Field sexField = catClass.getDeclaredField("sex");
//        创建对象
         Cat cat = catClass.newInstance();

//        设置值
        //        name是私有的,需要取消检查
        nameField.setAccessible(true);
        nameField.set(cat,"加菲猫");

        //        sex是公开的不需要取消检查
        sexField.set(cat,999);

//        输出对象
        System.out.println(cat);
    }
}

再次强调一下设置值、获取值的方法时Filed类的需要用Filed类的对象来调用,而且不管是设置值、还是获取值,都需要依赖于该变量所属的对象。代码如下

效果:

1.4获取类的成员方法

反射的第二步:通过字节码文件对象获得成员方法对象

在Java中反射包中,每一个成员方法用Method对象来表示,通过Class类提供的方法可以获取类中的成员方法对象。如下下图所示

方法记忆技巧

  • getXX:获取什么
  • Constructor: 构造方法的意思
  • Declared:获得任意的,包含private的,没有这个单词表示只能获取一个public修饰的
  • 后缀s: 表示可以获取多个,没有后缀s只能获取一个

获得成员方法的示例代码 👇

java
public class Test4 {
    public static void main(String[] args) throws Exception {
//获得字节码文件
        Class<Cat> catClass = Cat.class;
//        通过字节码文件 获得指定的 无参 成员方法
         Method run = catClass.getDeclaredMethod("run");
//        通过字节码文件 获得指定的 有参 成员方法
        Method eat = catClass.getDeclaredMethod("eat",String.class);

//        调用方法需要对象,创建对象
         Cat cat = catClass.newInstance();

//         注意:2个方法都是private 需要取消安全检查
        run.setAccessible(true);
        eat.setAccessible(true);
//         调用方法
        run.invoke(cat);

//        eat方法有返回值
         Object ret = eat.invoke(cat, "欧力给");
        System.out.println(ret);


    }
}

获取到成员方法的对象之后该如何使用呢

在Method类中提供了方法,可以将方法自己执行起来。

下面我们演示一下,把run()方法和eat(String name)方法执行起来。看分割线之下的代码

java
public class Test3Method{
    public static void main(String[] args){
        //1、反射第一步:先获取到Class对象
        Class c = Cat.class;

        //2、获取类中的全部成员方法
        Method[] methods = c.getDecalaredMethods();

        //3、遍历这个数组中的每一个方法对象
        for(Method method : methods){
            System.out.println(method.getName()+"-->"+method.getParameterCount()+"-->"+method.getReturnType());
        }

        System.out.println("-----------------------");
        //4、获取private修饰的run方法,得到Method对象
        Method run = c.getDecalaredMethod("run");
        //执行run方法,在执行前需要取消权限检查
        Cat cat = new Cat();
        run.setAccessible(true);
        Object rs1 = run.invoke(cat);
        System.out.println(rs1)

        //5、获取private 修饰的eat(String name)方法,得到Method对象
        Method eat = c.getDeclaredMethod("eat",String.class);
        eat.setAccessible(true);
        Object rs2 = eat.invoke(cat,"鱼儿");
        System.out.println(rs2)
    }
}

打印结果如下图所示:run()方法执行后打印猫跑得贼快~~,返回null; eat()方法执行完,直接返回猫最爱吃:鱼儿

2️⃣ 1.2 反射应用 🍐 ✏️

知识前提

  1. 能获得字节码文件对象
  2. 能通过字节码对象获得普通方法对象和私有方法(Method)对象
  3. 能通过字节码对象获得普通成员对象和私有成员(Field)对象
  4. 理解反射可以获得运行状态下的类的成员变量和成员方法,并可以调用成员方法和修改成员变量的值

温馨提示

各位小伙伴,按照前面我们学习反射的套路,我们已经充分认识了:

  1. 什么是反射,
  2. 反射的核心作用是用来获取类的各个组成部分并执行他们。
    • 但是由于小伙伴的经验有限,对于反射的具体应用场景还是很难感受到的(这个目前没有太好的办法,只能慢慢积累,等经验积累到一定程度,就会豁然开朗了)。
      • 下面会提供2个案例,一起来巩固下

需求1

让我们写一个框架,能够将任意一个对象的属性名和属性值写到文件中去。不管这个对象有多少个属性,也不管这个对象的属性名是否相同。

需求分析

java
1.先写好两个类,一个Student类和Teacher类
2.写一个ObjectFrame类代表框本架
ObjectFrame类中定义一个saveObject(Object obj)方法,用于将任意对象存到文件中去
	参数:Object obj: 就表示要存入文件中的对象

3.编写方法内部的代码,往文件中存储对象的属性名和属性值
	1)参数obj对象中有哪些属性,属性名是什么实现值是什么,中有对象自己最清楚。
	2)接着就通过反射获取类的成员变量信息了(变量名、变量值)
	3)把变量名和变量值写到文件中去

点击查看示例代码

1️⃣ 写一个ObjectFrame表示自己设计的框架,代码如下图所示

java
public class ObjectFrame{
    public static void saveObject(Object obj) throws Exception{
        PrintStream ps =
            new PrintStream(new FileOutputStream("模块名\\src\\data.txt",true));
        //1)参数obj对象中有哪些属性,属性名是什么实现值是什么,中有对象自己最清楚。
		//2)接着就通过反射获取类的成员变量信息了(变量名、变量值)
        Class c = obj.getClass(); //获取字节码
        ps.println("---------"+class.getSimpleName()+"---------");

        Field[] fields = c.getDeclaredFields(); //获取所有成员变量
		//3)把变量名和变量值写到文件中去
        for(Field field : fields){
            String name = field.getName();
            Object value = field.get(obj)+"";
            ps.println(name);
        }
        ps.close();
    }
}

使用自己设计的框架,往文件中写入Student对象的信息和Teacher对象的信息。

2️⃣ 先准备好Student类和Teacher类

java
public class Student{
    private String name;
    private int age;
    private char sex;
    private double height;
    private String hobby;
}
java
public class Teacher{
    private String name;
    private double salary;
}

3️⃣ 创建一个测试类,在测试中类创建一个Student对象,创建一个Teacher对象,用ObjectFrame的方法把这两个对象所有的属性名和属性值写到文件中去。

java
public class Test5Frame{
    @Test
    public void save() throws Exception{
        Student s1 = new Student("黑马吴彦祖",45, '', 185.3, "篮球,冰球,阅读");
        Teacher s2 = new Teacher("播妞",999.9);

        ObjectFrame.save(s1);
        ObjectFrame.save(s2);
    }
}

4️⃣ 打开data.txt文件,内容如下图所示,就说明我们这个框架的功能已经实现了

需求2

  1. 在泛型为 Integer 的 ArrayList 中,存入一个字符串
  2. 调用 ArrayList 中私有的方法 outOfBoundsMsg,并且打印返回值

点击查看案例代码

java
/**
 * Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,
 * 该机制允许程序员在编译时检测到非法的类型。
 */
class ClassLoaderDemo2 {
    public static void main(String[] args) throws Exception {
        ArrayList<Integer> integers = new ArrayList<>();

        integers.add(111);
        integers.add(222);
        integers.add(333);
//        integers.add("韩立"); 因为泛型的原因,无法直接添加字符串

        //在运行状态下,获取add方法,并且在运行状态下 调用add方法,并传入参数
        Method add = integers.getClass().getMethod("add", Object.class);
        add.invoke(integers,"韩立");
        System.out.println(integers);

        //无视修饰符,可以调用任意方法
        Method outOfBoundsMsg = integers.getClass().getDeclaredMethod("outOfBoundsMsg", int.class);
        outOfBoundsMsg.setAccessible(true);
        Object invoke = outOfBoundsMsg.invoke(integers, 1111);
        System.out.println("结果:"+invoke);

    }
}

二、注解 🚩

1️⃣ 2.1 认识注解&定义注解

学习目的

注解和反射一样,都是用来做框架的,我们这里学习注解的目的其实是为了以后学习框架或者做框架做铺垫的。

学习技巧:先认识注解,掌握注解的定义和使用格式,然后学习应用场景

一、注解的定义和使用

注释:给程序员看的,了解程序的情况

注解:程序的标记,程序获取其中的信息,根据信息执行程序

  1. Java注解是代码中的特殊标记,比如@Override@Test等,作用是:让其他程序根据注解信息决定怎么执行该程序。 👈

点击查看用过的注解

比如:Junit框架的@Test注解可以用在方法上,用来标记这个方法是测试方法,被@Test标记的方法能够被Junit框架执行。

再比如:@Override注解可以用在方法上,用来标记这个方法是重写方法,被@Override注解标记的方法能够被IDEA识别进行语法检查。

  1. 注解不光可以用在方法上,还可以用在类上、变量上、构造器上等位置。 👈

二、自定义注解:

自定义注解的格式如下图所示

  1. 格式属性类型
    • 基本数据类型
    • String
    • Class 字节码文件对象
    • 注解
    • 枚举 Enum
    • 以上类型的一维数组

注意:如果只有一个属性需要赋值,并且属性的名称是 value,则 value 可以省略,直接定义值即可(如下图)

三、自定义注解和使用自定义注解

比如:现在我们自定义一个MyTest注解

java
public @interface MyTest{
    String aaa();
    boolean bbb() default true;	//default true 表示默认值为true,使用时可以不赋值。
    String[] ccc();
}

定义好MyTest注解之后,我们可以使用MyTest注解在类上、方法上等位置做标记。注意使用注解时需要加@符号,如下

java
@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{
    @MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"})
    public void test1(){

    }
}

注意:注解的属性名如何是value的话,并且只有value没有默认值,使用注解时value名称可以省略。比如现在重新定义一个MyTest2注解

java
public @interface MyTest2{
    String value(); //特殊属性
    int age() default 10;
}

定义好MyTest2注解后,再将@MyTest2标记在类上,此时value属性名可以省略,代码如下

java
@MyTest2("孙悟空") //等价于 @MyTest2(value="孙悟空")
@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{
    @MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"})
    public void test1(){

    }
}

四、注解本质是接口

Haah注解代码:

经过对MyTest1注解字节码反编译(javap命令)我们会发现:

  1. Haah注解本质上是接口,每一个注解接口都继承子Annotation接口
  2. Haah注解中的属性本质上是抽象方法
  3. @Haah("hanli")里面的属性值,可以通过调用value()方法获取到。

2️⃣ 3.2 元注解

什么是元注解

元注解是修饰注解的注解。这句话虽然有一点饶,但是非常准确。我们看一个例子

接下来分别看一下@Target注解和@Retention注解有什么作用,如下所示

  • @Target是用来声明注解只能用在那些位置,比如:类上、方法上、成员变量上等
  • @Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期
  • @Target元注解的使用:比如定义一个MyTest3注解,并添加@Target注解用来声明MyTest3的使用位置
java
@Target(ElementType.TYPE)	//声明@MyTest3注解只能用在类上
public @interface MyTest3{

}

接下来,我们把@MyTest3用来类上观察是否有错,再把@MyTest3用在方法上、变量上再观察是否有错

如果我们定义MyTest3注解时,使用@Target注解属性值写成下面样子

java
//声明@MyTest3注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface MyTest3{

}

此时再观察,@MyTest用在类上、方法上、变量上是否有错

到这里@Target元注解的使用就演示完毕了。

  • @Retetion元注解的使用:定义MyTest3注解时,给MyTest3注解添加@Retetion注解来声明MyTest3注解保留的时期
java
@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期
	@Retetion(RetetionPloicy.SOURCE): 注解保留到源代码时期、字节码中就没有了
	@Retetion(RetetionPloicy.CLASS): 注解保留到字节码中、运行时注解就没有了
	@Retetion(RetetionPloicy.RUNTIME):注解保留到运行时期
	【自己写代码时,比较常用的是保留到运行时期】
java
//声明@MyTest3注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
//控制使用了@MyTest3注解的代码中,@MyTest3保留到运行时期
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest3{

}

3️⃣ 3.3 解析注解以应用

知识储备

  1. 能够自己定义注解
  2. 能够把自己定义的注解标记在类上或者方法上等位置
  3. 也理解元注解的作用
  4. 也知道注解能给程序做标记,但是缺乏实战

解析注解说明

解析注解:获取类上、方法上、变量上等位置注解及注解属性值

解析注解套路如下:

  1. 如果注解在类上,先获取类的字节码对象,再获取类上的注解
  2. 如果注解在方法上,先获取方法对象,再获取方法上的注解
  3. 如果注解在成员变量上,先获取成员变量对象,再获取变量上的注解

总之:注解在谁身上,就先获取谁,再用谁获取谁身上的注解

用到的方法:

案例1 解析注解

解析来看一个案例,来演示解析注解的代码编写

先定义一个MyTest4注解

java
//声明@MyTest4注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
//控制使用了@MyTest4注解的代码中,@MyTest4保留到运行时期
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest4{
    String value();
    double aaa() default 100;
    String[] bbb();
}

定义有一个类Demo

java
@MyTest4(value="蜘蛛侠",aaa=99.9, bbb={"至尊宝","黑马"})
public class Demo{
    @MyTest4(value="孙悟空",aaa=199.9, bbb={"紫霞","牛夫人"})
    public void test1(){

    }
}

③ 写一个测试类AnnotationTest3解析Demo类上的MyTest4注解

java
public class AnnotationTest3{
@Test
public void parseClass(){
    //1.先获取Class对象
    Class c = Demo.class;

    //2.解析Demo类上的注解
    if(c.isAnnotationPresent(MyTest4.class)){
        //获取类上的MyTest4注解
        MyTest4 myTest4 = (MyTest4)c.getDeclaredAnnotation(MyTest4.class);
        //获取MyTests4注解的属性值
        System.out.println(myTest4.value());
        System.out.println(myTest4.aaa());
        System.out.println(myTest4.bbb());
    }
}

@Test
public void parseMethods(){
    //1.先获取Class对象
    Class c = Demo.class;

    //2.解析Demo类中test1方法上的注解MyTest4注解
    Method m = c.getDeclaredMethod("test1");
    if(m.isAnnotationPresent(MyTest4.class)){
        //获取方法上的MyTest4注解
        MyTest4 myTest4 = (MyTest4)m.getDeclaredAnnotation(MyTest4.class);
        //获取MyTests4注解的属性值
        System.out.println(myTest4.value());
        System.out.println(myTest4.aaa());
        System.out.println(myTest4.bbb());
    }
}
}

课堂作业

🚩 1. 安装上述代码,完整的练习一遍,理解其中的流程

案例2 模拟单元测试框架

  • 需求说明:自定一个@MyTest的注解,配置在方法上
    • @MyTest注解的方法可以被执行
    • 没有@MyTest注解的方法不能被执行

第一步:先定义一个MyTest注解

java
@Target(ElementType.METHOD)
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest{

}

第二步:写一个测试类AnnotationTest4,在类中定义几个被@MyTest注解标记的方法

java
public class AnnotationTest4{
    @MyTest
    public void test1(){
        System.out.println("=====test1====");
    }

    @MyTest
    public void test2(){
        System.out.println("=====test2====");
    }


    public void test3(){
        System.out.println("=====test2====");
    }

    public static void main(String[] args){
        AnnotationTest4 a = new AnnotationTest4();

        //1.先获取Class对象
        Class c = AnnotationTest4.class;

        //2.解析AnnotationTest4类中所有的方法对象
        Method[] methods = c.getDeclaredMethods();
        for(Method m: methods){
            //3.判断方法上是否有MyTest注解,有就执行该方法
            if(m.isAnnotationPresent(MyTest.class)){
            	m.invoke(a);
        	}
        }
    }
}

用心去做高质量的内容网站,欢迎 star ⭐ 让更多人发现