欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

秒懂Java反射

程序员文章站 2022-04-24 10:22:40
...

版权申明】非商业目的注明出处可*转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/81809999
出自:shusheng007

前言

前段时间看 Retrofit2源码 的时候,发现其大量使用了反技术,在此框架中使用反射技术来获取方法以及其参数的注解。虽说反射技术在我们日常的开发当使用不是很频繁,但是其在构建框架则会大放异彩。反射技术应该也算是Java进阶的知识了,对有追求的Java程序员来说是必须要掌握的一项技能。

概述

什么是反射?解决什么问题?具体如何使用?其是什么原理?有什么弊端?

什么是反射

In computer science, reflection is the ability of a computer program to examine, introspect, and modify its own structure and behavior at runtime.
在计算机领域,反射是一种计算机程序在程序运行时检查,自省,改变其结构和行为的能力。(自省的意思是:可以在运行时检查一个类的类型,属性等信息)

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

解决什么问题

一项技术能解决什么问题,关键是要从其具有的特性出发去探寻。我们知道,反射的特性就是具有动态,那么在任何需要在运行时对类型做操作的场景下都可以考虑反射。

1.可以根据配置文件来动态生成相应的类型对象。

例如我们现在要在mysqloracle数据库直接切换,如何不使用反射我们就需要在代码中预先写好所有的分支逻辑,而通过反射我们就可以使用配置文件,然后动态生成相应的类,这样就大大增强了可扩展性。例如现在产品经理要求接入SqlServer数据库,如果不使用反射就得改代码了,然后重新编译打包。

2.生成动态代理

3.可以突破一些sdkAPI接口限制。

例如在日常开发当中想要访问一个类的私有字段,私有方法等。
等等场景。

什么原理

Java程序是运行在JVM中的,使得我们可以拿到运行时的类型信息,所以我们就可以对这些类型信息做动态的处理了。要想比较深入的理解Java反射的原理,首先要对Java虚拟机机制以及类加载机制有一定的理解。

Java运行机制

众所周知Java程序是运行在JVM上的,我们简单的可以把JVM 理解成一个运行在操作系统上的单独的进程。我们写好一个java类 HelloWorld.java,首先要使用Java编译器将其编译为ByteCode(字节码) 存放在.class格式的文件中。然后Java虚拟机通过类加载器.class文件加载到虚拟机中,为HelloWorld 类生成一个对应的Class 对象,接着就可以执行相关的操作了。

Java跨平台能力就是通过这个机制实现的,因为所有的java程序都是跑在Java虚拟机上的,所以我们只需要按照《Java虚拟机规范》开发相应平台的虚拟机就好了,那样一个java程序开发出来就可以不加修改和编辑直接运行在不同的平台上了。例如Java虚拟机HotSpot同时存在Windows,Linux,Mac 三个平台的版本,那么我们这个helloword java程序就实现了跨平台。

类加载机制

其实将反射的话,理解Java运行机制就足够了,此处稍微深入一点,毕竟类的加载是反射的前提。

类的加载

类从被加载到虚拟机内存开始,到卸载出内存为止,声明周期包括:加载——连接——初始化——使用——卸载。其中连接又包含验证,准备,解析三个步骤。

如下图所示,图片来源于周志明的《深入理解Java虚拟机》一书
秒懂Java反射
1: 加载

通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

上面是《Java虚拟机规范》中对加载的规定,可以简单理解为使用类加载器将class文件载入虚拟机内存。

2:验证
验证的目的是为了确保 class 文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。

文件格式验证:要验证的字节流是否符合Class文件格式的规范。例如Class文件格式要求以魔数0xCAFEBABE开头,那么如果我们的字节流不符合这个规定就会验证失败。
元数据的验证:对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。例如类A是使用final修饰的类,那么我们知道这个类是不允许继承的,但是如果我们的字节流中包含了继承至A类的B类,那么此步骤就会验证失败。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如我们将String类型的数据赋值给int等操作。
符号引用验证:要明白符号引用验证首先需要明白什么是符号,我们这里简单的描述一下,例如你在class文件中使用java.lang.String这样一个符号代表一个你要引用的类,那么虚拟机就需要在解析的时候将这个符号java.lang.String解析成直接引用,例如解析出这个类在虚拟机内存中的位置等。

