聊聊Java/Scala的继承和多态

547 阅读6分钟

继承和多态是现代编程语言最为重要的概念。继承和多态允许用户将一些概念进行抽象,以达到代码复用的目的。本文用一些例子快速回顾一下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不允许多继承的问题。

比如,我们定义一个名为MoveinterfaceDog类继承并重写了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 {
  ...
}

interfaceclass的主要区别在于,从功能上来说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注解。