前言
这将是一个系列文章。原因是自己写了很多文章,也看了很多文章。从最开始的仅仅充当学习笔记,到现在认认真真去写文章去分享。中间发现了很多事情,其中最大发现是:收藏不看!总是想着先收藏以后有时间再看,到后来…大家都懂得。大多数文章仿佛石沉大海,失去了应有的价值。
因为技术文章大多需要比较重的思考,但是现如今时间碎片化很严重,因此收藏不看也实属不得已。所以萌生了这个系列的想法,系列文章的特点:以一些日常开发中不起眼的基础知识点为内核,围绕此包裹通俗易懂的文字。尽量用少思考的模式去讲述一个知识。让我们能够真正在碎片化的时间里学到东西!
出场角色
小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:那可不可以最多讲一讲加载方法的不同之处的?
JVM如何调用方法
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...
static重写
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:这些内容,学习的时候还真没有好好的去思考...以后要加油了!