在多态的方法调用中为什么会出现“左边编译左边运行”的现象?多态创建的对象到底是谁属于父类还是子类?通过深扒集合remove方法调用理解其原理
目录
“左边编译左边运行”的两个原因:
什么是“编译看左边,运行看右边”?
为什么会出现“左边编译左边运行”现象?
1. 子类没有重写父类的方法
2. 重载与重写的混淆(重难点)
问题:编译器是怎么看一个方法是重写还是重载的呢?
区分方式:查看方法的签名
如何避免“左边编译左边运行”的限制?
拓展:多态创建的对象到底是谁属于父类还是子类?
1. 对象本质属于子类
2. 引用属于父类
3. 编译时看引用类型,运行时看对象类型
编译时:
运行时:
4. 多态性不改变对象的实际类型
5. 总结
总结
在 Java 的多态机制中,“编译看左边,运行看右边” 是一个非常常见的规则,它描述了 Java 在编译时和运行时对方法调用的不同处理方式。然而,有时候我们会遇到一种情况,即使对象的实际类型是子类,编译器依然只允许调用父类的方法,这种现象就是所谓的“左边编译左边运行”。本文将详细解释这种现象及其背后的原因。
“左边编译左边运行”的两个原因:
- 子类没有重写父类的方法
- 重载与重写的混淆
知道了原因后,我们下面来进行进一步的深挖。
什么是“编译看左边,运行看右边”?
在 Java 中,多态允许我们使用父类的引用指向子类对象。这种机制下,方法的调用行为可以用“编译看左边,运行看右边”来描述:
编译时看左边:编译器根据变量的声明类型(即左边的类型)来确定哪些方法是合法的。换句话说,编译器会检查父类或接口中是否存在要调用的方法,如果存在,编译通过。(看看父类或者接口中有没有调用的方法)
运行时看右边:在程序运行时,实际的对象类型(即右边的类型)决定了具体执行哪个版本的方法。也就是说,如果子类重写了父类的方法,运行时将执行子类的重写版本。(如果父类中有该方法则执行子类重写的该方法)
然而,有时我们会遇到一种似乎只看“左边”的情况,也就是所谓的“左边编译左边运行”。让我们深入了解这种现象。
为什么会出现“左边编译左边运行”现象?
尽管“编译看左边,运行看右边”是多态的核心原则,但在某些情况下,我们确实会看到编译器似乎只根据左边的类型来限制方法的调用。这种现象主要发生在以下两种情况下(上面已经提及过):
子类没有重写父类方法、重载与重写的混淆
1. 子类没有重写父类的方法
在多态中,当我们通过父类的引用调用一个方法时,如果子类没有重写父类中的该方法,编译器只能调用父类的方法。这种情况导致即使子类对象在运行时被赋给父类引用,程序依然会执行父类中的方法,而不会调用子类中的逻辑。
示例:
class Animal {public void makeSound() {System.out.println("Animal makes sound");}
}
class Dog extends Animal {// Dog 没有重写 makeSound 方法
}
public class Main {public static void main(String[] args) {Animal myDog = new Dog(); // 父类引用指向子类对象myDog.makeSound(); // 输出:Animal makes sound}
}
在这个例子中:
-
myDog
的实际对象类型是Dog
,但因为Dog
没有重写Animal
类中的makeSound()
方法,所以调用的仍然是父类Animal
的makeSound()
方法。 -
编译时:编译器会根据变量的声明类型
Animal
检查makeSound()
方法,并确定可以调用。 -
运行时:由于子类
Dog
没有重写makeSound()
,因此即使myDog
实际是Dog
类型,程序仍然会调用Animal
的实现。这就是所谓的“左边编译左边运行”现象。
2. 重载与重写的混淆(重难点)
在多态场景下,方法重写(Override)和方法重载(Overload)的行为有所不同:
重写:子类重写父类的方法,编译时只要父类中定义了这个方法,编译就会通过。运行时,程序会根据子类对象来调用重写后的版本。
重载:重载是指在同一个类中定义多个同名但参数不同的方法。在多态情况下,编译器只会根据变量的编译时类型来决定调用哪个重载版本。如果编译时类型没有匹配的重载方法,编译会报错。
示例:
Collection<String> list = new ArrayList<>();
list.remove(1); // 编译错误,Collection 接口中没有 remove(int index) 方法
在这个例子中,Collection
接口中只定义了 remove(Object o)
,而 ArrayList
类重载了这个方法,添加了 remove(int index)
。由于编译器只看 Collection
的方法集,无法识别 remove(int index)
,因此会报编译错误。即便实际对象是 ArrayList
,编译器依然不会允许调用 remove(int index)
,因为它“看不到”这个方法。
问题:编译器是怎么看一个方法是重写还是重载的呢?
那么问题来了!我们注意到在ArrayList的源代码中有两个remove方法,编译器是怎么看一个方法是重写还是重载的呢?
在
ArrayList
中,我们确实看到了两个remove
方法:
remove(Object o)
:根据对象删除元素。
remove(int index)
:根据索引删除元素。这两个方法名字相同,但参数类型不同,所以它们是方法重载(overloading)的例子。但是其中的
remove(Object o)
是对Collection
接口中定义的remove(Object o)
方法的重写- 而
remove(int index)
是ArrayList
特有的重载方法。为了验证上述说法,我们还可以看一下父类接口中的remove方法源代码:
即便没有 @Override
注解,我们依然可以通过以下方式区分哪个是重写,哪个是重载。
区分方式:查看方法的签名
重写(Override)
-
重写发生在子类中,方法的签名必须与父类或接口中的方法完全一致。
-
方法的签名包括:方法名、参数类型、参数顺序、返回类型。
在 ArrayList
中的 remove(Object o)
方法,签名与 Collection
接口中的 remove(Object o)
完全一致:
// Collection 接口中的定义
boolean remove(Object o);
-
方法名:
remove
-
参数类型:
Object
-
返回类型:
boolean
ArrayList
中的remove(Object o)
的实现与这个签名完全一致,因此它是对Collection
接口的重写,而剩下同名的方法则是remove方法的重载。
如何避免“左边编译左边运行”的限制?
为了避免编译时的限制,你可以通过类型转换来解决问题。通过强制类型转换,编译器会认识到你正在处理子类类型,从而允许调用子类特有的方法。
示例:
Animal myDog = new Dog();
((Dog) myDog).fetch(); // 合法,强制转换为 Dog 类型后可以调用 fetch
这种转换告诉编译器:你确实要调用子类的方法,从而避免编译时的类型限制。
拓展:多态创建的对象到底是谁属于父类还是子类?
在 Java 中,当我们谈论多态时,我们常常会使用父类的引用指向子类的对象。这个时候,多态生成的对象实际上是子类的实例(运行看子类),但它通过父类的引用(编译看父类)进行访问和操作。
让我们通过分解几个关键点,来明确这个问题:
1. 对象本质属于子类
在多态情况下,尽管我们使用父类的引用去指向对象,但这个对象的实际类型是子类的实例。换句话说,无论引用的类型是什么,对象始终是子类的实例,并且它具有子类的所有特性和行为。
示例:
Animal myDog = new Dog(); // 父类引用指向子类对象
在这段代码中:
myDog
是一个Animal
类型的引用,但它指向了Dog
类的实例。实际对象 是
Dog
,即使引用类型是Animal
,这个对象本质上属于子类Dog
。2. 引用属于父类
尽管对象是子类的实例,但在多态情况下,我们使用的是父类的引用类型。在编译时,Java 编译器只知道这个引用是父类类型,因此它只允许我们调用父类中定义的方法和属性。
示例:
myDog.makeSound(); // 调用的是 Dog 的 makeSound 方法
在编译时,编译器检查到
Animal
类中有makeSound()
方法,因此允许调用。运行时,由于
myDog
实际上是Dog
的实例,执行的是Dog
类的makeSound()
方法(如果Dog
重写了makeSound()
)。但是属性是调用父类的,运行也是使用父类的属性值
3. 编译时看引用类型,运行时看对象类型
这就是 Java 中多态的关键:编译时根据引用类型检查方法的可用性,运行时根据实际对象类型执行相应的行为。
编译时:
编译器只根据父类的引用类型(左边)来检查哪些方法可以调用,即使对象是子类实例,编译器也不会允许调用子类特有的方法(除非子类重写了父类的方法)。
运行时:
程序在运行时会根据实际的子类对象来决定执行哪个版本的方法。如果子类重写了父类的方法,调用的将是子类的实现。
4. 多态性不改变对象的实际类型
多态本质上是通过父类引用来操作子类对象的机制,但它不会改变对象的实际类型。对象仍然是子类的实例,拥有子类的所有特性和行为。例如,即使你使用父类的引用,你仍然可以通过类型转换访问子类特有的方法。
示例:
Animal myDog = new Dog(); // myDog.fetch(); // 编译错误,Animal 没有 fetch 方法 // 需要类型转换来调用子类的特有方法 ((Dog) myDog).fetch(); // 合法,调用 Dog 类的 fetch 方法
myDog
实际上是Dog
类型的对象,只不过它被一个Animal
类型的引用所引用。如果你希望调用子类的特有方法,需要进行强制类型转换,这表明对象的本质仍然是子类对象。
5. 总结
多态生成的对象本质上属于子类。即使通过父类的引用来访问,该对象始终是子类的实例,并且拥有子类的所有属性和方法(包括重写的方法)。
引用属于父类。在编译时,编译器只能看到父类中的方法和属性,并且只能允许调用这些方法。
运行时根据子类对象执行行为。即使引用是父类类型,程序在运行时会调用子类中重写的方法。
因此,在多态中,对象始终属于子类,但我们通过父类引用来控制和操作它。
说白了就是编译器只能调用父类中的方法和属性,除非子类中对父类的方法进行了重写或者进行强制类型转换
总结
Java 多态中的“编译看左边,运行看右边”原则强调了编译时和运行时的行为差异。在大多数情况下,方法的调用是根据变量的编译时类型检查的,而实际的执行依赖于运行时对象的类型。然而,当父类或接口中没有子类特有的方法,或者遇到重载方法时,编译器只看左边的类型,导致“左边编译左边运行”现象。
理解这一现象有助于我们更好地运用 Java 的多态机制,并知道何时以及如何使用类型转换来解决编译时的限制问题!