JAVA高级(六)------ 反射

一、类加载过程

在这里插入图片描述
要知道反射机制,还需要理解类的加载过程。总的来说,类加载的五个过程:加载、验证、准备、解析、初始化。
除了加载(装载)阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。
在这里插入图片描述

(1)装载

加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。有两处需要说明一下:

  1. 字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译而来的.class文件
  2. 类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。(为什么会有自定义类加载器?一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。)

具体的,在加载阶段,虚拟机主要完成三件事:

  1. 通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方式。
  2. 将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域),这里只是结构的转化,不涉及其他操作。
  3. 在方法区中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。(这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。)
(2)验证(链接过程)

类的加载过程后生成了类的java.lang.Class对象,接着会进入链接阶段,连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中。类的连接大致分三个阶段,首先是验证:即验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。
这个验证很好理解,加载过程只是完成了.class文件的导入,但导入的文件是否有效有用符合规范就不能保证了,因此需要验证。

(3)准备(链接过程)

准备就是为类的静态变量、静态常量在方法区分配内存,并为静态变量赋默认初值(0值或null值)。如static int a = 100; 静态变量a就会在准备阶段被赋默认值0。(普通的成员变量是在类实例化时候,随对象一起分配在堆内存中。)

(4)解析(链接过程)

将类的二进制数据中的符号引用换为直接引用。(就像学号和学生姓名映射关系一样,利用学号找到姓名再找到这个学生就是符号引用,学号就是符号;利用姓名直接找到学生就是直接引用)

  • 符号引用(Symbolic References):以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
  • 直接引用:
    直接引用可以是(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)(3)一个能间接定位到目标的句柄。直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
(5)初始化

到了初始化阶段才真正执行Java代码。
类的初始化的主要工作是为静态变量赋程序设定的初值。
如static int a = 100;在准备阶段,a被赋默认值0,在初始化阶段就会被赋值为100。

二、什么是反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
反射就是在运行前只知道这个类名称是啥,在运行时才知道要操作的类是什么,并可以在运行时获取类的完整构造,并调用对应的方法。
反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释。
「正」常的,我们使用某个类时必定知道它是什么类,是用来做什么的。先导入,然后对这个类进行实例化,之后使用这个类对象进行操作。
「反」过来,反射是开始并不知道我要初始化的类对象是什么,无法使用 new 关键字来创建对象。但是由上面类的加载过程可知,类加载器在加载好某个类的.class文件后,都会在方法区中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口,这就为使用这个类提供大门。(Class对象的由来是将class文件读入内存,并为之创建一个Class对象,每个类只有唯一的Class对象)
总的来说,反射是动态加载,也就是在运行的时候才会加载,而不是在编译的时候。
正常导入后再使用如下:

package Reflect;

public class Ball {
    private int price;
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
}
package Reflect;

public class Demo1 {
    public static void main(String[] args) {
        Ball ball = new Ball();
        ball.setPrice(100);
        System.out.println(ball.getPrice());
    }
}

通过JDK 提供的反射 API 进行反射调用如下:

public class Demo1 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 正常调用
        Ball ball = new Ball();
        ball.setPrice(66);
        System.out.println("正常调用" + ball.getPrice());
        // 反射调用
        // (此时只知道这个类叫啥,这个类的内部信息还一无所知)
        Class clz = Class.forName("Reflect.Ball");
        // (通过这个类的Class对象去了解这个类的内部)
        Method setMethod = clz.getMethod("setPrice", int.class);
        Constructor constructor = clz.getConstructor();
        Object object = constructor.newInstance();
        setMethod.invoke(object, 66);
        Method getMethod = clz.getMethod("getPrice");
        System.out.println("反射调用" + getMethod.invoke(object));
    }
}
正常调用66
反射调用66

上面两段代码的执行结果,其实是完全一样的。但是其思路完全不一样,第一段代码在未运行时就已经确定了要运行的类(Ball),而第二段代码则是在运行时通过字符串值才得知要运行的类(Reflect.Ball)。

三、Class对象特点

  1. Class 类的实例对象表示正在运行的 Java 应用程序中的类和接口。也就是jvm中有很多的实例,每个类都有唯一的Class对象。
  2. Class 类没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机自动构造的。也就是说我们不需要创建,JVM已经帮我们创建了。
  3. Class 对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法

四、反射的用途

反射机制允许程序在运行时取得任何一个已知名称的class的内部信息,包括其modifiers(修饰符),fields(属性),methods(方法)等,并可于运行时改变fields内容或调用methods。那么我们便可以更灵活的编写代码,代码可以在运行时装配,无需在组件之间进行源代码链接,降低代码的耦合度;还有动态代理的实现等等。

例如:
在日常的第三方应用开发过程中,经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法。
另外虽然反射并不能方便你去创建一个对象,但会让代码更加灵活,降低耦合,提高代码的自适应能力。

