一个反射问住一大堆人

点击蓝色字免费订阅,天天收到这样的好信息java

单单是问反射有什么用,其实最经常使用的就两个:面试

  • 根据类名建立实例(类名能够从配置文件读取,不用new,达到解耦)spring

  • 用Method.invoke执行方法数据库

可是这些其实不难理解,难的是反射自己。若是有兴趣能够往下看:vim

因为反射自己确实抽象(说是Java中最抽象的概念也不为过),因此我当初写做时也用了大量的比喻。可是比喻有时会让答案偏离得更远。前阵子看了些讲设计模式的文章,把比喻都用坏了。有时理解比喻,居然要比理解设计模式自己还费劲...那就南辕北辙了。因此,这一次,能不用比喻就尽可能不用,争取用最实在的代码去解释。设计模式

主要内容:数组

  • JVM是如何构建一个实例的缓存

  • .class文件微信

  • 类加载器mybatis

  • Class类

  • 反射API

 


 

JVM是如何构建一个实例的

下文我会使用的名词及其对应关系

  • 内存:即JVM内存,栈、堆、方法区啥的都是JVM内存,只是人为划分

  • .class文件:就是所谓的字节码文件,这里称.class文件,直观些

假设main方法中有如下代码:

Person p = new Person();

不少初学者会觉得整个建立对象的过程是下面这样的

javac Person.java
java Person

 

 

不能说错,可是粗糙了一点。

稍微细致一点的过程能够是下面这样的

 

 

经过new建立实例和反射建立实例,都绕不开Class对象。

 


 

.class文件

有人用编辑器打开.class文件看过吗?

好比我如今写一个类

 

 

用vim命令打开.class文件,以16进制显示就是下面这副鬼样子:

 

 

在计算机中,任何东西底层保存的形式都是0101代码。

.java源码是给人类读的,而.class字节码是给计算机读的。根据不一样的解读规则,能够产生不一样的意思。就比如“这周日你有空吗”,合适的断句很重要。

一样的,JVM对.class文件也有一套本身的读取规则,不须要咱们操心。总之,0101代码在它眼里的样子,和咱们眼中的英文源码是同样的。

 

 

 


 

类加载器

在最开始复习对象建立过程时,咱们了解到.class文件是由类加载器加载的。关于类加载器,若是掰开讲,是有不少门道的,能够看看

@请叫我程序猿大人

写的好怕怕的类加载器。可是核心方法只有loadClass(),告诉它须要加载的类名,它会帮你加载:

 

 
  



protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查是否已经加载该类 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 若是还没有加载,则遵循父优先的等级加载机制(所谓双亲委派机制) if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // 模板方法模式:若是仍是没有加载成功,调用findClass() long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } // 子类应该重写该方法 protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }

 

加载.class文件大体能够分为3个步骤:

  1. 检查是否已经加载,有就直接返回,避免重复加载

  2. 当前缓存中确实没有该类,那么遵循父优先加载机制,加载.class文件

  3. 上面两步都失败了,调用findClass()方法加载

须要注意的是,ClassLoader类自己是抽象类,而抽象类是没法经过new建立对象的。因此它的findClass()方法写的很随意,直接抛了异常,反正你没法经过ClassLoader对象调用。也就是说,父类ClassLoader中的findClass()方法根本不会去加载.class文件。

正确的作法是,子类重写覆盖findClass(),在里面写自定义的加载逻辑。好比:

