深入理解Java中的反射以及案例演示

1 反射

JAVA反射机制是在程序运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

  要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象。
在这里插入图片描述
反射主要应用在一些通用性比较高的代码当中。例如框架:方便程序员开发,框架的底层都是通过反射完成。

1.1 反射的原理

在这里插入图片描述
文字描述:
(1)编写的文件是.java 源文件,保存在磁盘上。
(2)java源文件被编译器编译,生成一个字节码文件。(检查源文件是否存在语法错误,语法错误不能生成字节码文件)。
(3)JVM将字节码文件加载到内存当中。
(4)java是面向对象的,万事万物皆对象。 将字节码文件看成了一个对象,对象的类型就是Class 类型。
(5)获得了Class 类型之后,就可以通过反射的机制,操作文件当中所有的内容:
Java将一个类的成员变量、构造器、成员方法都看作对象并封装到相应的类中,而通过Class对象,调用相应的方法即可获得对应的类的成员变量对象、构造器对象、成员方法对象。然后通过这些对象调用方法,就相当于该类在操作自身的属性、构造器、成员方法!这就是反射!
补充:
  每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。
  类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用Class.forName(“com.mysql.jdbc.Driver”) 这种方式来控制类的加载,该方法会返回一个 Class 对象。
  反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。

1.1.1 反射相关类

  类: Class 表示类;java.lang包
  成员变量:字段—封装到Field对象,可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
  构造器:有参和无参—封装到Constructor对象,可以用 Constructor 的 newInstance() 创建新的对象。
  成员方法:普通方法,静态方法,有参方法—封装到Method对象,可以使用 invoke() 方法调用与 Method 对象关联的方法;
  后三种都位于java.lang.reflect包,而Class位于java.lang包

1.1.2 Class类

  Java程序在运行时,Java运行时系统一直对所有的对象进行所谓的运行时类型标识,即所谓的RTTI。
  这项信息纪录了每个对象所属的类。虚拟机通常使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。Class类封装一个对象和接口运行时的状态,当装载类时,Class类型的对象自动创建。
说白了就是:

  1. Class类也是类的一种,只是名字和class关键字高度相似。Java是大小写敏感的语言。不过二者没什么关系,class不过是描述类的一个关键字。而Class却是保存着运行时信息的类。
  2. Class类的对象内容是你创建的类的类型信息,比如你创建一个shapes类,那么,Java会生成一个内容是shapes的Class类的对象。
  3. Class类的对象不能像普通类一样,以 new shapes() 的方式创建,它的对象只能由JVM创建,因为这个类没有public构造函数
    它能做什么?Class类可以帮助我们在程序运行时分析类,说白了运行时就是获取类中的值。可能瞬间就想到了反射,没错!Class一般就是和反射配套使用的,因为我们向Class提供一个类或一个类的类名,Class就可以提供我们很多信息,比如属性/方法/修饰符/构造器/类名等等。然后我们就可以进一步进行反射。

1.1.2.1 获取Class类对象

获取Class类的三种基本方式:

  1. 类名.class,对基本类型也支持

Class c = int.class; Class c = int[].class; Class c = String.class

  1. 对象.getClass()方法

Class c = obj.getClass();

  1. Class.forName()通过类的全路径名称加载类,这种方法只要有类名称就可以得到Class;

Class c = Class.forName(“cn.itcast.Demo”);

1.1.2.1 Class类相关方法

String getName() //获取类名称,包含包名;
String getSimpleName() //获取类名称,不包含包名;
Class getSupperClass() //获取父类的Class,例如:new Integer(100).getClass().getSupperClass()返回的是Class<Number>!但new Object().getSupperClass()返回的是null,因为Object没有父类;
T newInstance() //使用本类无参构造器来创建本类对象;
boolean isArray() //是否为数组类型;
boolean isAnnotation() //是否为注解类型;
boolean isAnnotationPresent(Class annotationClass) //当前类是否被annotationClass注解了;
boolean isEnum() //是否为枚举类型;
boolean isInterface() //是否为接口类型;
boolean isPrimitive() //是否为基本类型;
boolean isSynthetic() //是否为引用类型;