五、反射机制的相关类

类名 用途
Class类 代表类的实体,在运行的Java应用程序中表示类和接口
Field类 代表类的成员变量(成员变量也称为类的属性)
Method类 代表类的方法
Constructor类 代表类的构造方法

Class类

包含以下方法(黄色标注为分界,获得类、类中属性、类中注解、类中构造器、类中方法):

类名 用途
asSubclass(Class clazz) 把传递的类的对象转换成代表其子类的对象
Cast 把对象转换成代表类或是接口的对象
getClassLoader() 获得类的加载器
getClasses() 返回一个数组,数组中包含该类中所有公共类和接口类的对象
getDeclaredClasses() 返回一个数组,数组中包含该类中所有类和接口类的对象,包括private 声明的和继承类
forName(String className) 根据类名返回类的对象
getName() 获得类的完整路径名字
newInstance() 创建类的实例
getPackage() 获得类的包
getSimpleName() 获得类的名字
getSuperclass() 获得当前类继承的父类的名字
getInterfaces() 获得当前类实现的类或是接口
getField(String name) 获得某个公有的属性对象
getFields() 获得所有公有的属性对象
getDeclaredField(String name) 获得某个属性对象
getDeclaredFields() 获得所有属性对象
getAnnotation(Class<?> annotationClass) 返回该类中与参数类型匹配的公有注解对象
getAnnotations() 返回该类所有的公有注解对象
getDeclaredAnnotation(Class<?> annotationClass) 返回该类中与参数类型匹配的所有注解对象
getDeclaredAnnotations() 返回该类所有的注解对象
getConstructor(Class…<?> parameterTypes) 获得该类中与参数类型匹配的公有构造方法
getConstructors() 获得该类的所有公有构造方法
getDeclaredConstructor(Class…<?> parameterTypes) 获得该类中与参数类型匹配的构造方法
getDeclaredConstructors() 获得该类所有构造方法
getMethod(String name, Class…<?> parameterTypes) 获得该类某个公有的方法
getMethods() 获得该类所有公有的方法
getDeclaredMethod(String name, Class…<?> parameterTypes) 获得该类某个方法
getDeclaredMethods() 获得该类所有方法
isAnnotation() 如果是注解类型则返回true
isAnnotationPresent(Class<? extends Annotation> annotationClass) 如果是指定类型注解类型则返回true
isAnonymousClass() 如果是匿名类则返回true
isArray() 如果是一个数组类则返回true
isEnum() 如果是枚举类则返回true
isInstance(Object obj) 如果obj是该类的实例则返回true
isInterface() 如果是接口类则返回true
isLocalClass() 如果是局部类则返回true
isMemberClass() 如果是内部类则返回true

Field类

类名 用途
equals(Object obj) 属性与obj相等则返回true
get(Object obj) 获得obj中对应的属性值
set(Object obj, Object value) 设置obj中对应属性值

Method类

类名 用途
invoke(Object obj, Object… args) 传递object对象及参数调用该对象对应的方法

Constructor类

类名 用途
newInstance(Object… initargs) 根据传递的参数创建类的对象

下面利用代码来尝试调用:

package reflect;

public class Ball {
    private int price;
    public String name;
    private String count;
    public int getPrice() {
        return price;
    }
    public void setPrice(int price) {
        this.price = price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getCount() {
        return count;
    }
    public void setCount(String count) {
        this.count = count;
    }
}
public class Demo3 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class clz = Class.forName("reflect.Ball");
        System.out.println("1、 " + clz.getField("name"));
        System.out.println("2、 " + clz.getName());
        Field fields [] = clz.getFields();
        for (int i = 0; i < fields.length; i++) {
            System.out.println("field: " + i + "、 " + fields[i]);
        }
        Method methods [] = clz.getMethods();
        for (int i = 0; i < methods.length; i++) {
            System.out.println("method: " + i + "、 " + methods[i]);
        }
        System.out.println("3、 " + clz.isArray());
        System.out.println("4、 " + clz.isEnum());
        System.out.println("5、 " + clz.isInterface());
        System.out.println("6、 " + clz.isLocalClass());
        System.out.println("7、 " + clz.isMemberClass());
    }
}
1、 public java.lang.String reflect.Ball.name
2、 reflect.Ball
field: 0、 public java.lang.String reflect.Ball.name
method: 0、 public java.lang.String reflect.Ball.getName()
method: 1、 public void reflect.Ball.setName(java.lang.String)
method: 2、 public void reflect.Ball.setPrice(int)
method: 3、 public int reflect.Ball.getPrice()
method: 4、 public void reflect.Ball.setCount(java.lang.String)
method: 5、 public java.lang.String reflect.Ball.getCount()
method: 6、 public final void java.lang.Object.wait() throws java.lang.InterruptedException
method: 7、 public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
method: 8、 public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
method: 9、 public boolean java.lang.Object.equals(java.lang.Object)
method: 10、 public java.lang.String java.lang.Object.toString()
method: 11、 public native int java.lang.Object.hashCode()
method: 12、 public final native java.lang.Class java.lang.Object.getClass()
method: 13、 public final native void java.lang.Object.notify()
method: 14、 public final native void java.lang.Object.notifyAll()
3、 false
4、 false
5、 false
6、 false
7、 false

