【Java】反射调用与面向对象结合使用产生的惊艳
缘起
我在看Spring的源码时,发现了一个隐藏的问题,就是父类方法(Method)在子类实例上的反射(Reflect)调用。
初次看到,感觉有些奇特,因为父类方法可能是抽象的或私有的,但我没有去怀疑什么,这可是Spring的源码,肯定不会有错。
不过我去做了测试,发现确实是正确的,那一瞬间竟然给我了一丝的惊艳。
这其实是面向对象(继承与重写,即多态)和反射结合的产物。下面先来看测试,最后再进行总结。
友情提示:测试内容较多,不过还是值得一看。
具体方法的继承与重写
先准备一个父类,有三个方法,分别是public,protected,private。
public class Parent {
public String m1() {
return "Parent.m1";
}
protected String m2() {
return "Parent.m2";
}
private String m3() {
return "Parent.m3";
}
}
再准备一个子类,继承上面的父类,也有三个相同的方法。
public class Child extends Parent {
@Override
public String m1() {
return "Child.m1";
}
@Override
protected String m2() {
return "Child.m2";
}
private String m3() {
return "Child.m3";
}
}
public和protected是对父类方法的重写,private自然不能重写。
首先,通过反射获取父类和子类的方法m1,并输出:
Method pm1 = Parent.class.getDeclaredMethod("m1");
Method cm1 = Child.class.getDeclaredMethod("m1");
Log.log(pm1);
Log.log(cm1);
输出如下:
public java.lang.String org.cnt.java.reflect.method.Parent.m1()
public java.lang.String org.cnt.java.reflect.method.Child.m1()
可以看到,一个是父类的方法,一个是子类的方法。
其次,比较下这两个方法是否相同或相等:
Log.log("pm1 == cm1 -> {}", pm1 == cm1);
Log.log("pm1.equals(cm1) -> {}", pm1.equals(cm1));
输入如下:
pm1 == cm1 -> false
pm1.equals(cm1) -> false
它们既不相同也不相等,因为一个在父类里,一个在子类里,它们各有各的源码,互相独立。
然后,实例化父类和子类对象:
Parent p = new Parent();
Child c = new Child();
接着,父类方法分别在父类和子类对象上反射调用:
Log.log(pm1.invoke(p));
Log.log(pm1.invoke(c));
输出如下:
Parent.m1
Child.m1
父类方法在父类对象上反射调用输出Parent.m1,这很好理解。
父类方法在子类对象上反射调用输出Child.m1,初次看到的话,还是有一些新鲜的。
明明调用的是父类版本的Method,输出的却是子类重写版本的结果。
然后,子类方法分别在父类和子类对象上反射调用:
Log.log(cm1.invoke(p));
Log.log(cm1.invoke(c));
输出如下:
IllegalArgumentException
Child.m1
子类方法在父类对象上反射调用时报错。
子类方法在子类对象上反射调用时输出Child.m1,这很好理解
按照同样的方式,对方法m2进行测试,得到的结果和m1一样。
它们一个是public的,一个是protected的,对于继承与重写来说是一样的。
然后再对方法m3进行测试,它是private的,看看会有什么不同。
首先,父类方法分别在父类和子类对象上反射调用:
Log.log(pm3.invoke(p));
Log.log(pm3.invoke(c));
输入如下:
Parent.m3
Parent.m3
可以看到,输出的都是父类里的内容,和上面确实有所不同。
其次,子类方法分别在父类和子类对象上反射调用:
Log.log(cm3.invoke(p));
Log.log(cm3.invoke(c));
输出如下:
IllegalArgumentException
Child.m3
子类方法在父类对象上反射调用时报错。
子类方法在子类对象上反射调用时输出Child.m3。
抽象方法的继承与重写
再大胆一点,使用抽象方法来测试下。
先准备一个抽象父类,有两个抽象方法。
public abstract class Parent2 {
public abstract String m1();
protected abstract String m2();
}
再准备一个子类,继承这个父类,并重写抽象方法。
public class Child2 extends Parent2 {
@Override
public String m1() {
return "Child2.m1";
}
@Override
protected String m2() {
return "Child2.m2";
}
}
使用反射分别获取父类和子类的方法m1,并输出下:
public abstract java.lang.String org.cnt.java.reflect.method.Parent2.m1()
public java.lang.String org.cnt.java.reflect.method.Child2.m1()
pm1 == cm1 -> false
pm1.equals(cm1) -> false
可以看到父类方法是抽象的,子类重写后变为非抽象的,这两个方法既不相同也不相等。
由于父类是抽象类,不能实例化,因此只能在子类对象上反射调用这两个方法:
Log.log(pm1.invoke(c2));
Log.log(cm1.invoke(c2));
输出如下:
Child2.m1
Child2.m1
没有报错。且输出正常,是不是又有一丝新鲜感,抽象方法也可以被反射调用。
对方法m2进行测试,得到相同的结果,因为protected和public对于继承与重写的规则是一样的。
接口方法的实现与继承
胆子渐渐大起来,再用接口来试试。
准备一个接口,包含抽象方法,默认方法和静态方法。
public interface Inter {
String m1();
default String m2() {
return "Inter.m2";
}
default String m3() {
return "Inter.m3";
}
static String m4() {
return "Inter.m4";
}
}
准备一个实现类,实现这个接口,实现方法m1,重写方法m2。
public class Impl implements Inter {
@Override
public String m1() {
return "Impl.m1";
}
@Override
public String m2() {
return "Impl.m2";
}
public static String m5() {
return "Impl.m5";
}
}
分别从接口和实现类获取方法m1,并输出:
public abstract java.lang.String org.cnt.java.reflect.method.Inter.m1()
public java.lang.String org.cnt.java.reflect.method.Impl.m1()
im1 == cm1 -> false
im1.equals(cm1) -> false
可以看到接口中的方法是抽象的。因为它没有方法体。
因为接口不能实例化,所以这两个方法只能在实现类上反射调用:
Impl c = new Impl();
Log.log(im1.invoke(c));
Log.log(cm1.invoke(c));
输出如下:
Impl.m1
Impl.m1
没有报错,输出正常,又一丝的新鲜,接口里的方法也可以通过反射调用。
对m2进行测试,m2是接口的默认方法,且被实现类重新实现了。
输出下接口中的m2和实现类中的m2,如下:
public default java.lang.String org.cnt.java.reflect.method.Inter.m2()
public java.lang.String org.cnt.java.reflect.method.Impl.m2()
im2 == cm2 -> false
im2.equals(cm2) -> false
这两个方法既不相同也不相等。
把它们分别在实现类上反射调用:
Impl c = new Impl();
Log.log(im2.invoke(c));
Log.log(cm2.invoke(c));
输出如下:
Impl.m2
Impl.m2
因为实现类重写了接口默认方法,所以输出的都是重写后的内容。
对m3进行测试,m3也是接口的默认方法,不过实现类没有重新实现它,而是选择使用接口的默认实现。
同样从接口和实现类分别获取这个方法,并输出:
public default java.lang.String org.cnt.java.reflect.method.Inter.m3()
public default java.lang.String org.cnt.java.reflect.method.Inter.m3()
im3 == cm3 -> false
im3.equals(cm3) -> true
发现输出的都是接口的方法,它们虽然不相同(same),但是却相等(equal)。因为实现类只是简单的继承,并没有重写。
这两个方法都在实现类的对象上反射调用,输出如下:
Inter.m3
Inter.m3
都输出的是接口的默认实现。
因为接口也可以包含静态方法,索性都测试了吧。
m4就是接口静态方法,也分别从接口和实现类来获取方法m4,并进行输出:
Method im4 = Inter.class.getDeclaredMethod("m4");
Method cm4 = Impl.class.getMethod("m4");
输出如下:
public static java.lang.String org.cnt.java.reflect.method.Inter.m4()
NoSuchMethodException
从接口获取静态方法正常,从实现类获取静态方法报错。表明实现类不会继承接口的静态方法。
通过反射调用接口静态方法:
Log.log(im4.invoke(null));
静态方法属于类(也称类型)本身,调用时不需要对象,所以参数传null(或任意对象都行)即可。
也可以使用接口直接调用静态方法:
Log.log(Inter.m4());
输出结果自然都是Inter.m4。
编程新说注:实现类不能调用接口的静态方法,接口的静态方法只能由接口本身调用,但子类可以调用父类的静态方法。
字段的继承问题
我也是脑洞大开,竟然想到用字段进行测试。那就开始吧。
先准备一个父类,含有三个字段。
public class Parent3 {
public String f1 = "Parent3.f1";
protected String f2 = "Parent3.f2";
private String f3 = "Parent3.f3";
}
再准备一个子类,继承父类,且含有三个相同的字段。
public class Child3 extends Parent3 {
public String f1 = "Child3.f1";
protected String f2 = "Child3.f2";
private String f3 = "Child3.f3";
}
纳尼,子类可以定义和父类同名的字段,而且也不报错,关键IDE也没有提示。
请允许我吐槽几句,人们都说C#是一门优雅的语言,优雅在哪里呢?来见识下。
先写基类(C#里喜欢叫基类,Java里喜欢叫父类):
public class CsBase {
public string name = "李新杰";
}
再写继承类:
public class CsInherit : CsBase {
new public string name = "编程新说";
}
看到了吧,子类要想覆盖(即遮罩)父类里的成员,需要加一个new关键字,提示一下写代码的人,让他知道自己在干什么,别无意间弄错了。
这就是优雅,而Java呢,啥玩意儿都没有,存在出错的风险吧,当然其实一般也没有问题。
一吐为快:
C#就是一杯咖啡,即使不加奶不加糖不需要搅拌的时候也会给你一把小勺子,让你随意的搅动两下,体现一下优雅。
Java就是一个大蒜,不仅听到后就掉了档次,而且有人吃的时候连蒜皮都不剥,直接用嘴咬,然后再把皮吐出来。
这是以前郭德纲和周立波互喷的时候说的喝咖啡的高雅,吃大蒜的低俗,我这里借鉴过来再演绎一下,哈哈。
简单自嗨一下,不必当真,Java和C#在语法上的细节差异,主要是语言之父们的哲学思维不同,但是都说得通。
这就像是,靠左走还是靠右走好呢?没啥区别,定好规则即可。
言归正传,分别获取子类和父类的f1字段并进行输出:
public java.lang.String org.cnt.java.reflect.method.Parent3.f1
public java.lang.String org.cnt.java.reflect.method.Child3.f1
pf1.equals(cf1) -> false
这两个字段不相等。
然后分别实例化父类和子类:
Parent3 p = new Parent3();
Child3 c = new Child3();
父类字段分别在父类和子类实例上反射调用:
Log.log(pf1.get(p));
Log.log(pf1.get(c));
输出如下:
Parent3.f1
Parent3.f1
可以看到,输出的都是父类的字段值。
子类字段分别在父类和子类对象上反射调用:
Log.log(cf1.get(p));
Log.log(cf1.get(c));
输出如下:
IllegalArgumentException
Child3.f1
子类字段在父类对象上反射调用时报错。
子类字段在子类对象上反射调用时输出的是子类的字段值。
用相同的方法对字段f2和f3进行测试,得到的结果是一样的。即使一个是protected的,一个是private的。
结论
看了这么多,相信都已迫不及待的想知道结论了。那就一起总结下吧。
总的来看,反射调用输出的结果和直接使用对象调用是一样的,说明反射调用也是支持面向对象的多态特性的。不然就乱套了嘛。
使用对象调用时,会根据运行时对象的具体类型,找出该类型对父类方法的重写版本或继承版本,然后再在对象上调用这个版本的方法。
对于反射也是完全一样的,它也关注这两个东西,哪个方法和哪个运行时对象。
反射调用与继承重写结合后的规则是这样的:
对于public和protected的方法,由于可以被继承与重写,所以真正起作用的是运行时对象,跟方法(反射获取的Method)无关。
无论它是从接口获取的,还是从父类获取的,或是从子类获取的,或者说是抽象的,都无所谓,关键看在哪个对象上调用。
对于private的方法,由于不能被继承与重写,所以真正起作用的就是方法(反射获取的Method)本身,而与运行时对象无关。
对于public和protected的字段,可以被继承,但是面向对象规定字段是不可以被重写的,所以真正起作用的就是字段(反射获取的Field)本身,而与运行时对象无关。
对于private的字段,不可以被继承,也不能被重写,所以真正起作用的就是字段(反射获取的Field)本身,而与运行时对象无关。
哈哈,应该明白过来了吧,这不就是面向对象的特性嘛,谁说不是呢。因为反射调用也是要遵从面向对象的规则的。
还有一点,父类的字段和方法可以在子类对象上反射调用,因为子类是父类的一个特殊分支,子类继承了父类嘛。
但是,子类自己定义的字段与方法或者重写了的方法,不可以在父类对象上反射调用,因为父类不能转换为子类。
好比,可以说人是动物,但反过来,说动物是人就不对了。测试中遇到的报错就属于这种情况,这种规则也是面向对象规定的。
这就是反射和面向对象结合的惊艳,如果都明白了文章中的示例,那也就明白了这种惊艳。
此外,反射至少还有以下两个好处:
1)写法统一,不管什么类的什么方法,都是method.invoke(..)来调用,很适合用作框架开发,因为框架要求的就是统一模型或写法。
2)支持了面向对象的特征,且突破了面向对象的限制,因为反射可以调用父类的私有方法和私有字段,还可以在类的外面调用它的私有和受保护的方法和字段。
我之前写过一篇分析面向对象的文章《三个臭皮匠的OO哲学,from C++、C# and Java》,那里有比较深刻的思考,推荐一看。
示例完整源码:
https://github.com/coding-new-talking/java-code-demo.git
>>> 热门文章集锦 <<<
爸爸又给Spring MVC生了个弟弟叫Spring WebFlux
【面试】吃透了这些Redis知识点,面试官一定觉得你很NB(干货 | 建议珍藏)
【面试】如果你这样回答“什么是线程安全”,面试官都会对你刮目相看(建议珍藏)
【面试】迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章(快快珍藏)
【面试】一篇文章帮你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生(深度好文,建议珍藏)
作者是工作超过10年的码农,现在任架构师。喜欢研究技术,崇尚简单快乐。追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。下面是公众号的二维码,欢迎关注!

