多态和重写

模块 2:Java 核心
第 1 级 , 课程 6
可用

“阿米戈,你喜欢鲸鱼吗?”

“鲸鱼?没感觉,没听说过呀。”

“它和牛长不多,不过比牛体块大些,而且会游泳。顺便一提,鲸鱼源自牛。嗯,或者至少他们拥有共同的祖先。那不重要。”

多态和重写 - 1

“请注意,我想告诉你 OOP 的另一个非常有用的功能:多态。它有四个特征。”

1) 方法重写。

假如你在为某个游戏写 Cow 类。其中有很多成员变量和方法。这个类的对象可以做很多动作:walk(行走)、eat(进食)、sleep(睡觉)。Cow 行走的时候,身上的铃铛还会响。假设,你已经把这个类里的所有内容实现到每一个细节。

多态和重写 - 2

突然,客户说他想发布新的游戏版本,在那个版本里,所有动作都发生在海洋里,而主角是一头鲸鱼。

你开始着手设计 Whale 类,并意识到它与 Cow 类只有细微的差别。这两个类的逻辑非常相似,于是你决定使用继承。

Cow 类很适合做父类:它已经包含所有必要的变量和方法。你要做的只是添加鲸鱼游泳的技能。但问题是:你的鲸鱼有四条腿、两个犄角和一个铃铛。毕竟,Cow 实施了这个功能。你该怎么办呢?

多态和重写 - 3

方法重写前来救场。如果我们继承的方法里有新类不需要的内容,我们可以用另一方法来替换。

多态和重写 - 4

具体怎们做呢?在子类中,我们声明想要更改的方法(与父类相同的方法签名)。然后,我们给这个方法写一个新代码。这样就可以了。就好像父类里的老方法并不存在一样。

下面是此代码的工作方式:

代码 说明
class Cow
{
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a cow");
}
}class Whale extends Cow
{
public void printName()
{
System.out.println("I'm a whale");
}
}
这里我们定义了 2 个类:Cow 和 WhaleWhale 继承 Cow

 Whale 类重写了 printName(); 方法。

public static void main(String[] args)
{
Cow cow = new Cow();
cow.printName();
}
这个代码会在屏幕上显示我是一头奶牛
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printName();
}
这个代码会在屏幕上显示我是一条鲸鱼

继承 Cow 并重写 printName 后,Whale 类实际上拥有如下数据和方法:

代码 说明
class Whale
{
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a whale");
}
}
对于老方法,我们一无所知。

“事实上,这正是我想要的。”

2) 但这并不是全部。

“假设 Cow 类有一个 printAll 方法,它会调用另外两个方法。之后,代码的工作方式就如下所示:”

屏幕会显示:
我浑身白色
我是一条鲸鱼

代码 说明
class Cow
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a cow");
}
}

class Whale extends Cow
{
public void printName()
{
System.out.println("I'm a whale");
}
}
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printAll();
}
屏幕会显示:
我浑身白色
我是一条鲸鱼

请注意,在 Whale 对象上调用 Cow 类的 printAll() 方法时,Whale 的 printName() 方法就会被使用,而不是 Cow 的。

重要的不是写入方法的类,重要的是此方法被调用时所在的对象的类型(类)。

“我明白了。”

“你可以只继承和重写非 static 方法。Static 方法并未被继承,因此,无法重写。”

如下就是我们应用继承并重写方法后 Whale 类的样子:

代码 说明
class Whale
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a whale");
}
}
这是我们应用继承和重写方法后 Whale 类的样子。对于原先的 printName 方法,我们一无所知。

3) 类型转换。

还有一个更有意思的知识点。由于子类会继承父类的所有方法和数据,所以,此子类的对象可以是父类的引用变量(以及父类的父类,父类的父类的父类等的变量,直至 Object 类)。请考虑以下示例:

代码 说明
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printColor();
}
屏幕会显示:
我浑身白色。
public static void main(String[] args)
{
Cow cow = new Whale();
cow.printColor();
}
屏幕会显示:
我浑身白色。
public static void main(String[] args)
{
Object o = new Whale();
System.out.println(o.toString());
}
屏幕会显示:
Whale@da435a.
toString() 方法继承自 Object 类。

“太棒了。但是我们为什么要用它?”

“这是一个很重要的特征。将来,你会认识到它真的非常、非常重要。”

4) 后联编(动态分派)

下面是个后联编的例子:

代码 说明
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printName();
}
屏幕会显示:
我是一条鲸鱼。
public static void main(String[] args)
{
Cow cow = new Whale();
cow.printName();
}
屏幕会显示:
我是一条鲸鱼。