@Overridepublic Class<?> findClass(String name) throws ClassNotFoundException { try { /*本身另外写一个getClassData() 经过IO流从指定位置读取xxx.class文件获得字节数组*/ byte[] datas = getClassData(name); if(datas == null) { throw new ClassNotFoundException("类没有找到:" + name); } //调用类加载器自己的defineClass()方法,由字节码获得Class对象 return defineClass(name, datas, 0, datas.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("类找不到:" + name); }}

 

defineClass()是ClassLoader定义的方法,目的是根据.class文件的字节数组byte[] b造出一个对应的Class对象。咱们没法得知具体是如何实现的,由于最终它会调用一个native方法:

 

 

反正,目前咱们关于类加载只需知道如下信息:

 

 

 


 

Class类

如今,.class文件被类加载器加载到内存中,而且JVM根据其字节数组建立了对应的Class对象。因此,咱们来研究一下Class对象。

Class对象是Class类的实例,咱们将在这一小节一步步分析Class类的结构。

可是,在看源码以前,我想问问聪明的各位,若是你是JDK源码设计者,你会如何设计Class类?

假设如今有个BaseDto类

 

 

上面类至少包括如下信息(按顺序):

  • 权限修饰符

  • 类名

  • 参数化类型(泛型信息)

  • 接口

  • 注解

  • 字段(重点)

  • 构造器(重点)

  • 方法(重点)

最终这些信息在.class文件中都会以0101表示:

 

 

整个.class文件最终都成为字节数组byte[] b,里面的构造器、方法等各个“组件”,其实也是字节。

因此,我猜Class类的字段至少是这样的:

 

 

好了,看一下源码是否是如我所料:

 

字段、方法、构造器对象

注解数据

泛型信息

 

等等。

并且,针对字段、方法、构造器,由于信息量太大了,JDK还单独写了三个类,好比Method类:

 

 

也就是说,Class类准备了不少字段用来表示一个.class文件的信息,对于字段、方法、构造器等,为了更详细地描述这些重要信息,还写了三个类,每一个类里面都有很详细的对应。

 

 

也就是说,本来UserController类中全部信息,都被“解构”后保存在Class类、Method类等的字段中。

大概了解完Class类的字段后,咱们看看Class类的方法。

 

  • 构造器

 

 

能够发现,Class类的构造器是私有的,咱们没法手动new一个Class对象,只能由JVM建立。JVM在构造Class对象时,须要传入一个类加载器,而后才有咱们上面分析的一连串加载、建立过程。

 

  • Class.forName()方法

 

 

反正仍是类加载器去搞呗。

 

  • newInstance()

 

 

也就是说,newInstance()底层就是调用无参构造对象的newInstance()。

因此,本质上Class对象要想建立实例,其实都是经过构造器对象。若是没有空参构造对象,就没法使用clazz.newInstance(),必需要获取其余有参的构造对象而后调用构造对象的newInstance()。

 


 

反射API

没啥好说的,在平常开发中反射最终目的主要两个:

  • 建立实例

  • 反射调用方法

建立实例的难点在于,不少人不知道clazz.newInstance()底层仍是调用Contructor对象的newInstance()。因此,要想调用clazz.newInstance(),必须保证编写类的时候有个无参构造。

 

反射调用方法的难点,有两个,初学者可能会不理解。

再此以前,先来理清楚Class、Field、Method、Constructor四个对象的关系:

 

 

Field、Method、Constructor对象内部有对字段、方法、构造器更详细的描述:

 

 

OK,理清关系后咱们继续来看看反射调用方法时的两个难点。

 

  • 难点一:为何根据Class对象获取Method时,须要传入方法名+参数的Class类型

 

 

为何要传name和ParameterType?

由于.class文件中有多个方法,好比

 

 

因此必须传入name,以方法名区分哪一个方法,获得对应的Method。

那参数parameterTypes为何要用Class类型,我想和调用方法时同样直接传变量名不行吗,好比userName, age。

答案是:咱们没法根据变量名区分方法

User getUser(String userName, int age);
User getUser(String mingzi, int nianling);

这不叫重载,这就是同一个方法。只能根据参数类型。

我知道,你还会问:变量名不行,那我能不能传String, int。

很差意思,这些都是基本类型和引用类型,类型不能用来传递。咱们能传递的要么值,要么对象(引用)。而String.class, int.class是对象,且是Class对象。

实际上,调用Class对象的getMethod()方法时,内部会循环遍历全部Method,而后根据方法名和参数类型匹配惟一的Method返回。

 

循环遍历全部Method,根据name和parameterType匹配

 

 

难点二:调用method.invoke(obj, args);时为何要传入一个目标对象?

上面分析过,.class文件经过IO被加载到内存后,JDK创造了至少四个对象:Class、Field、Method、Constructor,这些对象其实都是0101010的抽象表示。

以Method对象为例,它究竟是什么,怎么来的?咱们上面已经分析过,Method对象有好多字段,好比name(方法名),returnType(返回值类型)等。也就是说咱们在.java文件中写的方法,被“解构”之后存入了Method对象中。因此对象自己是一个方法的映射,一个方法对应一个Method对象。

我在专栏的另外一篇文章中讲过,对象的本质就是用来存储数据的。而方法做为一种行为描述,是全部对象共有的,不属于某个对象独有。好比现有两个Person实例

Person p1 = new Person();
Person p2 = new Person();

对象 p1保存了"hst"和18,p2保存了"cxy"和20。可是不论是p1仍是p2,都会有changeUser(),可是每一个对象里面写一份太浪费。既然是共性行为,能够抽取出来,放在方法区共用。

但这又产生了一个棘手的问题,方法是共用的,JVM如何保证p1调用changeUser()时,changeUser()不会跑去把p2的数据改掉呢?

因此JVM设置了一种隐性机制,每次对象调用方法时,都会隐性传递当前调用该方法的对象参数,方法能够根据这个对象参数知道当前调用本方法的是哪一个对象!

 

 

一样的,在反射调用方法时,本质仍是但愿方法处理数据,因此必须告诉它执行哪一个对象的数据。

 

 

因此,把Method理解为方法执行指令吧,它更像是一个方法执行器,必须告诉它要执行的对象(数据)。

固然,若是是invoke一个静态方法,不须要传入具体的对象。由于静态方法并不能处理对象中保存的数据。

 

打油诗

 

我不在意个人做品文章是被如今的人读仍是由子孙后代来读。既然上帝花了六千年来等一位观察者,我能够花上一个世纪来等待读者。

 

 

往期推荐

 

 

spring 状态机

spring 状态机 (二)

mybatis用到的设计模式

jvm高级面试题(必须看)

MySQL索引实现原理分析

Spring中的用到的设计模式

SpringBoot 自动装配原理解析(一)

java 8 lambda表达式中的异常处理

spring @Async异步方法使用及原理说明

Spring 和 SpringBoot 之间到底有啥区别?

只需这10步,经过历史控制文件恢复数据库

让bug无处藏身,Java 线上问题排查思路、经常使用工具

 

 

 

 

本文分享自微信公众号 - Java小白学心理(gh_9a909fa2fb55)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索