1.2 反射的操作

操作的类

public class Student {
    private Integer id;
    private String name;

    @Override
    public String toString() {
        return "Student [id=" + id + ", name=" + name + "]";
    }

    public int getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public Student() {
        super();
    }

    public Student(Integer id, String name) {
        super();
        this.id = id;
        this.name = name;
    }

    public static void eat() {
        System.out.println("student can eat");
    }
}

1.2.1 通过反射操作类当中的构造器

1.2.1.1 通过Class类的newInstance方法

1.2.1.1.1 获得字节码文件对象:Class

同一个类的Class文件只有一份,因此它的Class对象也是唯一的。三种方法获取:

对象.getClass();
类名.class;
Class.forName(“类的全路径名称”);

1.2.1.1.2 调用class对象的newInstance方法

public T newInstance()
获得一个通过公共的无参构造器构造的新实例对象,如果没有提供公共的无参数的构造器,则抛出异常!

        // 1.获得class字节码对象
        // 方式一
        Class c1 = Student.class;
        // 方式二
        Class<? extends Student> c2 = new Student().getClass();
        // 方式三
        Class<?> c3 = Class.forName("com.javase.reflect.Student");
        // 2.通过newInstance操作无参构造器创建方法
        Student stu = (Student) c1.newInstance();
        stu.setId(11);
        stu.setName("lisi");
        //Student [id=11, name=lisi]
        System.out.println(stu);

1.2.1.2 通过Constructor类的newInstance方法

1.2.1.2.1 获得一个Class 实例

对象.getClass();
类名.class;
Class.forName(“类的全路径名称”);

1.2.1.2.2 获得对应构造器的Constructor对象
1.2.1.2.2.1 获得公共构造方法表示的Constructor对象

public Constructor getConstructor(Class<?>… parameterTypes)

  返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共构造方法。
parameterTypes 参数是Class对象的一个可变数组,这些Class对象按构造器的声明顺序来标识构造方法的形参类型,参数的类型是Class类型,填写的顺序必须和某个构造器的形参参数顺序相同。
  即 形参类型
  1.class,形参类型
  2.class。必须与构造器形参类型一致,基本类型的形参.class也适用,不能手动转为使用包装类!
  如果底层构造方法所需形参数为 0,则所提供的数组的长度可能为 0 或 传递null,或者不传参数。

public Constructor<?>[] getConstructors()

  返回一个包含某些 Constructor 对象的数组,这些对象反映此 Class 对象所表示的类的所有公共构造方法。如果该类没有公共构造方法,或者该类是一个数组类,或者该类反映一个基本类型或 void,则返回一个长度为 0 的数组。

1.2.1.2.2.2 获得所有构造方法表示的Constructor对象

  即能够获得包括私有在内的所有构造方法的Constructor对象。比较强大,这表示即使该类构造方法私有,也能创建对象,而Calss类的newInstance方法只能操作公共的无参构造方法!

public Constructor<?>[] getDeclaredConstructors()

  返回 Constructor 对象的一个数组,这些对象反映此 Class 对象表示的类声明的所有构造方法。它们是公共、保护、默认(包)访问和私有构造方法。返回数组中的元素没有排序,也没有任何特定的顺序。如果该类存在一个默认构造方法,则它包含在返回的数组中。如果此 Class 对象表示一个接口、一个基本类型、一个数组类或 void,则此方法返回一个长度为 0 的数组。

public Constructor getDeclaredConstructor(Class<?>… parameterTypes)

  返回一个 Constructor 对象,该对象反映此 Class 对象所表示的类或接口的指定构造方法。parameterTypes 参数是 Class 对象的一个数组,它按声明顺序标识构造方法的形参类型。 如果此 Class 对象表示非静态上下文中声明的内部类,则形参类型作为第一个参数包括显示封闭的实例。

1.2.1.2.3 调用Constructor对象的newInstance方法

注意
对于私有构造器的实例化,在调用newInstance方法之前,还需要设置:

con.setAccessible(true); //暴力使该私有构造器打开

  将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。值为 false 则指示反射的对象应该实施 Java 语言访问检查。

public T newInstance(Object… initargs)

  使用此 Constructor 对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。个别参数会自动解包,以匹配基本形参,必要时,基本参数和引用参数都要进行方法调用转换。
  initargs – 可变实参。将作为变量传递给构造方法调用的对象数组;基本类型的值被包装在适当类型的包装器对象(如 Float 中的 float)中。参数的类型必须和获得Constructor时候一致。
  如果底层构造方法所需形参数为 0,则所提供的数组的长度可能为 0 或 null。或者不传参数。
  返回通过调用此对象表示的构造方法来创建的新对象。

        /* 1.获得class字节码对象*/
        // 方式一
        Class c1 = Student.class;
        // 方式二
        Class<? extends Student> c2 = new Student().getClass();
        // 方式三
        Class<?> c3 = Class.forName("com.javase.reflect.Student");
        
        
        /*2.调用方法,获得Constructor对象*/
        Constructor cs = c1.getConstructor(Integer.class, String.class);
        
        
        /*3.调用instance方法创建对象,传入实参*/
        Student stu = (Student) cs.newInstance(123, "张三");
        //Student [id=123, name=张三]
        System.out.println(stu);
1.2.1.2.4 常见异常

  使用getConstructor调用私有构造器:NoSucnMethodException
原因是getConstructor()只能发现并获取公共的构造器
  使用getDeclaredConstructor调用私有构造器,但是不使用setAccessible(true),会抛出:IllegalAccessException:非法的访问异常。 原因是该构造器是私有的,正常情况下肯定不让访问了,此时只能暴力破解了!

1.2.1.2.5 Construcor常用方法
String getName() //获取构造器名;
Class getDeclaringClass() //获取构造器所属的类型;
Class[] getParameterTypes() //获取构造器的所有参数的类型 class;
Class[] getExceptionTypes() //获取构造器上声明的所有异常类型 class;
T newInstance(Object… initargs) //通过构造器反射对象调用构造器。
setAccessible(true)  //使得私有构造器打开
<T extends Annotation> T getAnnotation(Class<T> annotationClass) //如果存在该元素的指定类型的注解,则返回这些注解,否则返回 null。
 Annotation[] getDeclaredAnnotations() //返回直接存在于此元素上的所有注解。

1.2.2 通过反射操作类当中的成员变量

1.2.2.1 获得一个Class 实例

对象.getClass();
类名.class;
Class.forName(“类的全路径名称”);

1.2.2.2 获得对应属性的Filed对象

public Field getDeclaredField(String name)

  返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段。name 参数是一个 String,它是字段的名称。注意,此方法不反映数组类的 length 字段。

public Field[] getDeclaredFields()

  返回 Field 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段。包括公共、保护、默认(包)访问和私有字段,但不包括继承的字段。返回数组中的元素没有排序,也没有任何特定的顺序。如果该类或接口不声明任何字段,或者此 Class 对象表示一个基本类型、一个数组类或 void,则此方法返回一个长度为 0 的数组。

public Field getField(String name)

  返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段。name 参数是一个 String,用于指定所需字段的简称。

public Field[] getFields()

  返回一个包含某些Field对象的数组,这些对象反映此Class对象所表示的类或接口的所有可访问公共字段。返回数组中的元素没有排序,也没有任何特定的顺序。如果类或接口没有可访问的公共字段,或者表示一个数组类、一个基本类型或void,则此方法返回长度为 0 的数组。

1.2.2.3 调用Field的set方法为属性赋值

注意:对于私有属性调用之前还要通过field对象的方法设置操作权限:

public void setAccessible(boolean flag)

  将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。值为 false 则指示反射的对象应该实施 Java 语言访问检查。

1.2.2.3.1 设置属性值