3.:准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,这些变量所在使用的内存都将在方法区中进行分配。即为使用static修饰的类变量分配内存及初始化为默认值。

4: 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。由于Java是在类加载中动态连接的,所以class文件中不
保存方法及字段的内存布局信息,因此这些字段和方法需要通过解析这一步才能获得真正的内存直接入口地址。

5:初始化
类初始化是类加载过程的最后一个阶段,到初始化阶段,才真正开始执行类中的 Java 程序代码。

类加载器

从开发者的角度来看,类加载器分为4
秒懂Java反射

  1. 启动类加载器(Bootstrap ClassLoader):负责加载Java的核心类库。即%JAVA_HOME%/lib路径下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库。sunHotSpot虚拟机的启动类加载器由 C++ 实现,不是 ClassLoader 子类,无法被 Java 程序直接引用的。
  2. 扩展类加载器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader实现,负责加载Java平台中扩展功能的一些jar包,%JAVA_HOME%/jre/lib/ext或由 java.ext.dirs指定目录下的 jar 包。开发者可以直接使用扩展类加载器。
  3. 应用程序类加载器(Application ClassLoader):由 sun.misc.Launcher$AppClassLoader 来实现,负责加载用户类路径(ClassPath ) 中指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

如何使用

反射的一般使用较为简单,首先获得Class对象,然后根据要获取的操作信息,例如获取类的构造函数,方法,属性等调用相应的方法即可。

其中Class代表对象;Constructor 代表构造函数;Method 代表方法;Field代表字段。

现在假设我们有如下一个类

package top.ss7.reflect;

/**
 * Created by shusheng007 on 2018/8/19.
 */
@ProGender(gender = "女")
public class Programmer {
    private String name;
    private String proLang;

    public String pmName;

    public Programmer(){
        System.out.println("public 无参数构造器");
    }

    private Programmer(int num){
        System.out.println("private 一个Int类型参数构造器");
    }

    public Programmer(String proLang){
        this.name="ss007";
        this.proLang=proLang;
        System.out.println("public 一个String类型参数构造器");
    }

    public Programmer(String name,String proLang){
        this.name=name;
        this.proLang=proLang;
        System.out.println("public 两个String类型参数构造器");
    }


    public void work(){
        System.out.println(String.format("%s:在使用 %s 开发语言工作。", name,proLang));
    }

    private void workForModifiedDemand(String pName){
        System.out.println( pName+" fuck you PM: "+pmName);
    }
}

注解类ProGender

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ProGender {
    String gender() default "男";
}

获取Class对象

有三种方式获取一个类的Class对象:
1. 调用某个类的class属,例如 Programmer.class即可返回Programmer 类的Class对象;
2. 使用Class类的静态方法 Class.forName("类全限定名"),这种方式使用还是比较多的,可以通过一个字符串来生成相应的对象。
3. 调用某个对象的getClass()方法,该方法是java.lang,Object里面的方法,所以所有的Java对象都可以调用。

如果三种都可以使用的情况下,优先使用第一种方式。一旦多的Class对象,就可以调用其方法来获取此对象的真是信息了。下面代码演示了这三种方法。

Class<Programmer> pClass=Programmer.class;

try {
    Class<?> pClass2=Class.forName("top.ss7.reflect.Programmer");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}  

Programmer p=new Programmer();
Class<?> pClass3=p.getClass();

获取构造函数

我们知道一个类可以有很多构造函数,分为无参数构造器,与有参数构造器。

获取无参数构造器:

public T newInstance()

获取有参数public构造器:

public Constructor<T> getConstructor(Class<?>... parameterTypes)

获取有参数pivate构造器:

public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)

