IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 高薪程序员&面试题精讲系列24之你熟悉反射吗? -> 正文阅读

[Java知识库]高薪程序员&面试题精讲系列24之你熟悉反射吗?

一. 面试题及剖析

1. 今日面试题

今天 壹哥 带各位来复习一个非常牛逼的技术--反射!虽然反射在我们开发时用的很多,但关于反射的面试题,出现的频率倒并不算很多,一般就是问问我们对反射是否了解,如下:

你熟悉反射吗?

反射有哪些常用API?

反射是怎么使用的?

......

2. 题目剖析

刚才 壹哥 跟各位说,反射是一个很牛逼的技术,其实一听名字就感觉和别人不一样对吧,而反射自身的能力也对得起它的名字,确实很厉害!

那你可能会问,反射这么牛逼,我怎么没听过?我怎么不会用?如果真是这样的话,那只能说明你的Java还没学到位。

比如Java中的各种框架,无论是著名的还是不著名的,几乎每种框架,包括Java本身的内部实现中,反射的身影都随处可见。甚至可以说反射就是框架设计的灵魂,没有反射,很多框架都无法被设计出来!

举个栗子,假如面试官问你是否熟悉Spring框架的底层源码,你告诉面试官自己对Spring源码非常熟悉,结果你连反射这个概念都不知道,我相信很多面试官都会怀疑你到底有没有看过Spring的源码!

而且反射这种技术,简直就是“位面之子”,几乎就没有他不能干的。甚至类中私有的变量,一般情况下我们是拿不到的,但是反射中随意的一个getDeclaredField()方法就能拿到私有属性;一般的接口不能实例化,利用反射可以轻易生成一个生成代理Class对象,通过反射就能拿到所有想拿的信息。

也就是说,在反射面前,一个类几乎就是透明的存在,毫无秘密可以,Java中把这种可以“看透 class”的能力称为内省,这种能力在框架开发中尤为重要。

另外,虽然反射很牛逼,但是使用起来其实并不难,它的API并不是很多,而且使用起来一般也有固定的模式。但是对于初学者而言,反射最难得的地方不在于怎么用,而是不理解为什么要有反射,以及到底什么时候用反射,其实也就是不理解反射的作用。

所以面试官有时候就会通过我们对反射的掌握情况,来判断我们对Java面向对象的深入理解。

二. 反射简介

既然反射这么牛逼,我们还是赶紧来看看吧,都有点迫不及待了。

1. 概念

首先我们来回顾一下反射的概念,在给面试官介绍反射的时候,肯定是先给人家说一下什么是反射。我们来看看百度百科上对反射的介绍:

Java的反射(reflection)机制 是指 在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种 动态获取程序信息以及动态调用对象的功能 称为Java语言的反射机制。反射被视为动态语言的关键。

根据上面的的描述信息,壹哥 给各位提取出了几个关键信息,如下:

运行状态

任意操作

动态获取

动态调用

......

我之所以说反射牛逼,就是因为反射与我们正常创建对象的方式不一样。至于怎么不一样的,不要着急,我们一点点往下看。

2. 作用

其实根据上面对反射概念的描述,我们也能提取出反射的作用来。

反射的主要功能有:

在运行时判断任意一个对象所属的类;

在运行时构造任意一个类的对象;

在运行时判断任意一个类所具有的成员变量和方法;

在运行时调用任意一个对象的方法;

生成动态代理对象

.....

一句话,通过反射机制,我们可以利用class字节码反向创建出该字节码对应的类对象,并且可以反向调用该对象的各种属性和方法,包括私有的属性与方法。使用反射的这些特性,我们就可以获得与Java类进行动态交互的能力,在开发时实现类的动态加载,也可以实现自定义注解等功能。

等等!有的同学在这里开始提问了,难道反射也可以创建对象?

是的!反射的一个比较核心的功能真的就是用来创建对象的!