六、获取class文件对象的三种方式

可以概括为下面3种:

  1. Object类的getClass()方法
  2. 静态属性class
  3. Class类中静态方法forName()
(1)Object类的getClass()方法
public class Demo2 {
    public void get(Ball b1, Ball b2) {
        // 1. Object类的getClass()方法
        Class clz1 = b1.getClass();
        Class clz2 = b2.getClass();
        System.out.println(b1 == b2);     // false
        System.out.println(clz1 == clz2); // true
    }
}

通常应用在:比如你传过来一个 Object类型的对象,而我不知道你具体是什么类,用这种方法。

(2)静态属性class
public class Demo2 {
    public static void main(String[] args) {
        // 1. Object类的getClass()方法
        Ball b1 = new Ball();
        Class clz1 = b1.getClass();
        Ball b2 = new Ball();
        Class clz2 = b2.getClass();
        System.out.println(b1 == b2);     // false
        System.out.println(clz1 == clz2); // true

        // 2. 静态属性class
        Class clz3 = Ball.class;
        System.out.println(clz1 == clz3); // true
    }
}

该方法最为安全可靠,程序性能更高,这种方法只适合在编译前就知道操作的 Class。

(3)Class类中静态方法forName()
public class Demo2 {
    public static void main(String[] args) throws ClassNotFoundException {
        // 1. Object类的getClass()方法
        Ball b1 = new Ball();
        Class clz1 = b1.getClass();
        Ball b2 = new Ball();
        Class clz2 = b2.getClass();
        System.out.println(b1 == b2);     // false
        System.out.println(clz1 == clz2); // true

        // 2. 静态属性class
        Class clz3 = Ball.class;
        System.out.println(clz1 == clz3); // true

        // 3. Class类中静态方法forName()
        Class clz4 = Class.forName("reflect.Ball");
        System.out.println(clz4 == clz1); // true
    }
}

这里需要注意Class.forName(“reflect.Ball”);时需要ClassNotFoundException异常处理。这种方式用的最多,尤其是知道某类的全路径名时。

七、反射处理步骤

// 1、获取类的 Class 对象实例, 该对象表示正在运行的类和接口
Class clz = Class.forName("reflect.Ball");
// 2、根据 Class 对象实例获取 Constructor 对象
Constructor constructor = clz.getConstructor();
// 3、使用 Constructor 对象的 newInstance 方法获取反射类对象
Object object = constructor.newInstance();
// 4、获取方法的 Method 对象
Method setMethod = clz.getMethod("setPrice", int.class);
// 5、利用 invoke 方法调用方法
setMethod.invoke(object, 100);

前3步就是通过反射创建类对象的过程。

八、反射获得的对象与new对象区别

  1. 在使用反射的时候,必须确保这个类已经加载并已经链接了。因为反射是基于class对象的,由上面类的加载过程可以知道,只有加载后JVM才会创建这个类的class对象。
  2. new关键字可以调用任何public构造方法,而反射只能调用无参构造方法
  3. new关键字是强类型的,效率相对较高。 反射是弱类型的,效率低。

九、Spring中对反射机制的利用

Spring中利用了大量反射,以第七节反射处理步骤为例,Spring中若调用set方法大致会做如下处理:
1、先导入类的路径

bean.xml
<bean id="id" class="reflect.Ball">
    <property name="price" value="100" />
</bean>

2、Spring创建该类的实例,并注入值:

Class c = Class.forName("reflect.Ball");
Object bean = c.newInstance();

3、通过一些操作获取对price对应的setter方法名

String setname = "set" + "Price";
Method method = c.getMehod(setprice,int.Class);
method.invoke(bean,"100");

这样就完成了最基本的注入操作。
当然Spring还可以通过构造函数进行注入,或者Class还可以访问Annotation,这样Spring使用注解时就完成注入的功能。

十、总结

反射机制允许程序在运行时取得任何一个已知名称的class的内部信息,包括包括其modifiers(修饰符),fields(属性),methods(方法)等,并可于运行时改变fields内容或调用methods。
好处是使得代码编写更灵活,代码可以在运行时装配,无需在组件之间进行源代码链接,降低代码的耦合度,但反射使用不当会造成很高的资源消耗!

发布了92 篇原创文章 · 获赞 3 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_41231928/article/details/103400865