一文教你搞懂反射和注解!!!(入门级/简单)

582 阅读11分钟

反射(Reflect)

1.什么是反射?

  • 反射就是将类的各个组成部分封装成其他对象,这就是反射机制。

2.反射的好处

  • 可以在程序运行过程中操作被封装的对象
  • 可以解耦,提高程序的可扩展性

3.回顾一下java代码的执行底层原理

  • 在学习反射之前我们要先了解一下java代码的执行过程:java代码本质上就是.java文件,它经过java编译器的编译(命令是javac xxx.java)变为.class字节码文件(二进制文件)(存在于硬盘上),然后经过jvm(java虚拟机)内部的一系列执行过程**(在内存中)**就变成了底层操作系统可执行的操作指令。原理如下图:

在这里插入图片描述

  • 这里要说明几点:
  1. java语言的跨平台性就体现在java源码经过java编译器编译后变为统一格式的字节码文件,字节码文件交由java虚拟机执行为操作系统可识别的指令,这期间java虚拟机屏蔽了不同操作系统平台和底层硬件间的差异。
  2. jvm中的java解释器是用来将编译后的字节码文件解释为计算机底层可以使用的指令。其中java解释器和编译器分别对应jdk bin 目录下的javac.exe和javac.exe
  3. JDK的三个版本:javaSE:(java standard edition ) java标准版。javaEE:(java enterprise edition )java企业版。javaME:(java micro edition )java移动设备版
  4. jdk与jre的关系:jdk是java development kit的缩写,表示java开发工具箱,jre是java runtime environment的缩写,表示java程序运行的核心环境,包括jvm和一些核心库。在jdk7一前,jdk包括jre。
  5. Java虚拟机的主要任务是装载class文件,并执行其中的字节码,不同的Java虚拟机中,执行引擎可能有不同的实现。

4.反射的原理

在这里插入图片描述

  • 如上图所示,java源码在编译后被jvm的类加载器加载进内存,经过“反射”这一机制后,java源码中所有的属性(成员变量,构造方法,方法等)在内存中都一一对应,即像镜子一样把源码的内容反射进内存,同时jvm自动创建了一个Class对象,我们可以通过这个Class对象来调用反射进来的属性,通过控制这些属性来间接控制java代码中成员变量,方法,构造方法等。类加载的具体过程和在虚拟机中的具体实现这里不再详细说明,参见深入理解jvm

  • 要说明一下这里java编译后的字节码文件中的各个成员变量,方法,构造方法等在jvm内存中都有对应,成员变量对应Field方法 ,构造方法对应Construct方法,成员方法对应Method。

5.反射的使用

  • 反射的使用分两步,一是将字节码文件加载进内存,返回Class对象。二是调用Class对象的相应方法执行目的操作
步骤一:获取Class对象
  • 方法1:Class.forname("全类名"):将字节码文件加载进内存,返回Class对象。

多用于配置文件,将类名定义在配置文件中。读取文件,加载类

  • 方法2: 类名.class:通过类名的属性class获取

多用于参数的传递

  • 方法3:对象.getClass():getClass()方法在Object类中定义着。

多用于对象的获取字节码的方式

  • 举例说明
 		
public static void main(String[] args) throws Exception {
		//1.Class.forName("全类名")
        Class cls1 = Class.forName("cn.itcast.domain.Person");//全类名=包名+类名(package+.class)
        System.out.println(cls1);
        //2.类名.class
        Class cls2 = Person.class;//通过类名.class方法获得Class对象
        System.out.println(cls2);
        //3.对象.getClass()
        Person p = new Person();//先new一个对象
        Class cls3 = p.getClass();//通过对象.getclass方法获取Class对象
        System.out.println(cls3);
        //== 比较三个Class对象
        System.out.println(cls1 == cls2);//true
        System.out.println(cls1 == cls3);//true  
    //都返回true是因为同一个字节码文件(.class)只会被加载进内存一次,jvm会检测,防止一个字节码文件在内存中加载多次。
}
public class Person {
    各种方法...
}