但是Java中创建对象不都是通过new的方式来创建的吗?我们都知道,想要对象,就靠new!

其实呢,Java中创建对象的方式有两种,即“正常”的new方式,和“反常”的反射方式。两者区别如下:

new创建对象的方式属于静态编译,而反射属于动态编译,反射只有到运行时才能去获得该对象的实例。

我们可以这么理解,反射就好比是"逆向工程",比如你摆我面前一架战斗机,我就能给你进行反向破解,创造出一架新的战斗机,甚至还可以推陈出新搞出更先进的战斗机,是不是有点“我兔”的风格。当然这个比喻并不是很恰当,实际上反射的功能更强大。我们一点点往下看吧。

3. 意义

那么反射存在的意义到底有哪些呢?

首先,反射机制极大的提高了程序的灵活性和扩展性,降低了模块的耦合性,提高了自身的适应能力;

其次,通过反射机制可以让程序创建和控制任何类的对象,无需提前硬编码目标类;

再次,使用反射机制能够在运行时构造一个类的对象、判断一个类所具有的成员变量和方法、调用一个对象的方法;

最后,反射机制是构建框架技术的基础所在,使用反射可以避免将代码写死在框架中。

正是反射有以上的特征,所以它能动态地编译和创建对象,极大的激发了编程语言的灵活性,强化了多态的特性,进一步提升了面向对象编程的抽象能力,因而受到编程界的青睐。

4. 缺点

虽然反射机制带来了极大的灵活性及方便性,但反射也有缺点。反射机制的功能非常强大,但不能滥用,能不使用反射时,尽量不要使用,原因如下:

  1. 性能问题:Java反射机制中包含了一些动态类型,Java虚拟机是不能够对这些动态代码进行优化的。这就导致反射操作的效率要比正常操作的效率低很多。如果我们对程序的性能要求很高,就要尽量避免使用反射。
  2. 安全限制如果一个程序对安全性提出要求,则最好不要使用反射。
  3. 程序健壮性:有些平时不被允许的操作,利用反射就可以实现,比如访问某个类的私有成员,但这样的操作会破坏Java程序结构的抽象性。所以当程序运行的平台发生变化时,由于抽象的逻辑结构不能被识别,代码产生的效果会与之前会产生差异,有可能会导致意想不到的后果。

三. 反射API

在上面的章节中,壹哥 带各位复习了反射的各种理论概念,接下来我再带各位复习一下反射的常用API有哪些,以及如何进行代码实现。

1. 获取字节码的3种方式

实现反射操作的前提,就是先得获取Class字节码,在Java中一共给我们提供了3种获取字节码的方式。

通过 对象.getClass()方法 来获取字节码,比如 teacher.getClass()。该方式适用于方法中传递进来了一个引用类型的参数,我们可以利用这个参数获取字节码;

//通过getClass()方法获取字节码
Teacher teacher=new Teacher(1,"一一哥",35);
Class<? extends Teacher> clazz = teacher.getClass();

通过 类名.class 来获取字节码,比如 Teacher.class。该方式常用于各种方法调用时作为Class类型的参数传入;

//通过类名.class来获取字节码
Class<Teacher> clazz = Teacher.class;
//Spring中获取字节码对应类的对象
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
Teacher teacher = ctx.getBean("teacher", clazz);

通过 Class.forName(类的全路径) 来获取字节码,比如 Class.forName("java.lang.String")。该方式适用于对非本包里的类进行反射操作,比如有个类在别的包路径下。

