聊聊Java/Scala的继承和多态

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

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章