步骤二:调用Class对象的方法进行相关的目的操作

​ 这个步骤分为两步,第一步先获取要操作的方法,第二步是对获取到的方法进行目的操作。简单说就是先用Class对象调用方法,这会返回相应方法的对象,这就是第一步的先获取要操作的方法。然后要用Construct创建对象,用上一步获得的方法的对象调用相关的方法,将Construct创建的对象传参进去即可。(下面举例说明)

  • Class对象可获取的方法。
  1. 获取构造方法们
* Constructor<?>[] getConstructors()  
* Constructor<T> getConstructor(类<?>... parameterTypes)  

* Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)  
* Constructor<?>[] getDeclaredConstructors()  
    
   
  1. 获取成员变量们
* Field[] getFields() :获取所有public修饰的成员变量
* Field getField(String name)   获取指定名称的 public修饰的成员变量
//带Declared的方法都表示不考虑修饰符,下面的方法也是这样,不再说明
* Field[] getDeclaredFields()  获取所有的成员变量,不考虑修饰符
* Field getDeclaredField(String name)  
  1. 获取成员方法们
* Method[] getMethods()  
* Method getMethod(String name, 类<?>... parameterTypes)  

* Method[] getDeclaredMethods()  
* Method getDeclaredMethod(String name, 类<?>... parameterTypes)  
  1. 获取全类名
String getName()
  • 对获取的方法进行操作
  1. Constructor:构造方法
* 创建对象:
    //在使用构造方法时可以调用newInstance方法创建目的类的对象
		* T newInstance(Object... initargs)  

		* 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法
  1. Field:成员变量
* 操作:
		1. 设置值
			* void set(Object obj, Object value)  
		2. 获取值
			* get(Object obj) 
//在使用带Declared的方法时由于jvm的检查可能会报错,这时可以用下面的方法来忽略访问权限修饰符的安全检查
		3. 忽略访问权限修饰符的安全检查
			* setAccessible(true):暴力反射
  1. Method:方法对象
* 执行方法:
		* Object invoke(Object obj, Object... args)  //obj为new出来的实例化对象,args为方法参数

* 获取方法名称:
		* String getName:获取方法名
举例说明(都是在同一个包下)
  • 先创建一个person类
public class Person {
    private String name;
    private int age;
    public String a;
    public Person() {
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    public void eat(){
        System.out.println("eat...");
    }

    public void eat(String food){
        System.out.println("eat..."+food);
    }
}

  • 对成员变量们的测试类
public class ReflectDemo2 {
    public static void main(String[] args) throws Exception {

        //0.获取Person的Class对象
        Class personClass = Person.class;
        //1.Field[] getFields()获取所有public修饰的成员变量
        Field[] fields = personClass.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }
        System.out.println("------------");
        //2.Field getField(String name)获取由public修饰的名字为a的成员变量
        Field a = personClass.getField("a");
        //获取成员变量a 的值
      	Object p= personClass.newInstance();
        Object value = a.get(p);
        System.out.println(value);
        //设置a的值
        a.set(p,"张三");
        System.out.println(p);
        System.out.println("===================");
        //Field[] getDeclaredFields():获取所有的成员变量,不考虑修饰符
        Field[] declaredFields = personClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            System.out.println(declaredField);
        }
        //Field getDeclaredField(String name)
        Field d = personClass.getDeclaredField("d");
        //忽略访问权限修饰符的安全检查
        d.setAccessible(true);//暴力反射
        Object value2 = d.get(p);
        System.out.println(value2);

    }

}
  • 对构造方法们的测试类
