反射和多态的实现原理详解以及区别
共 17747字,需浏览 36分钟
·
2021-08-01 08:29
点击关注公众号,Java干货及时送达
真香!24W字的Java面试手册(点击查看)
反射和多态
事实上,反射和多态这两种技术并无直接联系,之所以把它们放在一起说,是因为,在Java技术体系中所提供的能够让我们在运行时识别对象和类的类型信息的方式,一共有两种:即反射和多态关键技术RTTI
RTTI,即run-Time Type Identification运行时类型判定,它的作用是在我们不知道某个对象的确切的类型信息时,即某个对象是哪个类的实例的时候,可以通过RTTI相关的机制帮助我们在编译时获取对象的类型信息,这其实也就是多态的实现基础。
反射机制允许我们在运行时发现和使用类的信息。因此多态和反射两者的最大的共同点在于,他们都是运行时获取程序信息的技术。反射技术对于java世界的众多框架以及特性都发挥着至关重要的作用,博主认为中文的基石一词能够准确形容反射技术的地位,理解反射技术对于学习java相关技术的背后原理非常重要,许多java世界里的特性的实现原理背后都离不开反射。
让我们先来看一下相对简单一点的多态的相关知识以及实现原理RTTI相关的知识。
多态
什么是多态
首先明确一点我们在这里只考虑运行时多态,而不考虑编译时多态(方法重载)。因此下列多态默认都是指运行时多态。
多态是面向对象编程里面的概念,一个接口的多种不同的实现方式,即为多态。注意这里的接口,不应理解得太死板,比如在java里面,继承一个类和实现一个接口本质上都是一种继承行为,因此都应该理解为多态的体现。
在计算机的世界里,尤其是编程的世界里,多态体现在:只有在运行的时候才知道引用变量所指向的具体实例对象。且有三个必要的条件:
继承 重写/实现 父类引用指向子类对象
多态的概念来源于生活,生活中的很多现象都是多态的体现,例如打印机,打印功能可以打印黑白色也可以打印彩色。同一款汽车可以用2.0l排量也可以有1.0l的排量。
多态的技术带来的一个重要影响是:由于一个借口可能有多个实现,而每个实现之间的大小,规模,是不一样的。因此多态对内存的分配是有影响的,不同的实现会有不同的内存分配. 这一点与现实世界的多态例子相比就会非常有意思,第一,我们会发现软件里的多态是动态的多态,而现实世界里的多态大部分是一个预先设定好的多态体现,现实里的多态更多的类似于编译时多态,即方法重载,例如打印机的例子。
java里多态的具体用法
如上面我们提到的一样多态通常有两种实现方法:
子类继承父类(extends) 类实现接口(implements)
核心之处就在于对父类方法的改写或对接口方法的实现,以取得在运行时不同的执行效果。要使用多态,在声明对象时就应该遵循一条法则:声明的总是父类类型或接口类型,而创建的是实际类型.
以ArrayList为例子,要使用多态的特性,要按照如下方式定义
List list = new ArrayList();
此外,在定义方法参数时也通常总是应该优先使用父类类型或接口类型,例如:
public void test(List list);
这样声明最大的好处在于它的灵活性,假如某一天ArrayList无法满足要求,我们希望用LinkedList来代替它,那么只需要在对象创建的地方把new ArrayList()改为new LinkedList即可,其它代码一概不用改动。
多态的实现原理与RTTI
RTTI,即Run-Time Type Identification运行时类型认定,通过运行时类型信息程序能够使用父类的指针或引用来检查这些指针或引用所指的对象的实际派生类型,是多态实现的技术基础。RTTI的功能主要是通过Class类文件实现的,更精确一点是通过Class类文件的方法表实现的。
Class类是"类的类"(class of classes)。如果说类是对象的抽象的话,那么Class类就是对类的抽象。Class对象就是用来创建一个类的所有的常规对象的。每个类都有一个Class对象,每当编写好并且编译了一个新的类,就会生成一个它对应的Class对象,被保存在一个与类同名的.class文件中。java虚拟机中的被称为类加载器的子系统,就是专门拿来做生成这个类的Class对象的工作的。
每一个Class类的对象代表一个特定的类。请看如下代码
import java.lang.Class;
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Cycle unicycle = new Unicycle("Unicycle");
Cycle.ride(unicycle);
Class c1 = unicycle.getClass();//获取clas对象
System.out.println(c1.getName());
Cycle bicycle = new Bicycle("Bicycle");
Cycle.ride(bicycle);
Class c2 = Class.forName("basic.Bicycle");//获取clas对象
System.out.println(c2.getName());
Cycle tricycle = new Tricycle("Tricycle");
Cycle.ride(tricycle);
Class c3 = Tricycle.class;//获取clas对象
System.out.println(c3.getName());
}
}
//父类
class Cycle {
private String name;
public Cycle(String str) {
name = str;
}
public static void ride(Cycle c) {
System.out.println(c.name + "is riding");
}
}
class Unicycle extends Cycle {
private String name;
public Unicycle(String str) {
super(str);
name = str;
}
}
class Bicycle extends Cycle {
private String name;
public Bicycle(String str) {
super(str);
name = str;
}
}
class Tricycle extends Cycle {
private String name;
public Tricycle(String str) {
super(str);
name = str;
}
}
这是一个普通的多态的示例程序,但是我在每一处多态调用时,分别去获取了他们的class对象并打印出来。打印结果如下:
Unicycleis riding
basic.Unicycle
Bicycleis riding
basic.Bicycle
Tricycleis riding
basic.Tricycle
可以发现即使我们将对象的引用向上转型,对象所指向的Class类对象依然是实际的实现类。
Java中每个对象都有相应的Class类对象,因此,我们随时能通过Class对象知道某个对象“真正”所属的类。无论我们对引用进行怎样的类型转换,对象本身所对应的Class对象都是同一个。这意味着java在运行时的确能确定真正的实现类是哪一个。
下面从虚拟机运行时的角度来简要介绍多态的实现原理,这里以Java虚拟机规范的实现为例。
在JVM执行Java字节码时,类型信息被存放在方法区中,通常为了优化对象调用方法的速度,方法区的类型信息中增加一个指针,该指针指向一张记录该类方法入口的表(称为方法表),表中的每一项都是指向相应方法的指针。
方法表的构造如下:
由于Java的单继承机制,一个类只能继承一个父类,而所有的类又都继承自Object类。方法表中最先存放的是Object类的方法,接下来是该类的父类的方法,最后是该类本身的方法。方法表从上至下如下图所示
![](https://filescdn.proginn.com/f2178b3bb27ee5ec2dc932cda6151fab/0074df1a2dd680fcaf65f56e97fa54f1.webp)
这里关键的地方在于,如果子类改写了父类的方法,那么子类和父类的那些同名方法共享一个方法表项,都被认作是父类的方法。如下所示
![](https://filescdn.proginn.com/2c3d48a17d95c7fcc0054c45fc6692a1/eab0abc58ee0ea07f082ef8f4bd60b71.webp)
注意这里只有非私有的实例方法才会出现,并且静态方法也不会出现在这里,原因很容易理解:静态方法跟对象无关,可以将方法地址直接引用,而不像实例方法需要间接引用。
更深入地讲,静态方法是由虚拟机指令invokestatic调用的,私有方法和构造函数则是由invokespecial指令调用,只有被invokevirtual和invokeinterface指令调用的方法才会在方法表中出现。
由于以上方法的排列特性(Object——父类——子类),使得方法表的偏移量总是固定的。例如,对于任何类来说,其方法表中equals方法的偏移量总是一个定值,所有继承某父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值。
前面说过,方法表中的表项都是指向该类对应方法的指针,这里就开始了多态的实现:
假设Class B是Class A的子类,并且B重写了A的方法method(),那么在B的方法表中,method()方法的指针指向的就是B的method方法入口而非类A的同名方法入口,也就是说,在虚拟机编译生成B的Class文件中的方法表时,就实现了多态,之后只需让对应的指令调用即可。
而对于A来说,它的方法表中的method方法则会指向其自身的method方法而非其父类的(这在类加载器载入该类时已经保证,同时JVM会保证总是能从对象引用指向正确的类型信息)。
结合方法指针偏移量是固定的以及指针总是指向实际类的方法域,我们不难发现多态的机制就在这里:
在调用方法时,实际上必须首先完成实例方法的符号引用解析,结果是该符号引用被解析为方法表的偏移量。虚拟机通过对象引用得到方法区中类型信息的入口,查询类的方法表,当将子类对象声明为父类类型时,形式上调用的是父类方法,此时虚拟机会从实际类的方法表(虽然声明的是父类,但是实际上这里的类型信息中存放的是子类的信息)中查找该方法名对应的指针(这里用“查找”实际上是不合适的,前面提到过,方法的偏移量是固定的,所以只需根据偏移量就能获得指针),进而就能指向实际类的方法了。
我们的故事还没有结束,事实上上面的过程仅仅是利用继承实现多态的内部机制,多态的另外一种实现方式:实现接口相比而言就更加复杂,原因在于,Java的单继承保证了类的线性关系,而接口可以同时实现多个,这样光凭偏移量就很难准确获得方法的指针。所以在JVM中,多态的实例方法调用实际上有两种指令:
invokevirtual指令用于调用声明为类的方法; invokeinterface指令用于调用声明为接口的方法。
当使用invokeinterface指令调用方法时,就不能采用固定偏移量的办法,只能老老实实挨个找了(当然实际实现并不一定如此,JVM规范并没有规定究竟如何实现这种查找,不同的JVM实现可以有不同的优化算法来提高搜索效率)。
我们不难看出,在性能上,调用接口引用的方法通常总是比调用类的引用的方法要慢。这也告诉我们,在类和接口之间优先选择接口作为设计并不总是正确的,当然设计问题不在本文探讨的范围之内,但显然具体问题具体分析仍然不失为更好的选择。
这就是多态的原理。总结起来说就是两点:
是方法表起了决定性作用,如果子类改写了父类的方法,那么子类和父类的那些同名方法共享一个方法表项,都被认作是父类的方法,因此可以写成父类引用指向子类对象的形式。 类和接口的多态实现不一样,类的方法表可以使用固定偏移,但接口只能挨个找,原因是接口的实现不是确定唯一的。
关于RTTI技术,典型的应用除了多态这样的类型转换,以及根据类生成Class对象这两种形式以外。还有一种常见的用法,就是关键字Instanceof。Instanceof的作用是返回一个布尔值,告诉我们一个对象是不是某个特定类的一个实例。例如:
if(x instanceof Dog){
}
即在判断x对象是不是Dog类的一个实例。
思考题:为什么使用多态创建一个对象引用之后,有的方法引用中不能调用?
反射
RTTI的作用是,当我们不知道一个对象的确切的类型的时候,可以通过RTTI来获取。但是这个功能存在一个限制:要通过RTTI获取的类型信息必须是在编译时已知的,这句话怎么理解,我们通过多态的例子来说明:
Animal dog = new Dog();
上述代码定义的引用是Animal类型,但是在编译时期,虚拟机可通过我们后面的new的代码获取到这个Animal的真正类型,这就是编译时已知,这里有个前提条件是代码里面其实是要给虚拟机留下这样的判断信息的。这事实上是一个限制。
原因在于,我们在很多时候并不能在编译时获知某个对象所述的类。例如,我们有个api的功能是接收字段并解析它属于哪个类,但是传入的字段可能是我们定义好的多个类的其中一个,这个时候就没办法在编译时知道类型信息了。而只能在运行时通过传入的字段进行判断才有可能知道。
什么是反射
反射的定义如下:java程序在运行状态中,对于任意一个类,都能够在运行时知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
上面这段话需要非常仔细的理解,我想通过问问题的形式来帮助读者理解反射的概念。
问题1:什么是类的属性和方法?
这个问题比较愚蠢,甚至有侮辱人的智商之嫌,但是博主的本意肯定不是这样。这是一个很简单问题,假设我们有下面代码:
public class Test{
private String s;
public void doNothing(){
}
}
那么,字符串s就是它的属性,doNothing就是它的方法
问题2:什么叫一个类的所有属性和方法
答:所有这个类中定义的成员变量,以及所有属于这个类的方法。
问题3:问题2中的所有属性和方法包含私有的吗?
答案是包含的
问题4:任何一个类,除了属性和方法,还有别的内容吗?
答案是没有了。注意,内部类不属于外部类,本质上是两个类。
问题5:什么叫获取一个方法,什么叫获取一个属性?
这是一个很好的问题,理解了这个问题之后也就不会觉得反射过于抽象了。java当中提供了专门的方法的抽象Method类和专门的属性的抽象Field类,以及专门的所有类的抽象Class类,并提供了一系列的方法,来帮助我们获取一个类的属性和方法。
获取到的属性和方法,将以普通对象的方式存在。与我们自己写的类并无任何区别。下面是一个代码例子:
public class MethodClass {
public static void main(String[] args) throws Exception {
//1.获取Class对象
Class xxxClass = Class.forName("com.XXX.XXX");
//2.获取所有公有方法
Method[] methodArray = xxxClass .getMethods();
//3.获取字段
Field[] fieldArray = xxxClass .getFields();
}
}
反射的实现原理
Class类与java.lang.reflect库一起对反射的概念提供了技术支持。java.lang.reflect
类库包含了Field类,Method类以及Constructor类。这些类用来表示未知类里对应的成员。Class类提供了获取getFields()
、getMethods()
和getConstructors()
等方法,而这些方法的返回值类型就定义在java.lang.reflect
当中。
如果不知道某个对象的确切类型(即list引用到底是ArrayList类型还是LinkedList类型),RTTI可以告诉你,但是有一个前提:这个类型在编译时必须已知,这样才能使用RTTI来识别它。
要想理解反射的原理,必须要结合类加载机。反射机制并没有什么神奇之处,当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类,然后再通过拿到的某一个类的全限定名去找这个类的Class文件 。因此,那个类的.class对于JVM来说必须是可获取的,要么在本地机器上,要么从网络获取。所以对于RTTI和反射之间的真正区别只在于:
RTTI,编译器在编译时打开和检查.class文件 反射,运行时打开和检查.class文件
对于反射机制而言.class文件在编译时是不可获取的,所以是在运行时获取和检查.class文件。
总结起来说就是,反射是通过Class类和java.lang.reflect类库一起支持而实现的,其中每一个Class类的对象都对应了一个类,这些信息在编译时期就已经被存在了.class文件里面了,Class 对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的defineClass方法自动构造的。对于我们定义的每一个类,在虚拟机中都有一个应的Class对象。
那么在运行时期,无论是通过字面量还是forName方法获取Class对象,都是去根据这个类的全限定名(全限定名必须是唯一的,这也间接回答了为什么类名不能重复这个问题。)然后获取对应的Class对象
总结: java虚拟机帮我们生成了类的class对象,而通过类的全限定名,我们可以去获取这个类的字节码.class文件,然后再获取这个类对应的class对象,再通过class对象提供的方法结合类Method,Filed,Constructor,就能获取到这个类的所有相关信息. 获取到这些信息之后,就可以使用Constructor创建对象,用get和set方法读取和修改与Field对象相关的字段,用invoke方法调用与Method对象关联的方法。
反射的应用
反射机制非常重要,应用也非常之广泛。在使用反射时,我们的代码里面可以出现任何一个具体的构造器,字段信息,方法,但是却能动态的生成对象,调用他们的方法,这是一个非常通用的功能,由此带来的价值也是惊人的。反射比较出名的应用有:
Spring/Mybatis等框架,行内有一句这样的老话:反射机制是Java框架的基石。最经典的就是xml的配置模式。 JDBC 的数据库的连接 动态生成对象,应用于工厂模式中. spring的bean容器也就是一个工厂 jdk动态代理,利用反射获取传入接口的实现类 注解机制的实现,利用反射可以获取每一个filed,Filed类提供了getDeclaredAnnotations方法以数组形式返回这个字段所有的注解.... 编辑器代码自动提示的实现
反射的弊端
1.性能
反射包括了一些动态类型,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被 执行的代码或对性能要求很高的程序中使用反射。
2.安全
使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
3.内部暴露
由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
4.丧失了编译时类型检查的好处,包括异常检查。如果程序企图用反射去调用不存在或者不可访问方法,在运行时将会失败。
5.从代码规范的角度来说,执行反射访问所需要的代码非常笨拙和冗长。这样的代码阅读起来很困难
核心反射机制最初是为了基于组件的应用创建工具而设计的,如spring。这类工具通常需要装载类,并且用反射功能找出它们支持哪些方法和构造器。这些工具允许用户交互式的构建访问这些类的应用程序。
反射功能只是在应用程序设计阶段被用到,通常,普通应用程序运行时不应该以反射方式访问对象。对于特定的复杂系统编程任务,它也许是非常必要的,但它也有一些缺点,如果你编写的程序必须要与编译时未知的类一起工作,如有可能就使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。
基于此,在effective java也总结了一条接口优先于反射机制的开发原则。
反射相关类
Class类
关于Class类请参考博客:[面向对象] 类与对象与Java里的Class类解析
Field类
即字段类,我们可以通过一个类的Class对象获取其Field类的对象,然后java当中提供了这个Field类来提供反射获取字段的相关信息,以及进行一些操作,比如set一个字段的值等功能。
Method类
即方法类,我们可以通过一个类的Class对象获取其Method类的一个实例对象,并且使用获得的Method对象去获取这个方法的相关信息,以及调用这个方法的功能。
以上三个类建议读者直接查看源代码,看看他们提供的方法,就会理解得更加清楚。
反射应用实例
下面的代码是使用反射获取方法的例子程序
model类
package com.dr.Reflection.getMethodByReflect;
public class Student {
//**************成员方法***************//
public void show1(String s) {
System.out.println("调用了:公有的,String参数的show1(): s = " + s);
}
protected void show2() {
System.out.println("调用了:受保护的,无参的show2()");
}
void show3() {
System.out.println("调用了:默认的,无参的show3()");
}
private String show4(int age) {
System.out.println("调用了,私有的,并且有返回值的,int参数的show4(): age = " + age);
return "abcd";
}
}
测试类
package com.dr.Reflection.getMethodByReflect;
import java.lang.reflect.Method;
/*
* 获取成员方法并调用:
*
* 1.批量的:
* public Method[] getMethods():获取所有"公有方法";(包含了父类的方法也包含Object类)
* public Method[] getDeclaredMethods():获取所有的成员方法,包括私有的(不包括继承的)
* 2.获取单个的:
* public Method getMethod(String name,Class<?>... parameterTypes):
* 参数:
* name : 方法名;
* Class ... : 形参的Class类型对象
* public Method getDeclaredMethod(String name,Class<?>... parameterTypes)
*
* 调用方法:
* Method --> public Object invoke(Object obj,Object... args):
* 参数说明:
* obj : 要调用方法的对象;
* args:调用方式时所传递的实参;
):
*/
public class MethodClass {
public static void main(String[] args) throws Exception {
//1.获取Class对象
Class stuClass = Class.forName("com.dr.Reflection.getMethodByReflect.Student");
//2.获取所有公有方法
System.out.println("***************获取所有的”公有“方法*******************");
stuClass.getMethods();
Method[] methodArray = stuClass.getMethods();
for(Method m : methodArray){
/*
注意,这里虽然student类自身只写了一个public方法,但是由于java所有的类都继承于object类,
因此object类中所有的public方法也会被打印出来
*/
System.out.println(m);
}
System.out.println("***************获取所有的方法,包括私有的*******************");
methodArray = stuClass.getDeclaredMethods();
for(Method m : methodArray){//不包含父类的,仅仅是这个自身定义的方法
System.out.println(m);
}
System.out.println("***************获取公有的show1()方法*******************");
Method m = stuClass.getMethod("show1", String.class);//根据方法名称,以及参数,获取方法对象
System.out.println(m);
//实例化一个Student对象
Object obj = stuClass.getConstructor().newInstance();
m.invoke(obj, "刘德华");
System.out.println("***************获取私有的show4()方法******************");
m = stuClass.getDeclaredMethod("show4", int.class);
System.out.println(m);
m.setAccessible(true);//解除私有限定
Object result = m.invoke(obj, 20);//需要两个参数,一个是要调用的对象(获取有反射),一个是实参
System.out.println("返回值:" + result);
}
}
一些问题
private修饰的方法可以通过反射访问,那么private意义何在?
答:首先java的private修饰符并不是为了安全性设计的,private并不是解决“安全”问题的。private想表达的不是“安全性”的意思,而是面向对象编程的封装概念,是一种编译器可以帮助我们在设计上的一个点。
private的设计理念是对一个类的封装,而封装带来的好处是,在项目开发过程当中,修改一个类的private属性是不影响使用的,因为不存在对private代码的显式引用。
反射技术主要是为实现一些开发工具以及框架服务。在实际的开发过程当中,我们应该尽量避免使用反射。而在使用反射时也要非常小心。另外,关注Java知音公众号,回复“后端面试”,送你一份面试题宝典!
反射和多态的区别
同为运行时获取信息,多态获取的信息仅仅在于确定方法应用所指向的实际对象。而反射在于获取一个类的所用信息。 多态是一种面向对象语言的机制。而反射技术是java提供的专门用于动态获取类的信息的技术。
如有文章对你有帮助,
欢迎关注❤️、点赞👍、转发📣!
推荐, Java面试手册 内容包括网络协议、Java基础、进阶、字符串、集合、并发、JVM、数据结构、算法、MySQL、Redis、Mongo、Spring、SpringBoot、MyBatis、SpringCloud、Linux以及各种中间件(Dubbo、Nginx、Zookeeper、MQ、Kafka、ElasticSearch)等等... 点击文末“阅读原文”可直达