1

和现实世界中:子女可以继承父母的一些特征(如:基因)、财产等一样。OOP 中也有提供类似的特性,一个类完全可以从其它类里获得一些属性和方法,而不需要我们自己重新定义。这种特性简单但强大 (Simple and powerful)。

快速了解继承

在 Java 的继承关系里:子类可以从获取父类的所有的公共和受保护成员(字段、方法和内部类)。当然,构造方法不是成员 (members) ,所以不能被继承。同时,在 Java 的继承里,子类可以做如下事情:

  • 直接使用继承来的字段
  • 直接使用继承来的方法
  • 声明和父类同名字段,隐藏 掉父类字段
  • 通过父类提供的公有/受保护的方法访问父类的私有成
  • 创建新的字段
  • 重写父类同方法签名的实例方法,隐藏 掉父类方法
  • 重写父类同方法签名的静态方法,隐藏 掉父类方法
  • 编写父类中不存在的方法
  • 使用 super 关键字,利用父类构造方法

覆盖

当子类拥有和父类(或接口)同样方法签名的方法,这种现象叫做覆盖。如以下代码:

class Father
{
    public void doSomthing(){
        // Father do something
    }
}

class Son extends Father{
    @Override
    public void doSomething(){
        // Son do something
    }
}
  • 覆盖父类实例方法
  • 覆盖父类静态方法
  • 覆盖接口默认方法 (JDK 8+)

对于实例方法的覆盖,实际是子类拥有自己的方法。
对于静态方法的覆盖,要记住:静态方法时属于类的,在多态中,调用的始终是类的方法。接口里的静态方法永远不会被覆盖。

    Father father = new Son();
    Father.staticMethod(); // 这里使用的是父类Father的静态方法

对于接口方法的覆盖,遵循以下原则:
1.实例方法的优先级大于接口方法 (JDK8+)

interface Animal{
    default void saySomething(){
        // Animal say something
    }
}
interface Cow extends Animal{
    default void saySomething(){
        // Cow say something
    }
}

class MyCow implements Cow{
    @Override
    public void saySomething(){
        // MyCow say something
    }
    
    Animal myCow = new MyCow();
    myCow.saySomething(); // MyCow say something
}

2.有共同祖先的接口,先被覆盖的方法(继承深度低)会被后覆盖的方法(继承级别高)覆盖

interface Animal{
    default void saySomething(){
        // Animal say something
    }
}
interface Pig extends Animal{
    default void saySomething(){
        // Pig say something
    }
}
interface BigPig extends Pig{
    default void saySomething(){
        // BigPig say something
    }
}
class MyPig implements Pig, BigPig{
    public static void main(String...args){
        MyPig myPig = new MyPig();
        myPig.saySomething(); // BigPig saySomething()
    }
}

P.S. 如果出现同一继承级别(上例中 BigPig 和 Pig 都继承 Animal 接口)或者子类继承的接口无相关关系,但是接口间有同方法签名的方法,就会出现覆盖冲突。需要用 super 关键字指明具体实现哪个接口的方法或者直接覆盖。

interface Run{
    default void run(){
        // Run run
    }
}

interface Car{
    default void run(){
        // Car run
    }
}

class MyCar implements Run, Car{
    @Override
    public void run(){
        Car.super.run();
    
    }
}

同时,我们需要注意,Java 子类覆盖父类方法,应该:

  • 方法的访问权限大于等于父类
  • 方法的返回值小于等于父类
  • 方法抛出的异常小余等于父类

如果子类定义和父类同方法签名的方法,会有如下结果:

x 父类的实例方法 父类的静态方法
子类的实例方法 覆盖父类方法 编译错误
子类的静态方法 编译错误 隐藏父类方法

多态

我们已经知道,子类可以覆盖父类的方法。如果有一个类继承自另外一个类,我们完全可以用一个父类来引用一个子类,如:

class Person{
    public void saySomething(){
        // Person say something
    }
}
class Father{
    @Override
    public void saySomething(){
        // Father say something
    }
}

class Test{
    pubilc static void main(String...args){
        Father father = new Father();
        Person person = father; // 父类引用子类对象
    }
}

像这种父类引用子类对象的现象,Java 里叫做多态 (Polymorphism)。在 Java 里,只有满足如下三个条件,才能叫多态:

  • 继承关系
  • 覆盖方法
  • 父类引用子类对象

对于多态方法的运行,编译器会列举所有父类和子类符合调用方法签名(方法名+参数列表)的方法,然后以以下原则编译、调用方法:

  • 成员变量(编译和运行都看左边)
  • 成员方法(编译看左边,运行看右边)
  • 静态方法(编译和运行都看左边)

其中,调用 private, final, static 方法的过程叫静态绑定,否则叫动态绑定
在使用多态的过程中,最有效的判断语句能否通过编译的方法是:

右边的类是否是(IS-A)左边的类

如示例代码里的 father(Father) IS-A Person。

阻止继承

有些情况下,我们可能不希望子类覆盖父类的方法,这时候,用 final 关键字修饰方法即可实现该目的。编译器可以对用final的方法进行内联操作优化处理。

强制转换

在多态里,我们知道一个父类可以引用一个子类对象:

Father father = new Son();

但是,反过来就不行了:

Son son = new Father();

如果需要让编译器不报错,我们就得进行强制类型转换操作:

Son son = (Son)new Father();

不过,这么做有风险,我们一般要使用 instanceof关键字先判断,能否将父类“安全”转换成子类:

Father f = ...;
if (f instanceof Son){
    Son son = (Son)f;
}

抽象类与接口

在继承体系里,比较常用的就是抽象类和接口。它们比较相似:

  • 都能被继承
  • 都包含抽象方法
  • 都不能被实例化

然而,它们还是有不同的,例如:

  • 抽象类可以声明非 final&&static 的字段,接口不行(默认 public static final )
  • 抽象类可以有构造方法,接口不行
  • 抽象类可以声明非 public 方法,接口不行(默认方法是 public )
  • 一个类只能继承一个抽象类,可以继承多个接口

然后,抽象类和接口有不同的使用场景:P
抽象类:

  • 在相关类里共享代码
  • 规定了一系列通用的方法和属性
  • 需要定义非静态、非共有的方法

接口:

  • 定义属性,如可比较 (Comparable)、可飞(Flyable)
  • 定义行为,不关心具体实现
  • 希望使用多继承

继承的技巧

在 Java 里,我们一般按照如下规则使用继承:

  • 将公共操作放在超类(父类)中
  • 不要使用受保护的域
  • 继承严格遵循 is-a 原则
  • 不要过多得使用反射

野原英雄
206 声望25 粉丝

一只小猿