前言

这将是一个系列文章。原因是自己写了很多文章,也看了很多文章。从最开始的仅仅充当学习笔记,到现在认认真真去写文章去分享。中间发现了很多事情,其中最大发现是:收藏不看!总是想着先收藏以后有时间再看,到后来…大家都懂得。大多数文章仿佛石沉大海,失去了应有的价值。

因为技术文章大多需要比较重的思考,但是现如今时间碎片化很严重,因此收藏不看也实属不得已。所以萌生了这个系列的想法,系列文章的特点:以一些日常开发中不起眼的基础知识点为内核,围绕此包裹通俗易懂的文字。尽量用少思考的模式去讲述一个知识。让我们能够真正在碎片化的时间里学到东西!

出场角色

小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:这些内容,学习的时候还真没有好好的去思考...以后要加油了!

剧终