重写、重载

183 阅读6分钟

构造器是否可以重写、重载?

由于构造器不会被继承,因此不能被重写。但可以重载。

子类自动调用父类构造器?

当父类有无参构造器时,子类不用显示调用。但是当父类没有无参构造器时,要求子类需要显示调用父类构造器。

重载(Overload)和重写(Override)的区别。重载的方法能 否根据返回类型进行区分?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态 性,而后者实现的是运行时的多态性。

重载: 发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不 同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。虽然在编译期无法通过返回值进行区分,但到了符号引用中,则可以通过返回值区分,例如修改字节码文件。

重写: 发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛 出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类 方法访问修饰符为private则子类中就不是重写。

解析:静态调用

class文件中保存的是符号引用,在类加载的解析阶段,需要将“ 编译期可知,运行期不可变 ”的方法的符号引用转换为直接引用,即解析。

“ 编译期可知,运行期不可变 ”指的是编译阶段可以确定方法调用的版本,并且在得到实际类型后,方法版本依旧不会改变。

解析是一个静态过程,编译期间就能完全确定符号引用,类加载时就能直接将符号引用转换为明确的直接引用,不必等到运行期完成。

什么是虚方法

在编译或解析阶段就可以知道方法调用的具体版本,这种方法成为非虚方法。反之,其他的都是虚方法。

虚方法包含:静态方法(与类型绑定)、私有方法(不会被子类重写)、实例构造器(与类型绑定)、父类方法(?)、final方法。

invokevirtual

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
  2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果 通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
    注意这里的实际类型:  例如用基类作为子类对象的引用,查找过程依旧是从子类开始的

分派

静态分派和重载

静态分派指的是:在编译期间通过调用对象的静态类型和参数的静态类型来确定调用哪个方法,并且会将这个方法的符号引用写入到 invokevirtual 指令的参数中,指令在执行时,回去查找真实的调用地址。

静态分派的典型应用就是:重载。

public class StaticDispatch {
    static abstract class Human {
    }

    static class Man extends Human {
    }

    static class Woman extends Human {
    }

    public void sayHello(Human guy) {
        System.out.println("hello,guy!");
    }

    public void sayHello(Man guy) {
        System.out.println("hello,gentleman!");
    }

    public void sayHello(Woman guy) {
        System.out.println("hello,lady!");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }
}
==========
hello,guy!
hello,guy!

什么是重载?

重载指的是:通过不同的参数类型、数量来实现同名方法的多个版本。是由静态分派来实现,在编译期已经能确定调用方法的符号引用。

\

静态类型和实际类型

Human man = new Man();

public void sayHello(Human guy);

sayHello(new Man());

上面两个例子:Human称为静态类型;Man称为实际类型。

Human human = (new Random()).nextBoolean() ? new Man() : new Woman();

// 静态类型变化

sr.sayHello((Man) human)

sr.sayHello((Woman) human)

\

首先实际类型在编译器无法预知,例如:例子中实际类型由随机数决定。而静态类型可以随着强转而发生变化,但这个变化是在编译器预知的。

在确定被调用对象的静态类型后,就可以根据参数来决定使用具体的重载方法。例子中被调用对象StaticDispatch,在StaticDispatch中通过参数的静态类型匹配对应的重载方法,并在编译时记录下这个方法的符号引用。

  1. 静态类型是写死在代码中的
  2. 而实际类型在编译期是无法预知的
  3. 两者都可以发生变化:静态类型会因为强制转换而发生变化,而实际类型类对象的不同而发生变化。

重载方法的如何匹配顺序?

\

动态分派与重写

什么是动态分派:静态分派确定方法的符号引用后,在运行期间,还需要根据调用对象的实际类型依据继承关系向上查找真实的调用方法。实现方式是: invokevirtual 指令

public class DynamicDispatch {
    static abstract class Human {
        protected abstract void sayHello();
    }

    static class Man extends Human {
        @Override
        protected void sayHello() {
            System.out.println("man say hello");
        }
    }

    static class Woman extends Human {
        @Override
        protected void sayHello() {
            System.out.println("woman say hello");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();

        man.sayHello();
        woman.sayHello();

        man = new Woman();
        man.sayHello();
    }
}
=========
man say hello
woman say hello
woman say hello

什么是重写?

重写指的是:子类对象重写了父类中同名同参的方法。底层依靠动态分派来实现,依据的是对象的实际类型类来确定重写版本。

子类也重载父类方法

子类的在调用show方法时,方法的符号引用是【tpf/notepaper/utils/OverWrite$C.show:(II)V】,指向的是子类C。虽然C中没有show:(II)V这个版本,但invokevirtual指令会在父类中继续查找。

\

字段不参与多态

public class FieldHasNoPolymorphic {
    static class Father {
        public int money = 1;

        public Father() {
            money = 2;
            showMeTheMoney();
        }

        public void showMeTheMoney() {
            System.out.println("I am Father, i have $" + money);
        }
    }

    static class Son extends Father {
        public int money = 3;

        public Son() {
            money = 4;
            showMeTheMoney();
        }

        public void showMeTheMoney() {
            System.out.println("I am Son, i have $" + money);
        }
    }

    public static void main(String[] args) {
        Father gay = new Son();
        System.out.println("This gay has $" + gay.money);
    }
}
=======
I am Son, i have $0
I am Son, i have $4
This gay has $2
  1. 输出两句都是“I am Son”,这是因为Son类在创建的时候,首先隐式调用了Father的构造函数
  2. 而 Father构造函数中对showMeTheMoney()的调用是一次虚方法调用,实际执行的版本是 Son::showMeTheMoney()方法,所以输出的是“I am Son”,这点经过前面的分析相信读者是没有疑问的 了。
  3. 而这时候虽然父类的money字段已经被初始化成2了,但Son::showMeTheMoney()方法中访问的却是子类的money字段,这时候结果自然还是0,因为它要到子类的构造函数执行时才会被初始化。
  4. main()的最后一句通过静态类型访问到了父类中的money,输出了2

单分派与多分派

方法的接受者和方法参数成为宗量,单分派依据一个宗量,而多分派依据多个宗量。

java中:静态分派需要依赖方法接受者和参数的静态类型,是多分派;动态分派只依据方法接收者的实际类型,属于单分派。所以:java是静态多分派、动态单分派的语言。