这将是一个系列文章。原因是自己写了很多文章,也看了很多文章。从最开始的仅仅充当学习笔记,到现在认认真真去写文章去分享。中间发现了很多事情,其中最大发现是:收藏不看!总是想着先收藏以后有时间再看,到后来…大家都懂得。大多数文章仿佛石沉大海,失去了应有的价值。
因为技术文章大多需要比较重的思考,但是现如今时间碎片化很严重,因此收藏不看也实属不得已。所以萌生了这个系列的想法,系列文章的特点:以一些日常开发中不起眼的基础知识点为内核,围绕此包裹通俗易懂的文字。尽量用少思考的模式去讲述一个知识。让我们能够真正在碎片化的时间里学到东西!
小A:刚踏入Java编程之路…
MDove:一个快吃不上饭的Android开发…
小A:MDove,我最近遇到一个问题百思不得其解。
MDove:正常,毕竟你这智商1+1都不知道为什么等于2。
小A:那1+1为啥等于2呢?
MDove:......说你遇到的问题。
小A:是这样的,我在学习多态的时候,重载和重写,有点蒙圈了...
public class MethodMain { public static void main(String[] args) { MethodMain main = new MethodMain(); Language language = new MethodMain().new Java(); Language java = new MethodMain().new Java(); main.sayHi(language); main.sayHi(java); } private void sayHi(Java java) { System.out.println("Hi Java"); } private void sayHi(Language language) { System.out.println("Im Language"); } public class Java extends Language {} public abstract class Language {} }
小A:程序运行结果为什么是这个呀?我觉得它应该一个是Im Language一个是Hi Java呀。
MDove:原来是这个疑惑呀。好,那今天就好好聊一聊重载/重写背后:方法调用的原理。为了更好理解,我尽量不用学术性强的语言来解释。开始之前让我们先看一行代码:
如果想了解更专业的内容,可以参考《Java虚拟机规范》或者《深入理解Java虚拟机》。
A a = new B();
MDove:对于A和B来说,他们有不同的学术名词。A称之为 静态类型 ,B称之为 实际类型 。对于 Language language = new MethodMain().new Java();
也是如此:Language是 静态类型 ,Java是 实际类型 。
MDove:从你写的demo里,我们可以看出来: main.sayHi(language); main.sayHi(java);
最终都是调用了 private void sayHi(Language language)
。我们是不是可以得出一个结论:方法的调用是根据 静态类型 去匹配的?
就像你的那个demo一样,language和java的 静态类型 都是Language所以就匹配了 private void sayHi(Language language)
这个方法。
小A:不对啊!!!如果用Override,重写的话,这个结论是不成立的!
public class MethodMain { public static void main(String[] args) { Language language = new MethodMain().new Java(); language.sayHi(); } public class Java extends Language { @Override public void sayHi() { System.out.println("Hi,Im Java"); } } public class Language { public void sayHi() { System.out.println("Hi,Im Language"); } } }
MDove:别急,你这是面向对象多态神经紊乱综合征。说白了就是看串了。你难道不觉得,这俩个demo写法上有不同么?或者再上升一下重载和重写是不是有不同之处?
小A:你这么一说好像真是! 重载 是在 一个类里边折腾 ;而 重写 是 子类折腾父类 。
MDove:没错,正式如此。导致了JVM在加载方法的时候采用了不同的方式。因此也就有了你所感到疑惑的,为什么重载会是这种结果,而重写会是那种结果。
小A:那可不可以最多讲一讲加载方法的不同之处的?
MDove:将调用之前,我们再回到上文提到的 静态类型 上。对于JVM来说,在编译期变量的 静态类型 是确定的,同样重载的方法也就能够确定。很好理解,因为二者都是确定无误的。所以对于这种方法,JVM采用 静态分派 的方式去调用。
MDove:说白了就是, 在编译期就决定好该怎么调用这个方法 。因此对于在运行期间生成的 实际类型 JVM是不关心的。只要你的 静态类型 是郭德纲,就算你new一个吴亦凡出来。这行代码也不能又长又宽...
小A:照这个逻辑来说,重写就是 动态分派 ,需要JVM在运行期间确定对象的 实际类型 ,然后再决定调用哪个方法。
MDove:没错,毕竟 重写 涉及到你是调用 子类 的方法还是调用 父类 。因此需要在运行期间去决定。当然我们用嘴说是很轻巧的,实际JVM去执行时是很复杂的过程。如果你感兴趣可以去了解这方面的知识。
MDove:因为 重载 的性质,重载在可变参数上是有坑的。我写的demo,你瞅瞅能不能感觉出奇怪的地方:
public class MethodMain { public static void main(String[] args) { MethodMain main = new MethodMain(); main.fun(null, 666); main.fun(null, 666, 666); } private void fun(Object obj, Object... args) { System.out.println("fun(Object obj, Object... args)"); } private void fun(String string, Object obj, Object... args) { System.out.println("fun(String string, Object obj, Object... args)"); } }
小A:我觉得应该是打印:fun(Object obj, Object... args)和fun(String string, Object obj, Object... args)吧?
MDove:最开始我也是这么认为的。我们从我们的角度出发,很自然的认为 main.fun(null, 666);
应该调用 private void fun(Object obj, Object... args)
,而 main.fun(null, 666, 666);
去调用 private void fun(String string, Object obj, Object... args)
。
MDove:可以如果我们站在程序的角度呢?因为我们写的是可变参数,程序怎么可能知道666和666,666应该去对应哪个方法。所以这个demo的结果是:
小A:那我有一个疑问,既然程序很难洞察应该调用哪个可变参数的方法,那它又是为什么调用了下边的而不是上边的呢?
MDove:那是因为编译期在匹配方法时,如果有多个可能性,它会使用更向下的类型,结合上述的demo。因为我们传入null时,虽然即能满足Object又能满足String。但由于String是 Object的子类(也就是更向下),因此编译器会认为第二个方法更为贴切。
小A:Skr,Skr...
MDove:我们继续聊一聊重写,咱们说了普通的重写。静态的重写岂能不提。首先来说static不能称之为重写,只能叫做隐藏父类实现。文字很抽象,直接看代码:
public class MethodMain { public static void main(String[] args) { Language.sayHi(); Java.sayHi(); } } public class Java extends Language { public static void sayHi() { System.out.println("Hi,Im Java"); } } public class Language { public static void sayHi() { System.out.println("Hi,Im Language"); } }
MDove:说白了就是: 老子是老子的,儿子是儿子的 。其实这个也比较好理解。static的变量、方法都是伴随类存在的,类加载完毕就生成了。它和对象new不new是没有关系的。因此也不存在什么 实际变量 一说。因此也就有了上边的这种情况,也就是常说的:隐藏父类。
小A:这些内容,学习的时候还真没有好好的去思考...以后要加油了!