public class ReflectDemo3 {
  public static void main(String[] args) throws Exception {

        //0.获取Person的Class对象
        Class<Person> personClass = Person.class;
     //2. 获取构造方法们
    //Constructor<T> getConstructor(类<?>... parameterTypes)
     //有参构造方法
        Constructor<Person> constructor = personClass.getConstructor(String.class, int.class);
        System.out.println(constructor);
        //创建对象
        Object person = constructor.newInstance("张三", 23);
        System.out.println(person);

        System.out.println("----------");
	//无参构造方法
        Constructor<Person> constructor1 = personClass.getConstructor();
        System.out.println(constructor1);
        //创建对象
        Object person1 = constructor1.newInstance();
        System.out.println(person1);
//无参构造方法可以简化为下面的一步,即直接调用class对象的newInstance()方法
        Object o = personClass.newInstance();
        System.out.println(o);
    
    }
}
  • 对成员方法们的测试类
public class ReflectDemo4 {
    public static void main(String[] args) throws Exception {

        //0.获取Person的Class对象
        Class personClass = Person.class;
        //3. 获取成员方法们:
        //获取指定名称的无参方法
        Method eat_method = personClass.getMethod("eat");
       Object p= personClass.newInstance();
        //执行无参方法
        eat_method.invoke(p);
        //获取指定名称的有参方法
        Method eat_method2 = personClass.getMethod("eat", String.class);
        //执行有参方法
        eat_method2.invoke(p,"饭");
        System.out.println("-----------------");
        //获取所有public修饰的方法
        Method[] methods = personClass.getMethods();
        for (Method method : methods) {
            System.out.println(method);
            String name = method.getName();
            System.out.println(name);
            //method.setAccessible(true);
        }
        //获取全类名
        String className = personClass.getName();
        System.out.println(className);
    }
}

注解(Annotation)

1.什么是注解

  • 注解是从 jdk5 引入的技术,它用来对程序进行声明,本身并不是程序的一部分(从这一点看相当于注释)。它可以声明在包,类,字段,方法,局部变量,方法参数的前面,对这些进行说明注释。
  • 注解的格式:@注解名称(参数),其中的参数值可以存在也可以没有,这要看具体的注解。

2.内置注解

  • java 中预定义了一些注解,主要有@Override(重写),@Deprecated(过时),@SuppressWarnings( 抑制警告 ) 等
  • @Override : 定义在java.lang.Override中,它只能修饰方法,表示下面的方法是从父类或者父接口继承来的,并且要重写。
  • @Deprecated:定义在java.lang.Deprecated中,它可以用来修饰方法,属性,类,表示下面所修饰的方法,属性,类已经过时了,不推荐使用。
  • @SuppressWarnings:定义在java.lang.SuppreddWarnings中,用来抑制编译时的警告信息。与前两个注解不同的是这个注解要添加参数才能使用。

@SuppressWarnings(“all”),@SuppressWarnings(“unchecked”),@SuppressWarnings(value={"uncheck","deprecation"})等等

3.元注解(mete-annotation)

  • 元注解是用来负责注解其他注解的,java提供了四个标准的元注解来对其他注解的类型进行说明。分别是 @Target , @Retention , @Documented , @Inherited
  • @Target:用来描述注解的使用范围,可选参数有TYPE(作用于类上),METHOD(作用于方法上),FIELD(作用于成员变量上)
  • @Retention:表示注解被保留的阶段,可选参数有SOURCE,CLASS,RUNTIME,其中范围由大到小是RUNTIME>CLASS>SOURCE,即RUNTIME就包含了后两者的范围。所以经常用的就是RUNTIME。
  • @Documented:说明注解将被包含在javadoc中
  • @Inherited:说明子类可以继承父类的该注解
package com.annotation; 
import java.lang.annotation.*;
//测试元注解
public class Test2 {
@MyAnnotation 
public void test(){ 
	} 
}
//定义一个注解
@Target(value = {ElementType.METHOD,ElementType.TYPE}) 
@Retention(value = RetentionPolicy.RUNTIME)
@Inherited @Documented
@interface MyAnnotation{
//测试作用域 , 了解@Retention的概念 
}