请注意,决定具体我们调用哪个 printName 方法(Cow 类或 Whale 类的方法)的不是变量的类型,而是那个变量所引用的对象的类型。

Cow 变量存储了 Whale 对象的引用,Whale 类中定义的 printName 方法会被调用。

“加入这些并不是为了清楚起见。”

“是的,并没有那么清楚。切记下面这条重要原则:”

你可以对一个变量调用的方法集实际上取决于该变量的类型。但是具体哪种方法/实现会被调用,则又取决于该变量所引用的对象的类型/类。

“我会去试试。”

“你会时不时地碰到这种情况,所以,你很快就会明白其中的道理,并且永远不会忘记。”

5) 类型转换。

转换在引用类型(类)中的运作方式不同于其在原始类型里运作的方式。但是,拓宽转换和窄化转换也可以应用于引用类型。请考虑以下示例:

拓宽转换 说明
Cow cow = new Whale();

典型的拓宽转换。现在,你只可以在 Whale 对象上调用 Cow 类定义的方法。

编译器只让你使用 Cow 变量来调用这些 Cow 类型定义的那些方法。

窄化转换 说明
Cow cow = new Whale();
if (cow instanceof Whale)
{
Whale whale = (Whale) cow;
}
典型的窄化转换且带有类型检查。Cow 类型的 cow 变量存储了 Whale 对象的引用。
我们检查发现事实就是这样,然后执行(窄化)类型转换。这也被称为类型转换
Cow cow = new Cow();
Whale whale = (Whale) cow; //exception
你也可以对引用类型执行窄化转换,而不对对象进行类型检查。
这里,如果 cow 变量指向 Whale 对象以外的其他东西,它就会抛出一个异常 (InvalidClassCastException)。

6) 现在来看一些有趣的东西。调用原始方法。

有时候,重写继承的方法时,你不想全都替换它。有时,你只想在里面加一点点东西。

这时,你如果真的希望用新方法的代码来调用同样的方法,但却是针对基类的。Java 可以帮到你。具体做法如下: super.method()

下面是一些示例:

代码 说明
class Cow
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a cow");
}
}

class Whale extends Cow
{
public void printName()
{
System.out.print("This is false: ");
super.printName();

System.out.println("I'm a whale");
}
}
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printAll();
}
屏幕会显示:
我浑身白色
下面这句是错的:我是一头奶牛
我是一条鲸鱼

“嗯。这是个教训。我的金属耳朵就要融掉了。”

“是的,事情从不简单。这会是你看到的最难的材料。教授说会提供一些其他作者写的材料的链接,所以,如果还有不明白的地方,你可以通过这些材料来扫除盲点。”

评论 (13)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
黄z聰 级别 18,Tokyo,China
29 十一月 2023
似懂非懂
OldDog-Z124 级别 22,Germany,中国 Expert
5 九月 2023
1. 决定具体我们调用哪个 printName 方法(Cow 类或 Whale 类的方法)的不是变量的类型,而是那个变量所引用的对象的类型。 2. 对一个变量调用的方法集实际上取决于该变量的类型。但是具体哪种方法/实现会被调用,则又取决于该变量所引用的对象的类型/类。 3. 在子类调用父类的方法super.method(); 补充 4. 在子类构造函数调用父类构造函数的方法super();
Anonymous #11015171 级别 24,Chengdu,中国
10 七月 2022
这个类型转换不是很明白,为什么和第10级的相反的
Anonymous #11015171 级别 24,Chengdu,中国
10 七月 2022
String s = "妈妈"; Object o = s; // o 存储一个 String 典型的拓宽引用转换 Object o = "妈妈"; // o 存储一个 String String s2 = (String) o; 典型的窄化引用转换 这第10章的类型转换,相反的
z18335776829 级别 19,China,China
6 五月 2023
父类 变量名 = 子类引用 拓宽 子类 变量名 = 父类引用 窄化
马格纳斯 级别 16,China,China
28 六月 2022
你给我除去
阿狼 级别 32,Zhengzhou,China
13 六月 2022
day 10
Anonymous #10930302 级别 16,China,China
9 三月 2022
动态分派的中文课程很少有讲的,深入理解java虚拟机这本书里有提及
杜少雄 级别 20,Taiyuan,China
15 三月 2021
come on! hard
TaoLu 级别 20,泾县,China
11 三月 2021
4) 后联编(动态分派) 这个英文是什么,翻译的看不懂啊
TaoLu 级别 20,泾县,China
11 三月 2021
Post-linking (dynamic dispatching)
zdkk 级别 24
20 三月 2021
动态绑定
momoshenchi 级别 22,Wenzhou,China
6 八月 2020
看不懂..