【Java】反射调用与面向对象结合使用产生的惊艳 痛定思痛。 2021-07-25 01:04 280阅读 0赞 > 我在看Spring的源码时,发现了一个隐藏的问题,就是父类方法(Method)在子类实例上的反射(Reflect)调用。 初次看到,感觉有些奇特,因为父类方法可能是抽象的或私有的,但我没有去怀疑什么,这可是Spring的源码,肯定不会有错。 不过我去做了测试,发现确实是正确的,那一瞬间竟然给我了一丝的惊艳。 这其实是面向对象(继承与重写,即多态)和反射结合的产物。下面先来看测试,最后再进行总结。 友情提示:测试内容较多,不过还是值得一看。 ## 具体方法的继承与重写 ## 先准备一个父类,有三个方法,分别是public,protected,private。 public class Parent { public String m1() { return "Parent.m1"; } //加入Java开发交流君样:756584822一起吹水聊天 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"; } } //加入Java开发交流君样:756584822一起吹水聊天 ```c public和protected是对父类方法的重写,private自然不能重写。 首先,通过反射获取父类和子类的方法m1,并输出: ```c 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)); //加入Java开发交流君样:756584822一起吹水聊天 输入如下: 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§); Log.log(cm1.invoke©); 输出如下: IllegalArgumentException Child.m1 子类方法在父类对象上反射调用时报错。 子类方法在子类对象上反射调用时输出Child.m1,这很好理解 按照同样的方式,对方法m2进行测试,得到的结果和m1一样。 它们一个是public的,一个是protected的,对于继承与重写来说是一样的。 然后再对方法m3进行测试,它是private的,看看会有什么不同。 首先,父类方法分别在父类和子类对象上反射调用: Log.log(pm3.invoke§); Log.log(pm3.invoke©); 输入如下: Parent.m3 Parent.m3 可以看到,输出的都是父类里的内容,和上面确实有所不同。 其次,子类方法分别在父类和子类对象上反射调用: Log.log(cm3.invoke§); Log.log(cm3.invoke©); 输出如下: 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"; } //加入Java开发交流君样:756584822一起吹水聊天 @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(); //加入Java开发交流君样:756584822一起吹水聊天 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"; } //加入Java开发交流君样:756584822一起吹水聊天 @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(); //加入Java开发交流君样:756584822一起吹水聊天 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() //加入Java开发交流君样:756584822一起吹水聊天 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 { //加入Java开发交流君样:756584822一起吹水聊天 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 = "李新杰"; } //加入Java开发交流君样:756584822一起吹水聊天 再写继承类: 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 //加入Java开发交流君样:756584822一起吹水聊天 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)); //加入Java开发交流君样:756584822一起吹水聊天 输出如下: IllegalArgumentException Child3.f1 子类字段在父类对象上反射调用时报错。 子类字段在子类对象上反射调用时输出的是子类的字段值。 用相同的方法对字段f2和f3进行测试,得到的结果是一样的。即使一个是protected的,一个是private的。 ## 结论 ## 看了这么多,相信都已迫不及待的想知道结论了。那就一起总结下吧。 总的来看,反射调用输出的结果和直接使用对象调用是一样的,说明反射调用也是支持面向对象的多态特性的。不然就乱套了嘛。 使用对象调用时,会根据运行时对象的具体类型,找出该类型对父类方法的重写版本或继承版本,然后再在对象上调用这个版本的方法。 对于反射也是完全一样的,它也关注这两个东西,哪个方法和哪个运行时对象。 ## 反射调用与继承重写结合后的规则是这样的: ## 对于public和protected的方法,由于可以被继承与重写,所以真正起作用的是运行时对象,跟方法(反射获取的Method)无关。 无论它是从接口获取的,还是从父类获取的,或是从子类获取的,或者说是抽象的,都无所谓,关键看在哪个对象上调用。 对于private的方法,由于不能被继承与重写,所以真正起作用的就是方法(反射获取的Method)本身,而与运行时对象无关。 对于public和protected的字段,可以被继承,但是面向对象规定字段是不可以被重写的,所以真正起作用的就是字段(反射获取的Field)本身,而与运行时对象无关。 对于private的字段,不可以被继承,也不能被重写,所以真正起作用的就是字段(反射获取的Field)本身,而与运行时对象无关。 哈哈,应该明白过来了吧,这不就是面向对象的特性嘛,谁说不是呢。因为反射调用也是要遵从面向对象的规则的。 还有一点,父类的字段和方法可以在子类对象上反射调用,因为子类是父类的一个特殊分支,子类继承了父类嘛。 但是,子类自己定义的字段与方法或者重写了的方法,不可以在父类对象上反射调用,因为父类不能转换为子类。 好比,可以说人是动物,但反过来,说动物是人就不对了。测试中遇到的报错就属于这种情况,这种规则也是面向对象规定的。 这就是反射和面向对象结合的惊艳,如果都明白了文章中的示例,那也就明白了这种惊艳。 ## 此外,反射至少还有以下两个好处: ## 1)写法统一,不管什么类的什么方法,都是method.invoke(…)来调用,很适合用作框架开发,因为框架要求的就是统一模型或写法。 2)支持了面向对象的特征,且突破了面向对象的限制,因为反射可以调用父类的私有方法和私有字段,还可以在类的外面调用它的私有和受保护的方法和字段。 ![image][] [**最新2020整理收集的一些高频面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等,需要获取这些内容的朋友请加Q君样:756584822**][2020_mysql_netty_spring_spring cloud_jvm_Q_756584822] [image]: /images/20210724/5a4d654dd69b4cd190623778e8b16a6f.png [2020_mysql_netty_spring_spring cloud_jvm_Q_756584822]: https://jq.qq.com/?_wv=1027&k=0IsBuUb0
还没有评论,来说两句吧...