【Java】反射调用与面向对象结合使用产生的惊艳的更多相关文章
- Java 反射 调用私有域和方法(setAccessible)
Java 反射 调用私有域和方法(setAccessible) @author ixenos AccessibleObject类 Method.Field和Constructor类共同继承了Acces ...
- 利用java反射调用类的的私有方法--转
原文:http://blog.csdn.net/woshinia/article/details/11766567 1,今天和一位朋友谈到父类私有方法的调用问题,本来以为利用反射很轻松就可以实现,因为 ...
- 通过Java反射调用方法
这是个测试用的例子,通过反射调用对象的方法. TestRef.java import java.lang.reflect.Method; import java.lang.reflect.In ...
- java反射调用dubbo接口
需求:项目增加幂等 场景:1.三个项目:a .b.c2.a项目加幂等3.b项目dubbo调用项目a的时候超时没有获取返回结果,增加重试机制(非立即重试,3min or 5min 后重试)4.c项目是一 ...
- Java 反射 调用私有构造方法
单例类: package singleton; public class SingletonTest { // 私有构造方法 private SingletonTest(){ System.out.p ...
- java黑魔法-反射机制-02-通过Java反射调用其他类方法
package com.aaron.reflect; import java.lang.reflect.Method; import java.lang.reflect.InvocationTarge ...
- java反射调用api
cglib的fastmethod 简单示例: FastClass serviceFastClass = FastClass.create(Person.class); Person p = new P ...
- JAVA反射调用方法
1.用户类 package com.lf.entity; import com.lf.annotation.SetProperty; import com.lf.annotation.SetTable ...
- Java 反射调用的一种优化
写一些Java框架的时候,经常需要通过反射get或者set某个bean的field,比较普通的做法是获取field后调用java.lang.reflect.Field.get(Object),但每次都 ...
随机推荐
- textarea 是否换行的问题解决
需求:判断当前textarea是否已经换行(这个换行有2种方式:1.不断输入文字直到超过指定宽度后自动换行:2.按了回车以后进行换行) 单纯的解决第二种换行很简单.网上提供了很多常规的解决方案. De ...
- 通过samus驱动实现基本数据操作
传统的关系数据库一般由数据库(database).表(table).记录(record)三个层次概念组成,MongoDB是由(database).集合(collection).文档对象(documen ...
- response读取图片+下载图片
读取图片 import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import ...
- python自动化测试技术-Allure
文末有源码 大部分人可能做的是爬虫和web,数据分析方面的工作,今天分享个在自动化测试领域python能做什么样的事情,比如下方,是用python+pytest+allure生成的精美自动化测试报告, ...
- RxJava学习笔记(操作符)
前言 上一篇文章介绍了RxJava的基础知识和简单实现,篇幅已经比较多了,所以把操作符(Operators)相关的内容放在这一篇.有了上一篇文章的基础,相信会比较容易理解操作符相关的内容了. 操作符( ...
- 乱世兄弟(豹老头 X 天捣臼)
论CP之冷冷冷 只为白凡扫剧- 片源: 乱世兄弟 BGM:兄弟 人物角色: 豹老头 - 白凡 天捣臼 大专栏 乱世兄弟(豹老头 X 天捣臼)- 姚鲁 B站:豹老头 X 天捣臼-MV<乱世兄弟& ...
- C++学习之旅
到现在为止学习C++也已经有一个半月了.一个半个月里我怀着好奇与敬畏一步步的走来,一步步的走向C++的内心深处,也发现了C++"内心的复杂".虽有坎坷,但从未放弃. 我承认,我不是 ...
- Java入门教程十(抽象类接口内部类匿名类)
抽象类(abstract) 一个类只定义了一个为所有子类共享的一般形式,至于细节则交给每一个子类去实现,这种类没有任何具体的实例,只具有一些抽象的概念,那么这样的类称为抽象类. 在面向对象领域,抽象类 ...
- 达拉草201771010105《面向对象程序设计(java)》第七周学习总结
达拉草201771010105<面向对象程序设计(java)>第七周学习总结 实验七继承附加实验 实验时间 2018-10-11 1.实验目的与要求 (1)进一步理解4个成员访问权限修饰符 ...
- Chrome 插件安装时报错: "CRX_HEADER_INVALID"
1. 将 crx 插件后缀名改为 rar 2. 解压 rar 包 3. 进入浏览器 1). 点击Chrome浏览器地址栏最右边的三个点 --> 更多工具 --> 扩展插件 --> 点 ...