方法的重写Overriding和重载Overloading是java多态性的不同表现,重写是父类与子类之间的多态性的一种表现,而重载则是主要是在同一个类中表现出多态性。
相关术语说明
- 多态:
polymorphism,在面向对象的程序设计语言中,多态是继数据封装和继承之后的第三个基本特征。多态是分离做什么和怎么做,从另一个角度将接口和实现分离开来,将改变的事物与未变的事物分离开来。多态也称作动态绑定、后期绑定或者运行时绑定。将一个方法调用和一个方法体连接起来就称为绑定,C语言在编译期就连接了实际运行的方法体,所以就是前期绑定。 - 方法重写:
Override,在子类中重写覆盖父类中对于子类可见的方法,方法重写也被称为运行时多态。 - 方法重载:
Overload,重载是在一个类里面,方法名字相同而参数不同,返回类型可以相同也可以不同的方法,可以是静态方法,也可以是实例方法,最常用的地方就是构造器的重载,方法重载也被称为编译时多态。 - 方法调用:方法调用不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,即调用哪个签名的方法,不涉及方法运行的内部细节,在编译期就已经确定。
- 方法执行:方法执行是在运行期,根据方法的接收者的实际类型,连接方法到JVM方法区的直接引用,执行引用指向的方法体。
- 方法签名:方法签名包括方法名称和参数的数量和类型,返回类型和抛出异常并不认为是一个方法签名的一部分。另外在JVM中还有一个字节码的特征签名,特征签名中还包括了方法返回值和受检异常表,从而在字节码中形成一个真正独一无二的方法ID。
- 方法体:运行期执行的实际类型的方法所包括的代码。
- 方法接收者:
Receiver,即运行期被执行方法的所有者,方法所在的实例对象。 - 静态方法:即类方法,而对象实例调用的方法则称为实例方法。
- 静态类型:方法签名中参数变量声明的类型称为静态类型(
static type)或者是外观类型(apparent type),而参数传入对象的实际类型(actual type)可以是静态类型的子类或者实现类。 - 实际类型:方法执行的接收者的实际类型,方法参数传入的实际类型,参数通过方法暴露出来的可能是接口名或者基类名,实际传入的是其子类或者实现类的实例。
- 符号引用:一个类文件,它会包含它引用的其他类的全名和描述符,并跟他们建立符号引用,符号引用是一种虚拟的,非物理连接的方式。java在编译期间已经将方法调用的完整符号引用生成出来,作为方法调用指令的参数存储到class文件中。这个符号引用包含了此方法定义在哪个具体类型中,方法的名字和参数顺序,参数类型和方法返回值等信息。反编译class文件可以看到,字节码的方法调用就以class常量池中指向方法的符号引用作为参数,这些符号引用一部分如static方法调用,在类加载解析阶段或者第一次使用时就被转化为直接引用,这部分称为静态解析,而虚方法的符号引用到运行期才会转化为直接引用,这部分称为动态连接。
- 直接引用:当程序第一次执行到符号引用的位置时,JVM会检查这个符号链接的正确性,然后建立真正的物理引用,即直接引用,使用直接引用指向一个类、字段或方法的指针或偏移量。虚拟机会记住这个直接引用,这样当它以后再次遇到同样的引用时,就可以直接使用,而不需要重新解析该符号引用了。
- 编译期:通过javac这样的编译器将java源文件编译生成class文件。
- 运行期:在JVM中把class文件翻译成机器语言后运行,
HotSpot中默认是编译器加解释器混合模式协作完成字节码的运行。 - 虚方法:除
final方法(private方法也是final方法)之外的所有实例方法就是虚方法,也就是public、protected、default的三种实例方法都是虚方法,虚方法对应的虚拟机指令是invokevirtual。 - 非虚方法:java中除了
static方法和final方法(private方法实际上是final方法)之外,其他所有方法都是虚方法,是后期绑定的,之所以将定义方法为final,为了避免被子类继承外,这样也可以有效的“关闭”动态绑定,告诉编译器不需要动态绑定,在类加载的解析阶段就会连接方法的直接引用。static方法和private方法与之对应的虚拟机指令是invokestatic和invokespecial,final方法的虚拟机指令是invokevirtual。构造方法是invokespecial指令执行的,不是static方法,因为默认传入了新new的对象,并在其中操作实例属性和方法,更多类似于实例方法,但却不是虚方法,即没有后期绑定的。 - 类加载:虚拟机通过类加载器对某个类进行加载操作,类加载会经过加载、验证、准备、解析、初始化、使用、卸载这些阶段,非虚方法的符号引用在解析阶段就会被连接到JVM方法区的目标方法的直接引用。
重写 - Override
在子类中重写覆盖父类中对于子类可见的方法,实现子类的特定功能,如需执行父类被覆盖的方法,可以通过super进行引用。
Override规则
- 参数列表必须完全与被重写方法的相同;
- 返回类型必须完全与被重写方法的返回类型相同;
- 访问权限不能比父类中被重写的方法的访问权限更高。例如:如果父类的一个方法被声明为
public,那么在子类中重写该方法就不能声明为protected。 - 父类的成员方法只能被它的子类重写。
- 声明为
final的方法不能被重写。 - 声明为
static的方法不能被重写,但是能够被再次声明。 - 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为
private和final的方法。 - 子类和父类不在同一个包中,那么子类只能够重写父类的声明为
public和protected的非final方法。 - 重写的方法能够抛出任何非受检异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的受检异常,或者比被重写方法声明的更广泛的受检异常,反之则可以。
- 重写的方法能够抛出更少或更有限的异常,也就是说,被重写的方法声明了异常,但重写的方法可以什么也不声明。
- 构造方法不能被重写。
- 如果不能继承一个方法,则不能重写这个方法。
Override示例
public class OverrideExample { static class OverrideParent { public void print(Number number) { System.out.println(number); System.out.println("Parent#print(Number) method print : " + this.getClass().getSimpleName()); } // 这个方法不但重载了 print(Number) 方法,而且被子类重写了 // 父类的被重写的方法抛出的异常要宽于子类抛出的异常 public void print(Integer integer) throws IOException { System.out.println(integer); System.out.println("Parent#print(Integer) method print : " + this.getClass().getSimpleName()); } } static class OverrideChild extends OverrideParent { public void print(Integer integer) throws EOFException { System.out.println(integer); System.out.println("Child#print(Number) method print : " + this.getClass().getSimpleName()); } } public static void main(String[] args) throws IOException { OverrideParent parent = new OverrideParent(); OverrideParent child = new OverrideChild(); Number number = new Long("1"); Integer integer = new Integer("2"); parent.print(number); parent.print(integer); child.print(number); child.print(integer); }} |
子类重写父类的方法时,可以不抛出异常,或者是更小的异常,比如示例中的IOException的子类EOFException,但不能抛出更广的异常,如Exception。
程序运行后输出:
1
Parent#print(Number) method print : OverrideParent
2
Parent#print(Integer) method print : OverrideParent
1
Parent#print(Number) method print : OverrideChild
2
Child#print(Number) method print : OverrideChild
Override示例运行结果详解
为了更好的说明方法重写,将上述代码生成的class文件反编译,查看class中生成的java字节码:
javap -c com.example.test.lang.OverrideExample |
-c Prints out disassembled code, that is, the instructions that comprise the Java bytecodes, for each of the methods in the class. These are documented in the Java Virtual Machine Specification.
Compiled from "OverrideExample.java"public class com.example.test.lang.OverrideExample { public com.example.test.lang.OverrideExample(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]) throws java.io.IOException; Code: 0: new #2 // class com/example/test/lang/OverrideExample$OverrideParent 3: dup 4: invokespecial #3 // Method com/example/test/lang/OverrideExample$OverrideParent."<init>":()V 7: astore_1 8: new #4 // class com/example/test/lang/OverrideExample$OverrideChild 11: dup 12: invokespecial #5 // Method com/example/test/lang/OverrideExample$OverrideChild."<init>":()V 15: astore_2 16: new #6 // class java/lang/Long 19: dup 20: ldc #7 // String 1 22: invokespecial #8 // Method java/lang/Long."<init>":(Ljava/lang/String;)V 25: astore_3 26: new #9 // class java/lang/Integer 29: dup 30: ldc #10 // String 2 32: invokespecial #11 // Method java/lang/Integer."<init>":(Ljava/lang/String;)V 35: astore 4 37: aload_1 38: aload_3 39: invokevirtual #12 // Method com/example/test/lang/OverrideExample$OverrideParent.print:(Ljava/lang/Number;)V 42: aload_1 43: aload 4 45: invokevirtual #13 // Method com/example/test/lang/OverrideExample$OverrideParent.print:(Ljava/lang/Integer;)V 48: aload_2 49: aload_3 50: invokevirtual #12 // Method com/example/test/lang/OverrideExample$OverrideParent.print:(Ljava/lang/Number;)V 53: aload_2 54: aload 4 56: invokevirtual #13 // Method com/example/test/lang/OverrideExample$OverrideParent.print:(Ljava/lang/Integer;)V 59: return} |
关于符号引用
javap反编译时加上-verbose参数可以看到更详细的信息,如主次版本,常量池中的类、方法、变量的符号引用,每个方法的栈深和局部变量数量等信息。
常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
- 类和接口的全限定名
- 字段名称和描述符
- 方法名称和描述符
javap -c -verbose com.example.test.lang.OverrideExample |
命令输出如下:
Classfile /Users/david/github.com/learning-programming/java/target/test-classes/com/example/test/lang/OverrideExample.class Last modified Nov 15, 2016; size 1117 bytes MD5 checksum bc597130acffb600f30c658477471ad3 Compiled from "OverrideExample.java"public class com.example.test.lang.OverrideExample minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #15.#41 // java/lang/Object."<init>":()V #2 = Class #42 // com/example/test/lang/OverrideExample$OverrideParent #3 = Methodref #2.#41 // com/example/test/lang/OverrideExample$OverrideParent."<init>":()V #4 = Class #43 // com/example/test/lang/OverrideExample$OverrideChild #5 = Methodref #4.#41 // com/example/test/lang/OverrideExample$OverrideChild."<init>":()V #6 = Class #44 // java/lang/Long #7 = String #45 // 1 #8 = Methodref #6.#46 // java/lang/Long."<init>":(Ljava/lang/String;)V #9 = Class #47 // java/lang/Integer #10 = String #48 // 2 #11 = Methodref #9.#46 // java/lang/Integer."<init>":(Ljava/lang/String;)V #12 = Methodref #2.#49 // com/example/test/lang/OverrideExample$OverrideParent.print:(Ljava/lang/Number;)V #13 = Methodref #2.#50 // com/example/test/lang/OverrideExample$OverrideParent.print:(Ljava/lang/Integer;)V #14 = Class #51 // com/example/test/lang/OverrideExample #15 = Class #52 // java/lang/Object #16 = Utf8 OverrideChild #17 = Utf8 InnerClasses #18 = Utf8 OverrideParent #19 = Utf8 <init> #20 = Utf8 ()V #21 = Utf8 Code #22 = Utf8 LineNumberTable #23 = Utf8 LocalVariableTable #24 = Utf8 this #25 = Utf8 Lcom/example/test/lang/OverrideExample; #26 = Utf8 main #27 = Utf8 ([Ljava/lang/String;)V #28 = Utf8 args #29 = Utf8 [Ljava/lang/String; #30 = Utf8 parent #31 = Utf8 Lcom/example/test/lang/OverrideExample$OverrideParent; #32 = Utf8 child #33 = Utf8 number #34 = Utf8 Ljava/lang/Number; #35 = Utf8 integer #36 = Utf8 Ljava/lang/Integer; #37 = Utf8 Exceptions #38 = Class #53 // java/io/IOException #39 = Utf8 SourceFile #40 = Utf8 OverrideExample.java #41 = NameAndType #19:#20 // "<init>":()V #42 = Utf8 com/example/test/lang/OverrideExample$OverrideParent #43 = Utf8 com/example/test/lang/OverrideExample$OverrideChild #44 = Utf8 java/lang/Long #45 = Utf8 1 #46 = NameAndType #19:#54 // "<init>":(Ljava/lang/String;)V #47 = Utf8 java/lang/Integer #48 = Utf8 2 #49 = NameAndType #55:#56 // print:(Ljava/lang/Number;)V #50 = NameAndType #55:#57 // print:(Ljava/lang/Integer;)V #51 = Utf8 com/example/test/lang/OverrideExample #52 = Utf8 java/lang/Object #53 = Utf8 java/io/IOException #54 = Utf8 (Ljava/lang/String;)V #55 = Utf8 print #56 = Utf8 (Ljava/lang/Number;)V #57 = Utf8 (Ljava/lang/Integer;)V{ public com.example.test.lang.OverrideExample(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 9: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/example/test/lang/OverrideExample; public static void main(java.lang.String[]) throws java.io.IOException; descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=5, args_size=1 0: new #2 // class com/example/test/lang/OverrideExample$OverrideParent 3: dup 4: invokespecial #3 // Method com/example/test/lang/OverrideExample$OverrideParent."<init>":()V 7: astore_1 8: new #4 // class com/example/test/lang/OverrideExample$OverrideChild 11: dup 12: invokespecial #5 // Method com/example/test/lang/OverrideExample$OverrideChild."<init>":()V 15: astore_2 16: new #6 // class java/lang/Long 19: dup 20: ldc #7 // String 1 22: invokespecial #8 // Method java/lang/Long."<init>":(Ljava/lang/String;)V 25: astore_3 26: new #9 // class java/lang/Integer 29: dup 30: ldc #10 // String 2 32: invokespecial #11 // Method java/lang/Integer."<init>":(Ljava/lang/String;)V 35: astore 4 37: aload_1 38: aload_3 39: invokevirtual #12 // Method com/example/test/lang/OverrideExample$OverrideParent.print:(Ljava/lang/Number;)V 42: aload_1 43: aload 4 45: invokevirtual #13 // Method com/example/test/lang/OverrideExample$OverrideParent.print:(Ljava/lang/Integer;)V 48: aload_2 49: aload_3 50: invokevirtual #12 // Method com/example/test/lang/OverrideExample$OverrideParent.print:(Ljava/lang/Number;)V 53: aload_2 54: aload 4 56: invokevirtual #13 // Method com/example/test/lang/OverrideExample$OverrideParent.print:(Ljava/lang/Integer;)V 59: return LineNumberTable: line 38: 0 line 39: 8 line 41: 16 line 42: 26 line 44: 37 line 45: 42 line 47: 48 line 48: 53 line 49: 59 LocalVariableTable: Start Length Slot Name Signature 0 60 0 args [Ljava/lang/String; 8 52 1 parent Lcom/example/test/lang/OverrideExample$OverrideParent; 16 44 2 child Lcom/example/test/lang/OverrideExample$OverrideParent; 26 34 3 number Ljava/lang/Number; 37 23 4 integer Ljava/lang/Integer; Exceptions: throws java.io.IOException}SourceFile: "OverrideExample.java"InnerClasses: static #16= #4 of #14; //OverrideChild=class com/example/test/lang/OverrideExample$OverrideChild of class com/example/test/lang/OverrideExample static #18= #2 of #14; //OverrideParent=class com/example/test/lang/OverrideExample$OverrideParent of class com/example/test/lang/OverrideExample |
上面第56行虚拟机指令invokevirtual的参数是一个符号引用#13,而#13在常量池中是由#2.#50组合形成的,再从常量池里找到#2和#50的符号引用,组合之后就看到了调用方法中符号引用的完整信息。
#2 = Class #42 // com/example/test/lang/OverrideExample$OverrideParent#50 = NameAndType #55:#57 // print:(Ljava/lang/Integer;)V |
由反编译生成的class文件可以看到其中第39、45、50、56行的invokevirtual指令指向的符号引用都是OverrideExample$OverrideParent.print()V方法,尤其是注意第56行,其符号引用还是指向父类的方法。编译期只知道接收者的声明时的静态类型和方法参数的静态类型,并且根据这些静态类型,确定了要执行哪个方法。
静态方法也可以有方法重载,因为静态方法与final的实例方法无须对方法接收者进行多态选择,所以在类加载的解析阶段就会把涉及的符号引用直接转变为实际要执行方法的直接引用。
编译期只是确定了做什么(调用哪个签名的方法),而怎么做却是在运行期决定的(执行哪个实例对象的方法体)。编译期并不知道这个方法要在哪个对象上执行,其执行的方法体是什么,这需要到运行期才会绑定,所以从多态这个概念是运行期绑定的角度来说,只有方法重写才是java多态的表现形式。
java代码运行中大致会经历以下这些步骤:
- 在JVM执行之前,需要先将java源码进行编译。
- 在编译期根据方法参数的静态类型就已经决定了需要调用哪个签名的重载方法版本。方法调用不等同于方法执行,编译器此时知道参数的静态类型,并以此为依据将对应的这个方法的符号引用写到方法的
invokevirtual指令的参数中,但编译器此时是不知道方法是在哪个对象上执行的,这需要到在JVM中的运行期才会绑定,方法调用在class文件中存储的都只是方法的符号引用,而不是方法实际运行的JVM方法区内存中的直接引用。 - JVM会加载执行
main()方法所在的类,及方法运行中使用new指令引入的相关类。 - 类加载会经过加载、验证、准备、解析、初始化、使用、卸载这些阶段,非虚方法的符号引用在解析阶段就会被连接到JVM方法区的目标方法的直接引用。
- 编译期确定了方法调用的符号引用,在实例方法运行时,再根据方法的接收者(即当前实例),真正绑定目标方法在内存中的直接引用,这就是java的动态绑定,或者叫运行时绑定,这个过程也就是java方法重写的本质,因此方法重写是在运行期才体现出作用。
上面示例程序中前面2个输出结果属于方法重载,方法接收者是同一个对象,根据重载方法的参数静态类型不同,执行相应方法。
程序第3个输出child.print(number)的结果,因为子类并没有重写父类的对应方法,所以子类运行的就是父类继承的方法。
最后一行代码child.print(integer)输出体现了方法重写的作用,在编译期确定了方法调用的版本,在运行期时确定接收者的实际类型为child对象,然后在child对象中检查是否有此签名的方法,因为child对象中正好重写了父类的此方法,此时连接方法的符号引用到子类重写方法的直接引用上,调用子类重写的方法,这就是java中的运行时绑定,也叫延迟绑定,是java多态的主要表现方式。
重载 - Overload
方法重载是在一个类里面,方法名字相同,而参数不同,返回类型可以相同也可以不同。每个重载的方法都必须有一个独一无二的参数类型列表。
方法重载在类编译时就已经确定了方法调用的版本。
Overload规则
- 被重载的方法必须改变参数列表。
- 被重载的方法可以改变返回类型 。
- 被重载的方法可以改变访问修饰符。
- 被重载的方法可以声明新的或更广的受检异常。
- 方法能够在同一个类中或者在一个子类中被重载。
Overload示例
public class OverloadExample { static class OverloadParent { public void print(Number number) { System.out.println(number); System.out.println("Parent#print(Number) method print : " + this.getClass().getSimpleName()); } public long print(Long number) { System.out.println(number); System.out.println("Parent#print(Long) method print : " + this.getClass().getSimpleName()); // 重载方法可以修改返回类型 return number.longValue(); } // public void print(Integer integer) throws IOException { // System.out.println(integer); // System.out.println("Parent#print(Number) method print : " + this.getClass().getSimpleName()); // } } static class OverloadChild extends OverloadParent { // @Override public void print(Integer integer) throws EOFException { System.out.println(integer); System.out.println("Child#print(Integer) method print : " + this.getClass().getSimpleName()); } } public static void main(String[] args) throws Exception { OverloadParent parent = new OverloadParent(); OverloadParent child = new OverloadChild(); Number number = new Long("1"); Integer integer = new Integer("2"); parent.print(number); parent.print((Long) number); // downcast Number to Long child.print(number); child.print(integer); // upcast Integer to Number ((OverloadChild) child).print(integer); }} |
上例程序运行输出:
1
Parent#print(Number) method print : OverloadParent
1
Parent#print(Long) method print : OverloadParent
1
Parent#print(Number) method print : OverloadChild
2
Parent#print(Number) method print : OverloadChild
2
Child#print(Integer) method print : OverloadChild
Overload示例运行结果详解
反编译OverloadExample代码:
javap -c com.example.test.lang.OverloadExample |
命令输出如下:
Compiled from "OverloadExample.java"public class com.example.test.lang.OverloadExample { public com.example.test.lang.OverloadExample(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]) throws java.lang.Exception; Code: 0: new #2 // class com/example/test/lang/OverloadExample$OverloadParent 3: dup 4: invokespecial #3 // Method com/example/test/lang/OverloadExample$OverloadParent."<init>":()V 7: astore_1 8: new #4 // class com/example/test/lang/OverloadExample$OverloadChild 11: dup 12: invokespecial #5 // Method com/example/test/lang/OverloadExample$OverloadChild."<init>":()V 15: astore_2 16: new #6 // class java/lang/Long 19: dup 20: ldc #7 // String 1 22: invokespecial #8 // Method java/lang/Long."<init>":(Ljava/lang/String;)V 25: astore_3 26: new #9 // class java/lang/Integer 29: dup 30: ldc #10 // String 2 32: invokespecial #11 // Method java/lang/Integer."<init>":(Ljava/lang/String;)V 35: astore 4 37: aload_1 38: aload_3 39: invokevirtual #12 // Method com/example/test/lang/OverloadExample$OverloadParent.print:(Ljava/lang/Number;)V 42: aload_1 43: aload_3 44: checkcast #6 // class java/lang/Long 47: invokevirtual #13 // Method com/example/test/lang/OverloadExample$OverloadParent.print:(Ljava/lang/Long;)J 50: pop2 51: aload_2 52: aload_3 53: invokevirtual #12 // Method com/example/test/lang/OverloadExample$OverloadParent.print:(Ljava/lang/Number;)V 56: aload_2 57: aload 4 59: invokevirtual #12 // Method com/example/test/lang/OverloadExample$OverloadParent.print:(Ljava/lang/Number;)V 62: aload_2 63: checkcast #4 // class com/example/test/lang/OverloadExample$OverloadChild 66: aload 4 68: invokevirtual #14 // Method com/example/test/lang/OverloadExample$OverloadChild.print:(Ljava/lang/Integer;)V 71: return} |
从反编译的第39、47行可以看到,编译期根据重载方法签名中的参数静态类型就确定了方法调用的版本,其中第44行中checkcast将Number对象向下强转为Long对象,从而编译期就能匹配到重载的方法OverloadParent.print:(Ljava/lang/Long;)J。
child.print(number)位于反编译的第53行,child对象在编译期的静态类型为OverloadParent,编译期就根据方法签名匹配到OverloadParent.print:(Ljava/lang/Number;)V,并且这个方法后面没有被子类重写,运行期就直接绑定到父类的OverloadParent.print:(Ljava/lang/Number;)V这个方法上。
child.print(integer)对应于反编译的第59行,因为编译期child对象的静态类型为OverloadParent,而OverloadParent中并没有OverloadParent.print:(Ljava/lang/Integer;)V签名的重载方法,这时编译器自动将Integer向上转型为Number类型的,并匹配到签名为OverloadParent.print:(Ljava/lang/Number;)V的方法,因为向上转型是安全的,所以在反编译的字节码中并没有checkcast指令。
从反编译的字节码中可以看到第63行中,将对象强制向下转型为OverloadChild,因为向下转型并不是安全的,所以有个checkcast指令判断,之后编译器就在第68行连接到OverloadChild.print:(Ljava/lang/Integer;)V方法。
Difference of Overloaded and Overridden Methods
| Difference | Overloaded | Overrided |
|---|---|---|
| argument list | Must change | Must not change |
| return type | Can change | Must not change |
| exceptions | Can change | Can reduce or eliminate. Must not throw new or broader checked exceptions |
| access | Can change | Must not make more restrictive (can be less restrictive) |
| invocation | Reference type determines which overloaded version (based on declared argument types) is selected. Happens at compile time. The actual method that's invoked is still a virtual method invocation that happens at runtime, but the compiler will already know the signature of the method to be invoked. So at runtime. the argument match will already have been nailed down. just not the actual class in which the method lives. | Object type (in other words. the type of the actual instance on the heap) determines which method is selected. Happens at runtime. |