4.自定义注解

  • 格式:(public) @interface 注解名称{属性列表}
  • 注解本质上就是一个接口,该接口在使用 上面的格式自定义注解时 , 会自动继承java.lang.annotation.Annotation接口 ,同时属性列表里的属性表示该注解要实现的功能,想要实现什么作用,就在属性里自主定义。
  • 属性列表的属性只能在基本类型,Class,String,enum中选,不能随意定义。
  • 属性列表的说明
  1. 定义属性时,如果使用default关键字给属性默认化初始值,则使用注解时可以不用赋值,直接使用即可。
  2. 如果属性列表只有一个属性要赋值且属性名称是value,则value可以省略,直接定义值即可。
  3. 数组赋值时,值使用{}包裹,如果数组中只有一个值,则{}可以省略。
  4. 注解元素必须要赋值,或者default默认值,或者在使用注解时在()里自行赋值。
  • 举例说明
package com.annotation; 
import java.lang.annotation.ElementType; 
import java.lang.annotation.Retention; 
import java.lang.annotation.RetentionPolicy; 
import java.lang.annotation.Target; 
//测试自定义注解
public class Test3 {
    //显示定义值 / 不显示值就是默认值 
    @MyAnnotation2(age = 18,name = "coder",id ,schools ) //各个属性顺序可以变换
    public void test() { 
    }
    //只有一个参数, 默认名字一般是value.使用可省略不写 
    @MyAnnotation3("aaa") 
    public void test2(){
    } 
}
//定义两个注解
@Target(value = {ElementType.METHOD}) 
@Retention(value = RetentionPolicy.RUNTIME)
@interface MyAnnotation2{ 
    //参数类型 , 参数名 
    String name() ; 
    int age() ; 
    int id() default -1; 
    String[] schools() default {"Thu","hnist"}; 
}
@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
@interface MyAnnotation3{
// 参数类型 参数名称 
    String value();
}

注解与反射的结合及应用

  • 在上面我们了解了注解和反射的原理,在javaweb学习中我们为了替代xml的配置方式而使用注解,传统的方式我们用xml配置文件来告诉类时如何运行的,这需要很多行代码,现在我们用注解只需要一行代码就可以完成。用起来很方便,下面通过代码来了解一下内部原理。
  • javaweb中我们写一行注解,jvm会自动执行相关的方法,在平常代码中我们只要写一个stunent类就好了,注解的定义和执行都由内部执行,在我们的角度看来,实现print()方法不用先new一个实例对象再调用方法,只要注解一下,自动执行,十分方便。
//定义一个含参注解

  	@Target(value = ElementType.METHOD)//表示自定义的注解的范围是作用于方法上
	@Retention(value = RetentionPolicy.RUNTIME)//表示注解保留的阶段是runtime,源码运行时有
	public  @interface myannotation{
    	String name() default "coder01";
    	int age() default 20 ;
}

//定义一个student类
public class student {
    public int age;
    public String name;
    public student() {

    }
    public student(int age,String name) {
        this.age=age;
        this.name=name;
    }
    @myannotation(age=10, name="code")//这里使用注解就可以调用下面的print()方法,具体实现过程是由java内部自动完成的。同时使用注解时可以往里面赋参数值也可以使用default的
    public void print(int age,String name){
        System.out.println(age+"----"+name);
    }
    
  //定义一个test01类来演示注解内部的实现原理
    public class text01 {
    public static void main(String[] args) {
        try {
            Class<student> cls=student.class;//把类加载进内存同时获取Class类对象
            System.out.println(cls);//输出全类名:class annotation.student
             Object s=cls.newInstance();//获取到student类的实例化对象
            Method method=cls.getMethod("print", int.class,String.class);
            myannotation   annotation= method.getAnnotation(myannotation.class);//获取方法上的注解
            method.invoke(s,annotation.age(),annotation.name());//调用方法同时将注解的属性值传入
        }
        catch(Exception e) {
            e.printStackTrace();
        }

    }
}


执行结果:

在这里插入图片描述

推荐两篇高质量的关于注解和反射的文章

java3y的反射: mp.weixin.qq.com/s?__biz=MzI…

java3y的注解: mp.weixin.qq.com/s?__biz=MzI…