继承和多态是现代编程语言最为重要的概念。继承和多态允许用户将一些概念进行抽象,以达到代码复用的目的。本文用一些例子快速回顾一下Java/Scala的继承和多态。
继承的数据建模
继承在现实世界中无处不在。比如我们想描述动物以及他们的行为,可以先创建一个动物类别,动物类别又可以分为狗和鱼,这样的一种层次结构其实就是编程语言中的继承关系。动物类涵盖了每种动物都有的属性,比如名字、描述信息等。从动物类衍生出的众多子类,比如鱼类、狗类等都具备动物的基本属性。不同类型的动物又有自己的特点,比如鱼会游泳,狗会吼叫。继承关系保证所有动物都具有动物的基本属性,这样就不必在创建一个新的子类的时候,将他们的基本属性(名字、描述信息)再复制一遍,写到新的子类中。同时,新的子类更加关注自己区别于其他类的特点,比如鱼所特有的游泳动作。
上图对动物进行了简单的建模。图中,每个动物都有一些基本属性:名字(name)和描述(description),有一些基本方法:getName()
和eat()
,这些基本功能共同组成了Animal
类。在这个类基础上,我们可以衍生出各种各样的子类、子类的子类等。比如,Dog
类有自己的dogData
属性和bark()
方法,同时也可以使用父类的name
等属性和eat()
方法。
class和interface
我们将上面的图转化为代码,一个动物的公共父类可以抽象为:
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 + "正在吃");
}
}
子类可以拥有父类非private
属性和方法,同时可以扩展属于自己的属性和方法。比如狗类或鱼类可以继承动物类,可以直接复用动物类里定义好的属性和方法。这样就不存在代码的重复问题,整个工程的可维护性更高。在Java和Scala中,子类继承父类时都要使用extends
关键字。
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 + "正在叫");
}
}
不过,Java只允许子类继承一个父类,或者说Java不支持多继承。class A extends B, C
这样的语法在Java中是不允许的。另外,有一些方法具有更普遍的意义,比如move()
方法,不仅动物会移动,一些机器也会移动,我们让Animal
类和Machine
类都继承一个Mover
类在逻辑上没有太大意义。对于这种场景,Java提供了接口类interface
,可以将一些方法进一步抽象出来,对外提供一种功能。不同的子类可以继承interface
接口,实现自己的业务逻辑,也解决了Java不允许多继承的问题。
比如,我们定义一个名为Move
的interface
。Dog
类继承并重写了move()
方法。
public interface Move {
public void move();
}
注意,在Java中,一个类可以实现多个interface
,并使用implements
关键字:
class ClassA implements Move, InterfaceA, InterfaceB {
...
}
在Scala中,一个类实现第一个interface
时使用extends
,后面则使用with
:
class ClassA extends Move with InterfaceA, InterfaceB {
...
}
interface
与class
的主要区别在于,从功能上来说interface
强调特定功能,class
强调所属关系;从技术实现上来说,interface
里提供的都是抽象方法,class
中只有用abstract
方法修饰的方法才是抽象方法。抽象方法是指只是定义了方法签名,没有定义具体的实现的方法。实现一个子类时,遇到抽象方法必须去做自己的实现。继承并实现interface
时,要实现里面所有的方法,否则会报错。
在很多框架的API调用过程中,绝大多数情况下都是继承一个父类或接口类。对于Java用户来说,如果是继承一个interface
,要使用implements
关键字,如果是继承一个class
,要使用extends
关键字。对于Scala用户来说,绝大多数情况使用extends
就足够了。
重写与@Override注解
可以看到,子类可以用自己的方式实现父类和接口类的方法,比如前面提到的move
方法。子类的实现会覆盖父类中已有的方法,实际执行时,会使用子类实现好的方法,而不是使用父类的方法,这个过程被称为重写(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
的方法有多种不同的具体实现,每种实现在参数和返回类型上都有区别。很多框架的源码和API上应用了大量的重载,目的是提供给开发者不同的调用接口。
小结
本文简单总结了Java/Scala的继承的基本原理和使用方法,包括数据建模、关键字的使用,方法的重载。简单概括下来,对于Java的一个子类,可以用extends
继承一个class
,用implements
实现一个interface
,如果需要覆盖父类的方法,需要使用@Override
注解。