转载

Java 反射调用与面向对象结合使用产生的惊艳

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

>>> 热门文章集锦 <<<

毕业10年,我有话说

【面试】我是如何面试别人List相关知识的,深度有点长文

我是如何在毕业不久只用1年就升为开发组长的

爸爸又给Spring MVC生了个弟弟叫Spring WebFlux

【面试】我是如何在面试别人Spring事务时“套路”对方的

【面试】Spring事务面试考点吐血整理(建议珍藏)

【面试】我是如何在面试别人Redis相关知识时“软怼”他的

【面试】吃透了这些Redis知识点,面试官一定觉得你很NB(干货 | 建议珍藏)

【面试】如果你这样回答“什么是线程安全”,面试官都会对你刮目相看(建议珍藏)

【面试】迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章(快快珍藏)

【面试】一篇文章帮你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生(深度好文,建议珍藏)

【面试】如果把线程当作一个人来对待,所有问题都瞬间明白了

Java多线程通关———基础知识挑战

品Spring:帝国的基石

作者 是工作超过 10年 的码农,现在任架构师。喜欢研究技术,崇尚简单快乐。 追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。 下面是公众号和知识星球的二维码,欢迎关注!

Java 反射调用与面向对象结合使用产生的惊艳 Java 反射调用与面向对象结合使用产生的惊艳

原文  https://mp.weixin.qq.com/s/H72wUEUgSNm6yRkKmTnfbQ
正文到此结束
Loading...