try {
    //通过Class.forName("类的全路径")来获取字节码
    Class<?> clazz = Class.forName("com.yyg.interview.entity.Teacher");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

以上3种方式,每种方式都有自己的使用场景,彼此之间其实并无优劣。

2. 反射的组成

反射最终也必须有类参与,Java中负责反射的类都位于java.lang.reflect包里,反射的实现一般有下面几个类来组成:

①.java.lang.reflect.AccessibleObject
②.java.lang.reflect.Constructor<T>
③.java.lang.reflect.Field
④.java.lang.reflect.Method

其中AccessibleObject 是另外三个类的基类,如下图所示:

我们在进行反射操作时,主要也就是操作这几个类。接下来 壹哥 就带各位细细的看看这些API如何操作。

3. 创建类对象

通过反射创建类对象主要有两种方式:

通过 Class 对象的 newInstance() 方法;

通过 Constructor 对象的 newInstance() 方法。

3.1 通过 Class 对象的 newInstance() 方法创建对象

首先我们可以用上面提到的3种获取字节码方式之一,得到一个Class字节码,然后调用newInstance()方法创建一个Teacher类对象。

//通过Class.forName()获取字节码
Class<?> clazz = Class.forName("com.yyg.interview.entity.Teacher");
            
//调用newInstance()方法创建Teacher类对象   
Teacher teacher = (Teacher) clazz.newInstance();

3.2 通过 Constructor 对象的 newInstance() 方法创建对象

另外我们知道,一个类中的构造方法可能会有好几个,所以我们在通过 Constructor 对象创建类对象时,可以选择某个特定的构造方法,而通过 Class 对象则只能使用默认的无参数构造方法。下面的代码就调用了一个有参数的构造方法进行了类对象的初始化。

4. 获取类的公开方法

利用clazz.getMethods()方法,可以获取类的所有公开方法。

public Method[] getMethods()

该方法会返回一个包含clazz类中公开的 Method对象的数组,这些Method对象代表了此 Class字节码对象所表示的类或接口(包括那些由该类或接口声明的,以及从超类和超接口继承的类或接口)中的公共成员方法

使用示例代码:

    public static void main(String[] args) {
        try {
            //通过Class.forName()获取字节码
            Class<?> clazz = Class.forName("com.yyg.interview.entity.Teacher");
            //调用newInstance()方法创建Teacher类对象
            Teacher teacher = (Teacher) clazz.newInstance();

            Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                System.out.println("method=" + method.getName());
            }
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

5. 获取指定名称和参数类型的公开方法

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

该方法会返回一个包含clazz类中公开的 Method对象的数组,这些Method对象代表了此 Class字节码对象所表示的类或接口(包括那些由该类或接口声明的,以及从超类和超接口继承的类或接口)中的公共成员方法

使用示例代码:

    public static void main(String[] args) {
        try {
            //通过Class.forName()获取字节码
            Class<?> clazz = Class.forName("com.yyg.interview.entity.Teacher");
            //调用newInstance()方法创建Teacher类对象
            Teacher teacher = (Teacher) clazz.newInstance();

            //获取名称是“setId”,且参数类型是Integer的方法。
            Method setId = clazz.getMethod("setId", Integer.class);
            //通过反射执行setId()方法,并传入参数100
            setId.invoke(teacher, 100);
            System.out.println("id="+teacher.getId());//id=100
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

注:

invoke()方法释义:

我们获取Method方法对象后,一般都要执行该Method方法,此时需要调用invoke()方法来进行执行。invoke()方法的第一个参数表示调用Method方法的目标对象,比如本案例中的teacher对象;第二个参数表示要传递给Method方法的实参,如果有多个参数,可以使用数组。

另外如果Method方法是静态的,或底层方法所需的形参数为 0,那么可以忽略指定的 obj 参数,该参数可以为 null。

6. 获取类的所有方法

上面的getMethods()方法,只能返回类中公开的方法,不能返回私有的等方法,我们可以使用如下方法来获取私有方法。

public Method[] getDeclaredMethods() 

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

使用示例代码:

    public static void main(String[] args) {
        try {
            //通过Class.forName()获取字节码
            Class<?> clazz = Class.forName("com.yyg.interview.entity.Teacher");
            //调用newInstance()方法创建Teacher类对象
            Teacher teacher = (Teacher) clazz.newInstance();

            Method[] declaredMethods = clazz.getDeclaredMethods();
            for (Method method : declaredMethods) {
                System.out.println("method=" + method.getName());
            }

        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

7. 获取指定名称和参数类型的所有方法

同样的,我们也可以获取类中指定名称和参数类型的所有方法。

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

该方法会返回一个 Method 对象,该对象代表了此 Class 对象所表示的类或接口中指定名称和参数类型的所有方法。当我们去获取类中的方法、类构造器、属性时,如果要获取私有的方法或私有的构造器、私有的属性,则必须使用带有 declared 关键字的方法。

使用示例代码:

    public static void main(String[] args) {
        try {
            //通过Class.forName()获取字节码
            Class<?> clazz = Class.forName("com.yyg.interview.entity.Teacher");
            //调用newInstance()方法创建Teacher类对象
            Teacher teacher = (Teacher) clazz.newInstance();
            
            Method setName = clazz.getDeclaredMethod("setName", String.class);
            //如果是私有方法,执行之前需要设置该方法,表示进行暴力反射。
            setName.setAccessible(true);
            setName.invoke(teacher, "一一哥");
            System.out.println("name=" + teacher.getName());//name=一一哥

        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

注意:

如果我们反射执行类中的私有方法,私有方法被执行之前,需要通过setAccessible(true)方法设置访问权限,或者被称为“暴力反射”,该设置会允许通过反射来访问类的私有变量。这个设置是针对私有变量而言的,public和protected等都不需要。如下所示:

setName.setAccessible(true);

8. 获取公开的成员变量

我们可以获取类中的方法,当然也可以获取类中的属性(成员变量)。我们通过 Class 对象的 getFields() 方法可以获取 Class 类的属性,但无法获取私有属性。

public Field[] getFields()

返回一个包含某些 Field 对象的数组,这个数组代表了这个 Class 对象所表示的类或接口中所有可访问的公共字段。如果该 Class 对象表示一个类,则此方法会返回该类及其所有超类中的公共字段。如果该 Class 对象表示一个接口,则此方法会返回该接口及其所有超接口中的公共字段。

另外我们还可以使用getField(String name)方法来获取指定名称的公共属性。

public Field getField(String name)

返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段。

使用示例代码:

    public static void main(String[] args) {
        try {
            //通过Class.forName()获取字节码
            Class<?> clazz = Class.forName("com.yyg.interview.entity.Teacher");
            //调用newInstance()方法创建Teacher类对象
            Teacher teacher = (Teacher) clazz.newInstance();

            //获取公开属性
            //Field[] fields = clazz.getFields();
            
            //获取所有属性
            Field[] fields = clazz.getDeclaredFields();
            for(Field field : fields){
                System.out.println("field="+field.getName());
            }

            //获取指定名称的公共属性
            //注意:当私有属性不存在时,可能会产生NoSuchFieldException异常
            //Field nameField = clazz.getField("name");
            
            //获取指定名称的所有属性
            Field nameField = clazz.getDeclaredField("name");
            System.out.println("nameField="+nameField.getName());

        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

9. 获取所有的成员变量

上面的方法用来获取类中公开的属性,但是不能获取私有的属性,我们可以使用 Class 对象的 getDeclaredFields() 方法,获取包括私有属性在内的所有属性。

public Field[] getDeclaredFields()

返回 一个 Field 对象数组,这个数组代表了这个 Class 对象所表示的类或接口中所声明的所有字段。包括公共、保护、默认(包)访问和私有字段,但不包括继承的字段

public Field getDeclaredField(String name)

示例代码:

    public static void main(String[] args) {
        try {
            //通过Class.forName()获取字节码
            Class<?> clazz = Class.forName("com.yyg.interview.entity.Teacher");
            //调用newInstance()方法创建Teacher类对象
            Teacher teacher = (Teacher) clazz.newInstance();

            //注意:当私有属性不存在时,可能会产生NoSuchFieldException异常
            Field nameField = clazz.getDeclaredField("name");
            //使用私有属性之前,需要设置可访问性
            nameField.setAccessible(true);
            nameField.set(teacher,"一一哥");
            System.out.println("name value="+nameField.get(teacher));//name value=一一哥

        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

10. 获取构造器

除了可以获取类中的属性、方法之外,我们还可以获取类中的构造方法,如下:

public Constructor<?>[] getDeclaredConstructors() 

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

public Constructor<?>[] getConstructors() 

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

构造器和上面的方法、属性使用过程大同小异,这里就不再一一展示了。

但需要注意的是,当返回Constructor数组时,返回类型是Constructor<?>[],而不是Constructor<T>[]!因为此方法返回之后,该数组可能会被修改以保存不同类的 Constructor 对象。

11. 获取接口信息

另外,我们也可以利用Class字节码来获取类的接口信息的,方法如下:

Class[] interfaces = clazz.getInterfaces();

因为一个类可以实现多个接口,所以getInterfaces()方法返回的是Class[]数组。

注意:

getInterfaces()方法只会返回指定类实现的接口,不会返父类实现的接口。

12. 获取泛型信息

很多人认为Java类在编译后,会把泛型信息给擦除掉,所以在运行时是无法获取到泛型信息的。但在某些情况下,我们还是可以通过反射在运行时获取到泛型信息的。比如我们可以先获取到java.lang.reflect.Method对象,然后就有可能获取到某个方法的泛型返回信息。

public class ReflectionTest03 {

    private List<String> getNames() {
        //获取泛型信息
        List<String> names = new ArrayList<>();
        names.add("一一哥");
        names.add("孙一一");
        return names;
    }

    public static void main(String[] args) {
        try {
            //获取Method信息
            Method method = ReflectionTest03.class.getDeclaredMethod("getNames", null);
            method.setAccessible(true);

            //获取方法中的泛型返回值
            Type returnType = method.getGenericReturnType();
            //判断返回值是否是参数类型
            if (returnType instanceof ParameterizedType) {
                //转为参数类型
                ParameterizedType type = (ParameterizedType) returnType;
                //获取实际类型参数
                Type[] typeArguments = type.getActualTypeArguments();
                for (Type typeArgument : typeArguments) {
                    Class typeArgClass = (Class) typeArgument;
                    System.out.println("类型参数 = " + typeArgClass);
                }
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

}

上面这段代码会打印如下结果:

typeArgClass = java.lang.String

13. 实现动态代理

我们在学习Spring AOP面向切面编程的时候,会涉及到动态代理的概念,所谓的动态代理就是指把运行时创建接口的动态实现称为动态代理。而动态代理也需要用到反射在运行时创建接口的动态实现来实现,java.lang.reflect.Proxy类就提供了创建动态实现的功能。

动态代理可以用于许多不同的目的,例如数据库连接和事务管理、用于单元测试的动态模拟对象以及其他类似aop的方法拦截等。

13.1 创建代理

调用java.lang.reflect.Proxy类的newProxyInstance()方法就可以创建动态代理,newProxyInstance()方法有三个参数:

  1. 用于“加载”动态代理类的类加载器;
  2. 要实现的接口数组;
  3. 将代理上的所有方法调用转发到InvocationHandler的对象。

13.2 示例代码

InvocationHandler handler = new MyInvocationHandler();

MyInterface proxy = (MyInterface) Proxy.newProxyInstance(MyInterface.class.getClassLoader(),
                                                         new Class[] {MyInterface.class},
                                                         handler);

运行上面代码后,proxy变量中就包含了MyInterface接口的动态实现。

13.3 InvocationHandler

对代理的所有调用都将由到实现了InvocationHandler接口的handler类对象来处理,所以必须将InvocationHandler的实现传递给Proxy.newProxyInstance()方法。对动态代理的所有方法调用都转发到实现接口的InvocationHandler对象。 InvocationHandler代码:

public interface InvocationHandler{
  
    Object invoke(Object proxy, Method method, Object[] args)
         throws Throwable;
    
}

13.4 实现InvocationHandler接口的类

public class MyInvocationHandler implements InvocationHandler{

  public Object invoke(Object proxy, Method method, Object[] args)
  throws Throwable {
    //do something "dynamic"
  }
    
}

上面代码中会生成一个MyInterface的接口对象proxy,通过proxy对象调用的方法都会由MyInvocationHandler类的invoke()方法处理。

下面详细介绍invoke()方法的三个参数:

Object proxy参数:实现接口的动态代理对象,通常不需要这个对象;

Method method参数:表示在动态代理实现的接口上调用的方法。通过Method对象,可以获取到方法名,参数类型,返回类型等信息。

Object[] args参数:包含调用接口中实现的方法时传递给代理的参数值。注意:如果接口中的参数是int、long等基本数据时,这里的args必须使用Integer, Long等包装类型。

四. 反射原理(重点)

1. Java开发步骤

我们进行Java开发时,一般要经历3个开发步骤,如下图所示:

根据上图可知,Java的开发步骤如下:

第1步:编写一系列的Java代码,创建出 .java 源文件;

第2步:执行javac命令,将 .java源文件编译成 .class字节码文件;

第3步:将 .class字节码文件 加载进行JVM虚拟机运行。

或者,我们也可以用下图把这个开发步骤再次展示:

2. Java类加载过程

根据上面的内容可知,当我们编写完一个 Java 项目之后,所有的 Java 文件都会被编译成一个个的 .class 字节码文件。根据虚拟机的工作原理,一般情况下,这些.class字节码最终被执行需要经历如下过程:

加载->验证->准备->解析->初始化->使用->卸载

所以 class 字节码文件 被编译出来之后,就需要被 ClassLoader类加载器 加载到JVM虚拟机中才能运行。类加载器 ClassLoader 加载class文件时,会把类里的一些数值常量、方法、类信息等加载到内存中,称之为类的元数据,另外还有这个类的父类、接口、构造函数、方法、属性等原始信息也会被加载进内存。最终 JVM虚拟机会根据这些信息,在内存中自动产生一个的 Class 类对象,这个对象会被保存在.class文件里。如下图所示:

3. 加载.class文件具体过程

根据上面的小节可知,我们的Java类需要被类加载器加载进内存才可以工作,类加载器加载某个类的过程大致如下图所示:

我这里把这个加载过程总结如下:

  1. 先检查某个.class文件是否已经被加载,若已加载则直接返回,不会重复加载;
  2. 接着查看JVM的缓存中是否有该Class对象的缓存,如果没有该类对象缓存,则遵循父优先加载机制,创建一个Class对象并存放到缓存中;
  3. 如果上面两步都失败了,则执行findClass()方法加载类对象。

这样,我们的 .class字节码文件 就被加载到了JVM中,并被转换成了对应的Class类对象,但此时反射还没有介入工作。

4. Class类对象的创建步骤

上面的小节中,我提到了生成Class类对象,接下来我们再来看看生成Class类对象的具体步骤。

根据上面章节可知,类加载器首先会检查这个类的Class对象是否已被加载过,如果尚未加载,默认的类加载器就会根据类名查找到对应的.class文件,然后得到对应的Class类对象。只有得到了Class对象的引用,才能在运行时使用类型信息。其实创建Class类对象的过程分为以下3个步骤:

  • 加载:由类加载器完成,找到对应的字节码,创建一个Class对象;
  • 链接:验证类中的字节码,为静态域分配空间;
  • 初始化:如果该类有超类,则对其初始化,执行静态初始化器和静态初始化块。

所以当我们编写了与反射相关的代码后,就生成了对应的反射相关.class字节码,最后再生成对应的Class类对象。在这个Class类对象里维护着该类的所有Method,Field,Constructor的cache,这份cache也可以被称作根对象。所以反射的原理之一其实就是动态的生成这个Class类对象,并将其加载到JVM中运行。

在Java中,Class类与java.lang.reflect类库一起对反射进行了支持,该类库包含Field、Method和Constructor类。这些Field、Method、Constructor等类对象会由JVM在启动时创建,用以表示未知类里对应的成员。

接着我们就可以使用Contructor来创建新的对象,用get()和set()方法获取和修改类中与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。

另外,我们还可以调用getFields()、getMethods()和getConstructors()等许多便利的方法,来返回表示字段、方法、以及构造器对象的数组。

这些对象信息都是在运行时被完全确定下来的,而在编译时不需要知道关于类的任何事情。

5. Method对象的创建过程

至此,我们就得到了一个包含很多类信息的Class类对象了。接下来我们再看看利用反射执行某个方法的执行原理。

由上面可知,Class类对象是在加载类时由JVM构造的,JVM为每个类都管理着一个独一无二的Class对象,这份Class对象里维护着该类的所有Method,Field,Constructor的cache,这份cache也可以被称作根对象。

每次getMethod()方法获取到的Method对象,都会持有对根对象的引用,因为一些重量级的Method的成员变量(主要是MethodAccessor),我们不希望每次创建Method对象时都要重新初始化。于是所有代表同一个方法的Method对象都共享着根对象的MethodAccessor,每一次创建都会调用根对象的copy方法复制一份。创建Method对象的过程如下图所示:

这样我们就创建了另一个对象---Method对象,接下来就可以利用该对象执行某个方法了。

6. invoke()方法执行过程

上面我们已经生成了Method对象,然后就可以调用Method.invoke()方法了。调用Method.invoke()方法后,会直接去调MethodAccessor.invoke。

MethodAccessor就是上面提到的所有同名method共享的一个实例,由ReflectionFactory创建。ReflectionFactory中采用了一种名为inflation的创建机制(JDK1.4之后):

如果该方法的累计调用次数<=15次,则会创建出NativeMethodAccessorImpl对象,它的实现就是直接调用native方法实现反射;

如果该方法的累计调用次数>15次,会用Java代码创建出由字节码组装而成的MethodAccessorImpl对象。

注:

是否采用inflation和15这个数字都可以在jvm参数中调整。

7. inflation机制的由来

那么为什么要有inflation机制,以及为什么用15次作为是否调用native()方法和Java代码的分水岭呢?

其实之所以要有inflation这个机制,是因为创建MethodAccessorImpl对象时,第1次和第16次调用是最耗时的(初始化NativeMethodAccessorImpl和字节码拼装MethodAccessorImpl)。毕竟初始化是不可避免的,而native方式的初始化会更快,因此前几次的调用会采用native方法。

但随着调用次数的增加,每次反射都使用JNI跨越native边界会对优化有阻碍作用,相对来说使用拼装出的字节码可以直接以Java调用的形式实现反射,发挥了JIT优化的作用,避免了JNI为了维护OopMap(HotSpot用来实现准确式GC的数据结构) 而进行封装/解封装的性能损耗。

因此在已经创建了MethodAccessor的情况下,使用Java版本的实现会比native版本更快。所以当调用次数到达一定次数(15次)后,会切换成Java实现的版本,来优化未来可能的更频繁的反射调用。

五. 结语

至此,壹哥 就带大家把反射相关的知识点复习回顾了一遍,我们在给面试官介绍反射时,要从反射的概念、作用、存在意义、使用方法开始介绍。当然如果你能把反射的底层执行原理,甚至反射的底层源码都能够回答的很清楚,我相信,但是一个反射,就足以让面试官对你另眼相看了。

不知本文对反射的介绍,有没有加深你之前的印象呢?可以在评论区留言讨论一下,说说你对反射的理解吧。

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-11-26 08:43:52  更:2021-11-26 08:44:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:27:51-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码