2.1. 继承和多态#
继承和多态是现代编程语言中最为重要的概念。继承和多态允许用户将一些代码进行抽象,以达到复用的目的。Flink 开发过程中会涉及大量的继承和多态相关问题。
继承、类和接口#
继承在现实世界中无处不在。比如我们想描述动物和它们的行为,可以先创建一个动物类别,动物类别又可以分为狗和鱼,这样的一种层次结构其实就是编程语言中的继承关系。动物类涵盖了每种动物都有的属性,比如名字、描述信息等。从动物类衍生出的众多子类,比如鱼类、狗类等都具备动物的基本属性。不同类型的动物又有自己的特点,比如鱼会游泳、狗会吼叫。继承关系保证所有动物都具有动物的基本属性,这样就不必在创建一个新的子类的时候,将它们的基本属性(名字、描述信息)再复制一遍。同时,子类更加关注自己区别于其他类的特点,比如鱼所特有的游泳动作。
图 2.1 所示为对动物进行的简单的建模。其中,每个动物都有一些基本属性,即名字(name)和描述(description);有一些基本方法,即 getName()和 eat(),这些基本功能共同组成了 Animal 类。在 Animal 类的基础上,可以衍生出各种各样的子类、子类的子类等。比如,Dog 类有自己的 dogData 属性和 bark()方法,同时也可以使用父类的 name 等属性和 eat() 方法。
我们将 图 2.1 所示的 Animal 类继承关系转化为代码,一个 Animal 公共父类可以抽象如代码清单 2-1 所示。
public class Animal {
private String name;
private String description;
public Animal(String myName, String myDescription) {
this.name = myName;
this.description = myDescription;
}
public String getName() {
return this.name;
}
public void eat(){
System.out.println(name + "正在吃");
}
}
代码清单 2-1 一个简单的 Animal 类
子类可以拥有父类非 private 的属性和方法,同时可以扩展属于自己的属性和方法。比如 Dog 类或 Fish 类可以继承 Animal 类,可以直接复用 Animal 类里定义的属性和方法。这样就不存在代码的重复问题,整个工程的可维护性更好。在 Java 和 Scala 中,子类继承父类时都要使用 extends 关键字。代码清单 2-2 实现了一个 Dog 类,并在里面添加了 Dog 类的一些特有成员。
public class Dog extends Animal implements Move {
private String dogData;
public Dog(String myName, String myDescription, String myDogData) {
this.name = myName;
this.description = myDescription;
this.dogData = myDogData;
}
@Override
public void move(){
System.out.println(name + "正在奔跑");
}
public void bark(){
System.out.println(name + "正在叫");
}
}
代码清单 2-2 Dog 类继承 Animal 类,并实现了一些特有的成员
不过,Java 只允许子类继承一个父类,或者说 Java 不支持多继承。class A extends B, C
这样的语法在 Java 中是不允许的。另外,有一些方法具有更普遍的意义,比如 move() 方法,不仅动物会移动,一些机器(比如 Machine 类和 Car 类)也会移动。因此让 Animal 类和 Machine 类都继承一个 Mover 类在逻辑上没有太大意义。对于这种场景,Java 提供了接口,以关键字 interface
标注,可以将一些方法进一步抽象出来,对外提供一种功能。不同的子类可以继承相同的接口,实现自己的业务逻辑,也解决了 Java 不允许多继承的问题。代码清单 2-2 的 Dog 类也实现了这样一个名为 Move 的接口。
Move 接口的定义如下。
public interface Move {
public void move();
}
Note
在 Java 中,一个类可以实现多个接口,并使用 implements
关键字。
class ClassA implements Move, InterfaceA, InterfaceB {
...
}
在 Scala 中,一个类实现第一个接口时使用关键字 extends
,后面则使用关键字 with
。
class ClassA extends Move with InterfaceA, InterfaceB {
...
}
接口与类的主要区别在于,从功能上来说,接口强调特定功能,类强调所属关系;从技术实现上来说,接口里提供的都是抽象方法,类中只有用 abstract
关键字定义的方法才是抽象方法。抽象方法是指只定义了方法签名,没有定义具体实现的方法。实现一个子类时,遇到抽象方法必须去做自己的实现。继承并实现接口时,要实现里面所有的方法,否则会报错。
在 Flink API 调用过程中,绝大多数情况下都继承一个父类或接口。对于 Java 用户来说,如果继承一个接口,就要使用 implements
关键字;如果继承一个类,要使用 extends
关键字。对于 Scala 用户来说,绝大多数情况使用 extends
关键字就足够了。
重写与重载#
重写#
子类可以用自己的方式实现父类和接口的方法,比如前文提到的 move() 方法。子类的方法会覆盖父类中已有的方法,实际执行时,Java 会调用子类方法,而不是使用父类方法,这个过程被称为重写(Override)。在实现重写时,需要使用 @Override
注解(Annotation)。重写可以概括为,外壳不变,核心重写;或者说方法签名等都不能与父类有变化,只修改花括号内的逻辑。
虽然 Java 没有强制开发者使用这个注解,但是 @Override
会检查该方法是否正确重写了父类方法,如果发现其父类或接口中并没有该方法时,会报编译错误。像 IntelliJ IDEA 之类的集成开发环境也会有相应的提示,帮助我们检查方法是否正确重写。这里强烈建议开发者在继承并实现方法时养成使用 @Override
的习惯。
public class ClassA implements Move {
@Override
public void move(){
...
}
}
在 Scala 中,在方法前添加一个 override
关键字可以起到重写提示的作用。
class ClassA extends Move {
override def move(): Unit = {
...
}
}
重载#
一个很容易和重写混淆的概念是重载(Overload)。重载是指,在一个类里有多个同名方法,这些方法名字相同、参数不同、返回类型不同。
public class Overloading {
// 无参数,返回值为 int 类型
public int test(){
System.out.println("test");
return 1;
}
// 有一个参数
public void test(int a){
System.out.println("test " + a);
}
// 有两个参数和一个返回值
public String test(int a, String s){
System.out.println("test " + a + " " + s);
return a + " " + s;
}
}
这段代码演示了名为 test() 的方法的多种不同的具体实现,每种实现在参数和返回值类型上都有区别。包括 Flink 在内,很多框架的源码和 API 应用了大量的重载,目的是给开发者提供多种不同的调用接口。
继承和多态小结#
本节简单总结了 Java/Scala 的继承和多态基本原理和使用方法,包括数据建模、关键字的使用、方法的重写等。从 Flink 开发的角度来说,需要注意以下两点。
对于 Java 的一个子类,可以用
extends
关键字继承一个类,用implements
关键字实现一个接口。如果需要覆盖父类的方法,则需要使用@Override
注解。对于 Scala 的一个子类,可以用
extends
关键字继承一个类或接口。如果需要覆盖父类的方法,则需要在方法前添加一个override
关键字。