转载

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...