如下实例所示

    public static void main(String[] args) {
        Class<Programmer> pClass=Programmer.class;
        try {
            //获取无参数公有构造函数
            Programmer programmer1=pClass.newInstance();

            //获取含有一个String类型参数的公有构造函数
            Constructor pCon=pClass.getConstructor(String.class);
            Programmer programmer2= (Programmer) pCon.newInstance("Java");

            //获取含有一个int类型参数的私有构造函数
            Constructor pCon2=pClass.getDeclaredConstructor(int.class);
            pCon2.setAccessible(true);
            Programmer programmer3= (Programmer) pCon2.newInstance(10);

            //获取含有两个String类型的公有构造函数
            Constructor pCon3=pClass.getConstructor(String.class,String.class);
            Programmer programmer4= (Programmer) pCon3.newInstance("王二狗","C++");
        } 
        ...
    }

运行结果为:

public 无参数构造器
public 一个String类型参数构造器
private 一个Int类型参数构造器
public 两个String类型参数构造器

关于获取构造函数的方法还有几个,可以参考Java Api

获取方法

方法的获取与构造函数的获取非常相似,只是可以使用方法名称来增强指向性。

获取public方法:

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

获取private方法:

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

第一个参数为方法名称,第二个参数为方法参数类型的数组。

我们的Programmer类有两个方法,一个是公有的一个是私有的,调用如下:

 //获取含有两个String类型的公有构造函数
 Constructor pCon3=pClass.getConstructor(String.class,String.class);
 Programmer programmer4= (Programmer) pCon3.newInstance("王二狗","C++");

 //调用公有方法
 Method m1=pClass.getMethod("work");
 m1.invoke(programmer4);

//调用私有方法
 Method m2=pClass.getDeclaredMethod("workForModifiedDemand",String.class);
 m2.setAccessible(true);
 m2.invoke(programmer4,"王二狗");

执行结果为:

王二狗:在使用 C++ 开发语言工作。
王二狗 fuck you PM: null

可以看到王二狗要fuck的那个产品经理为null,那是因为我们还没有为那个字段赋值,接下来就看一下如何通过反射操作Field

获取属性

属性的获取就更简单了,直接使用属性名称获取。

获取public属性:

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

获取private属性:

 public Field getDeclaredField(String name)

方法的参数为属性名,现在我们访问Programmer类的两个属性,并对其中一个赋值

//获取含有两个String类型的公有构造函数
Constructor pCon3=pClass.getConstructor(String.class,String.class);
Programmer programmer4= (Programmer) pCon3.newInstance("王二狗","C++");
//设置公有属性的值
Field f1=pClass.getField("pmName");
f1.set(programmer4,"牛翠花");

//访问私有属性的值
Field f2=pClass.getDeclaredField("name");
f2.setAccessible(true);
System.out.println("程序员名称被设置为:"+f2.get(programmer4));

输出结果为:

程序员名称被设置为:王二狗
王二狗 fuck you PM: 牛翠花

获取注解

我通过反射也可以获取到各种注解(Annotation),例如此例中我们简单的获取一下Programmer类上面的注解信息

ProGender pAno= pClass.getAnnotation(ProGender.class);
System.out.println("程序员性别为:"+pAno.gender());

其实通过反射还可以得到方法参数的信息,有兴趣的同学可以研究。

弊端

一个字:慢!
至于为什么慢,大家提及最多的就是不使用反射是
1:编译器会优化对象实例化的过程,而使用反射则完全不优化,那么实例化对象这块就会有比较大的差距。
2:查找检查等操作上的差距

总结

关于反射,能不用则不用,一有性能问题,二有复杂度问题。技术无好坏关键是适不适合当前使用场景。

范冰冰固然漂亮,但是不一定适合你,什么?你说不试一试怎么知道,那这个试错成本你承受的了吗?如果你一定要坚持就先拿5千万出来,我找王婆帮你联系。。。,记住是$

希望广大程序员生活愉快,编码愉快!