public void set(Object obj,Object value)
  通过合适的方法获得一个类的实例对象:obj
  通过Field对象的set方法给属性设置具体的值:value

        /* 1.获得class字节码对象*/
        Class c1 = Student.class;

        /*2.通过getFeild获得属性对象*/
        // 只能获得public的属性,使用该方法访问private属性将抛出:NoSuchFieldException
        // Field field = c1.getField("name");
        // 获得所有public属性的数组
        // Field[] fields = c1.getFields();
        // 无论是private还是public的属性都能获得
        Field field = c1.getDeclaredField("name"); 
        // 无论是private还是public的属性都能获得
        Field field2 = c1.getDeclaredField("id"); 
        // 获得所有的属性的数组
        // Field[] declaredFields = c1.getDeclaredFields();
        
        // 还需要设置访问权限,可以访问private修饰的属性,否则抛出:IllegalAccessException
        field.setAccessible(true);
        field2.setAccessible(true);

        /*3.获得一个对象实例*/
        Student stu = (Student) c1.newInstance();

        /*4.给该对象设置属性值.obj-实例对象 value-属性的值*/
        field.set(stu, "呵呵");
        field2.set(stu, 222);
        //Student [id=222, name=呵呵]
        System.out.println(stu);  
1.2.2.3.2 Field类的常用方法
String getName() //获取成员变量名;
Class getDeclaringClass() //获取成员变量所属的类的的类型 class;
Class getType() //获取当前成员变量的类型 class;
Object get(Object obj) //获取obj对象的成员变量的值;
void set(Object obj, Object value) //设置obj对象的成员变量值为value;

1.2.3 通过反射操作类当中的成员方法

1.2.3.1 获得一个Class 实例

对象.getClass();
类名.class;
Class.forName(“类的全路径名称”);

1.2.3.2 获得对应方法的Method对象

Method getMethod(String name, Class<?>… parameterTypes)

  返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。 获取本的公共方法

Method[] getMethods()

  返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法。本类以及父类的公共方法

public Method getDeclaredMethod(String name,Class<?>… parameterTypes)

  name - 方法名 parameterTypes – Class类型参数数组,或者以逗号间隔开的class类型,没有形参就可以不提供或者null。
  返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。name 参数是一个 String,它是方法的名称,parameterTypes 参数是 Class 对象的一个数组,它按声明顺序标识该方法的形参类型。
  如果在某个类中声明了带有相同参数类型的多个方法,并且其中有一个方法的返回类型比其他方法的返回类型都特殊,则返回该方法;否则将从中任选一个方法。如果名称是 “” 或 “”,则引发一个 NoSuchMethodException。

public Method[] getDeclaredMethods()

   返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。

1.2.3.3 调用Method的invoke方法

注意:对于私有方法调用之前还要通过method对象的方法设置操作权限:

public void setAccessible(boolean flag)

   将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。值为 false 则指示反射的对象应该实施 Java 语言访问检查。

public Object invoke(Object obj,Object… args)

   obj–实例对象 …args–用于方法调用的实参
   对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。个别参数被自动解包,以便与基本形参相匹配,基本参数和引用参数都随需服从方法调用转换。
   如果底层方法是静态的,那么可以忽略指定的 obj 参数。该参数可以为 null。
   如果底层方法所需的形参数为 0,则所提供的 args 数组长可以为 0 或 null,甚至可以不提供。
   如果底层方法是静态的,并且尚未初始化声明此方法的类,则会将其初始化。
   如果方法正常完成,则将该方法返回的值返回给调用者;如果该值为基本类型,则首先适当地将其包装在对象中。但是,如果该值的类型为一组基本类型,则数组元素不被包装在对象中;换句话说,将返回基本类型的数组。如果底层方法返回类型为 void,则该调用返回 null,打印值将会在方法调用完成后自动打印到控制台。

1.2.3.3.1 操作无返回值的方法
        /* 1.获得class字节码对象*/
        Class c1 = Student.class;

        /*2.获得method实例 name-方法名称 Class<?>...*/
        // parameterTypes-方法的参数类型,和各参数类型以逗号间隔,或者传入参数类型数组
        Method declaredMethod = c1.getDeclaredMethod("setId", Integer.class);

        /*3.获得一个实例对象*/
        Student stu = (Student) c1.newInstance();

        /*4.invoke*/
        //对传入对象,调用此method对象表示的底层方法.obj-实例对象 ; ... args-用于方法调用的实际参数 
        //如果底层方法是静态的,那么可以忽略指定的 obj 参数。该参数可以为 null。
        //如果底层方法所需的形参数为0,则所提供的args数组长度可以为0或 null。
        declaredMethod.invoke(stu, 12);
        //Student [id=12, name=null]null
        System.out.println(stu);
1.2.3.3.2 操作有返回值的方法
        Class c1 = Student.class;
        Student stu = (Student)c1.newInstance();
        Method declaredMethod2 = c1.getDeclaredMethod("setId", Integer.class);
        declaredMethod2.invoke(stu, 12);
        Method declaredMethod = c1.getDeclaredMethod("getId");
        Integer invoke = (Integer)declaredMethod.invoke(stu);
        System.out.println(invoke);    //12
1.2.3.3.3 操作静态方法
        Class c1 = Student.class;
        Student stu = (Student) c1.newInstance();
        Method declaredMethod = c1.getDeclaredMethod("eat");
        declaredMethod.invoke(null);

1.2.3.4 Method常用方法

String getName() //获取方法名;
Class getDeclaringClass() //获取方法所属的类的类型 全路径名;
Class[] getParameterTypes() //获取方法的所有参数的类型;
Class[] getExceptionTypes() //获取方法上声明的所有异常类型;
Class getReturnType() //获取方法的返回值类型;
Object invode(Object obj, Object… args) //通过方法反射对象调用方法,如果当前方法是实例方法,那么当前对象就是obj,如果当前方法是static方法,那么可以给obj传递null。args表示是方法的参数;

<T extends Annotation> T  getAnnotation(Class<T> annotationClass)   //如果存在该元素的指定类型的注释,则返回这些注释,否则返回 null。 
 Annotation[] getDeclaredAnnotations()   //返回直接存在于此元素上的所有注释。

1.2.4 通过反射操作注解

获取类、方法、字段上的注解:

Annotation[] annotations = class.getAnnotations(); Annotation[]
annotations = method.getAnnotations(); Annotation[]
declaredAnnotations = field.getDeclaredAnnotations();

获取单个注解

Annotation annotation = aClass.getAnnotation(Annotation.Class);

1.2.5 AccessibleObject

   AccessibleObject类是Constructor、Method、Field三个类的父类。它提供了将反射的对象标记为在使用时取消默认Java语言访问控制检查的能力。AccessibleObject最为重要的方法如下:

boolean isAccessible():判断当前成员是否可访问;
void setAccessible(boolean flag):设置当前成员是否可访问。
当Constructor、Method、Field为私有时,如果我们想反射操作,那么就必须先调用反射对象的setAccessible(true)方法,然后才能操作。

1.2.6 练习

给一个泛型类型为Integer的ArrayList集合添加一个字符串!

public class ArrayListDemo {
    public static void main(String[] args) throws Exception {
        ArrayList<Integer> in = new ArrayList<Integer>();
        /*获取ArrayList的Class对象*/
        Class<? extends ArrayList> aClass = in.getClass();
        /*反射获取add方法对象*/
        Method add = aClass.getDeclaredMethod("add", Object.class);
        /*反射调用add方法*/
        Object hello = add.invoke(in, "hello");
        //返回true
        System.out.println(hello);
        //[hello],添加成功
        System.out.println(in);
    }
}

  通过该实例我们知道,反射可以绕过泛型检查而做出一些不可思议的事情。
  实际上泛型只是给编译器检查用的,只是在编译期间检查类型是否一致,其底层源码并不知道需要限定只能添加Integer类型。
  因为查看add方法的底层源码为add(E e),我们知道E就代表object类型,而泛型不可能改变源码。
  我们用反射绕过了编译器的泛型检查,直接调用源码,而其底层源码又支持添加Object类型,自然可以添加String类型!但是这样的操作建议慎用!

发布了58 篇原创文章 · 获赞 105 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43767015/article/details/105140480
今日推荐