Java基础教程(18)--继承

一.继承的概念

  继承是面向对象中一个非常重要的概念,使用继承可以从逻辑和层次上更好地组织代码,大大提高代码的复用性。在Java中,继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法。在Java中,使用extends关键字来表示继承。
  从另一个类派生而来的类称为子类(也称为派生类),派生子类的类称为父类(也称为超类或基类)。当创建一个类时,总是在继承(Object类除外),如果没有明确指出要继承的类,这个类就会隐式地继承Object类。在Java中,Object类是所有类的祖先,所有类都是Object类的后代。

  在上一篇教程中,我们知道接口可以多继承。但是类不支持多继承,也就是说每个类只能直接继承自一个父类。
  继承的想法很简单但又很强大。当你想创建一个新类并且已经有一个包含你想要的代码的类时,你可以从现有的类来派生你的新类。在这样做时,你可以重用现有类的域和方法,而无需重新编写它们。

继承父类的成员

  子类继承了父类所有的public成员和protected成员(域、方法和嵌套类)。如果子类和父类在同一个包中,它还继承了父类的包私有成员。可以按原样使用继承自父类的成员,也可以替换、隐藏或补充它们。构造方法不是成员,因此它们不会被子类继承,但是可以在子类中调用父类的构造方法:

  • 继承的域可以直接使用,就像普通的域一样。
  • 可以在子类中声明相同名称的域来隐藏继承自父类中的域(不推荐)。
  • 可以声明父类中不存在的新域。
  • 继承的方法可以直接使用,就像普通的方法一样。
  • 可以在子类中编写签名相同的实例方法来重写继承自父类中的方法。
  • 可以在子类中编写签名相同的静态方法来隐藏父类中的静态方法。
  • 可以在子类中编写父类中不存在的方法。
  • 可以在子类的构造方法中调用父类的构造方法,无论是隐式地或者显式地通过super关键字去调用。

  子类不继承父类的private成员。但是,如果父类具有访问其private字段的public或protected方法,则子类也可以使用这些方法。
  可以在子类中声明相同名称的域来隐藏继承自父类中的域,即使它们类型不同。覆盖之后只能通过super关键字去访问。一般来说,不建议隐藏域,因为这样做会大大降低代码的可读性。
  嵌套类可以访问其外部类的所有私有成员。因此,子类继承的public或protected嵌套类可以间接访问父类的所有私有成员。

类型转换

  我们知道,可以将对一个对象的引用赋值给它对应的类类型的变量,例如:

MountainBike myBike = new MountainBike();

  MountainBike类继承自Bicycle类,而Bicycle类继承自Object类。因此,我们可以说一个MountBike一定是一个Bicycle,也可以说一个MountainBike一定是一个Object,因此,下面的两种写法都是正确的:

Bicycle bicycle = new MountainBike();
Object obj = new MountainBike();

  这种类型转换是隐式的,也就是说父类类型的变量可以引用子类对象。这种将子类类型的变量转换为父类类型的操作称为向上转型。实际上,在上一篇教程中,我们使用接口类型的变量去引用实现类的实例也是这向上转型。
  但反过来不一定成立。我们不能说一个Bicycle一定是一个MountBike,也不能说一个Object一定是一个MountBike。因此,如果我们像下面这样写:

Bicycle bicycle = new MountainBike();
MountBike myBike = bicycle;

  则会得到一个编译时错误。因为编译器不知道bicycle是否引用了一个MountBike的实例。当然,如果我们确定bicycle引用的就是MountBike类的实例,可以对它进行类型转换:

Bicycle bicycle = new MountainBike();
MountBike myBike = (MountBike) bicycle;

  bicycle变量本来是Bicycle类型的,这里我们将它进行转换后赋值给了子类类型MountBike。这种类型转换必须显示指定,将父类类型的变量转换为子类类型的操作称为向下转型。
  这种写法可以通过编译,但是有可能产生运行时异常。例如:

String bicycle = "MountBike";
MountBike myBike = (MountBike) bicycle;

  上面的写法将会在运行的时候抛出一个ClassCastException异常。因为bicycle引用的对象是String类型的,它无法转换成MountBike类型。可以使用instanceof运算符来在转换前进行检测:

if (myBike instanceof MountBike) {
    MountBike myBike = (MountBike) bicycle;
}

  instanceof运算符前面是需检测的变量,后面可以是类,也可以是接口。它用来检测前面的对象是否是后面的类的实例或接口的实现类。

二.重写或隐藏方法

1.重写父类的实例方法

  在子类中,可以重新编写签名相同(回忆一下,签名相同是指两个方法的名称以及参数列表都相同)的方法来覆盖继承自父类的方法,这样一来,子类中的方法将会覆盖父类中的方法,当通过子类的对象去调用这个方法时,将会执行子类中的方法而不是父类中的方法。考虑下面的Animal类:

public class Animal {
    public void method() {
        System.out.println("Method from Animal.");
    }
}

  现在我们要编写一个子类Tiger,并覆盖父类中的method方法:

public class Tiger extends Animal {
    public void method() {
        System.out.println("Method from Tiger.");
    }
}

  当调用new Tiger().method()时,将会输出“Method from Tiger.”。这样就完成了方法的覆盖,这种行为也称为重写。
  但是,在覆盖父类的过程中,必须要遵守以下几个原则,否则编译器将给出错误:

  1. 子类方法的权限不能低于父类方法的权限。例如,父类中的方法时public,子类覆盖这个方法后将权限修改为private,编译器将给出错误提示;
  2. 子类方法的返回值必须和父类一样或者小于父类方法的返回值类型。例如,父类方法的返回值是String,子类方法的返回值是Object,编译器将给出错误提示;
  3. 子类方法抛出的异常必须和父类一样或者小于父类方法抛出的异常(有关异常的内容会在以后的文章中进行介绍)。例如,父类抛出IndexOutOfBoundException,子类抛出Exception,编译器将会给出错误提示。

  在覆盖父类方法时,可以在方法上面加上@Override注解(在Java基础教程(16)--注解一文中有提到)。这个注解一来可以提高代码的可读性,在阅读代码时可以知道这个方法是在覆盖父类方法;二来也可以让编译器帮我们校验父类中是否存在这个方法,以避免手误带来的问题。

2.隐藏父类的静态方法

  如果在子类中定义了与父类签名相同的静态方法,那么子类中的这个方法会将父类中的方法隐藏。。子类中与父类签名相同的实例方法会将父类中的方法覆盖,覆盖之后父类中的方法在子类中将不存在。例如:

class SuperClass {
    public void instanceMethod() {
        System.out.println("Instance method in SuperClass.");
    }
}

public class SubClass extends SuperClass {
    public void instanceMethod() {
        System.out.println("Instance method in SubClass.");
    }

    public static void main(String[] args) {
        SubClass sub = new SubClass();
        sub.instanceMethod();
        SuperClass sup = sub;
        sup.instanceMethod();
    }
}

  上面的例子会输出:

Instance method in SubClass.
Instance method in SubClass.

  子类覆盖了父类的方法,所以子类中只有一个instanceMethod,无论怎么变换引用变量的类型,都会调用子类重写后的这个方法。
  而子类中与父类签名相同的静态方法会将父类中的方法隐藏,也就是说实际上在子类中这两个方法是同时存在的,如何去调用它们完全取决于引用变量的类型,例如:

class SuperClass {
    public static void staticMethod() {
        System.out.println("Static method in SuperClass.");
    }
}

public class SubClass extends SuperClass {
    public static void staticMethod() {
        System.out.println("Static method in SubClass.");
    }

    public static void main(String[] args) {
        SubClass sub = new SubClass();
        sub.staticMethod();
        SuperClass sup = sub;
        sup.staticMethod();
    }
}

  上面的例子会输出:

Static method in SubClass.
Static method in SuperClass.

  子类隐藏了父类的静态方法。如果通过子类类型的引用变量调用这个方法,则会选择子类的静态方法;如果通过父类类型的引用变量去调用这个方法,则会选择父类的静态方法。

3.接口中的方法

  接口中的默认方法和抽象方法就像实例方法那样被实现类继承。然而,当父类和接口提供相同签名的默认方法时,Java编译器将会按照以下规则来解决冲突:

  • 实例方法优先于接口的静态方法。
      考虑下面的类和接口:
public class Horse {
    public String identifyMyself() {
        return "I am a horse.";
    }
}
public interface Flyer {
    default public String identifyMyself() {
        return "I am able to fly.";
    }
}
public interface Mythical {
    default public String identifyMyself() {
        return "I am a mythical creature.";
    }
}
public class Pegasus extends Horse implements Flyer, Mythical {
    public static void main(String[] args) {
        Pegasus myApp = new Pegasus();
        System.out.println(myApp.identifyMyself());
    }
}

  上面的例子将会输出“I am a horse.”。

  • 已经被覆盖的方法将被忽略。当父类型拥有共同的祖先时,可能会出现这种情况。考虑下面的类和接口:
public interface Animal {
    default public String identifyMyself() {
        return "I am an animal.";
    }
}
public interface EggLayer extends Animal {
    default public String identifyMyself() {
        return "I am able to lay eggs.";
    }
}
public interface FireBreather extends Animal { }
public class Dragon implements EggLayer, FireBreather {
    public static void main (String... args) {
        Dragon myApp = new Dragon();
        System.out.println(myApp.identifyMyself());
    }
}

  上面的例子将输出“I am able to lay eggs.”。
  不只是两个接口,一个类和一个接口也是如此:

public interface Animal {
    default public String identifyMyself() {
        return "I am an animal.";
    }
}
public interface EggLayer extends Animal {
    default public String identifyMyself() {
        return "I am able to lay eggs.";
    }
}
class FireBreather implements Animal { }
public class Dragon extends FireBreather implements EggLayer {
    public static void main (String... args) {
        Dragon myApp = new Dragon();
        System.out.println(myApp.identifyMyself());
    }
}

  上面的例子仍然会输出“I am able to lay eggs.”。

  如果两个或多个独立定义的默认方法冲突,或者默认方法与抽象方法冲突,则Java编译器会产生编译错误。此时必须显式覆盖超类型方法。假设存在由计算机控制并且可以飞的汽车,现在有两个接口提供了startEngine方法的默认实现:

public interface OperateCar {
    // ...
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}
public interface FlyCar {
    // ...
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}

  同时实现了OperateCar和FlyCar接口的类必须覆盖方法startEngine。不过可以通过super关键字来调用默认实现:

public class FlyingCar implements OperateCar, FlyCar {
    // ...
    public int startEngine(EncryptedKey key) {
        FlyCar.super.startEngine(key);
        OperateCar.super.startEngine(key);
    }
}

  super前面的类型必须定义或者继承了被调用的默认方法。这种形式的调用用来区分多实现时签名相同的默认方法。在类中或在接口中都可以使用super关键字来调用默认方法。
  继承的实例方法可以覆盖抽象方法。考虑下面的例子:

public interface Mammal {
    String identifyMyself();
}
public class Horse {
    public String identifyMyself() {
        return "I am a horse.";
    }
}
public class Mustang extends Horse implements Mammal {
    public static void main(String... args) {
        Mustang myApp = new Mustang();
        System.out.println(myApp.identifyMyself());
    }
}

  上面的例子将会输出“I am a horse.”。Mustang类继承了Horse的identifyMyself方法,这个方法刚好覆盖了Mammal类中的抽象方法identifyMyself。
  接口中的静态方法永远不会被继承。
  下面的表格总结了当定义与父类中签名相同的方法时可能出现的情况:

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

  在子类中,可以对从父类中继承的方法进行重载。这样的方法既不隐藏也不覆盖父类的方法,它们是新方法,对于子类来说是唯一的。

三.多态

  在生物学中,多态的定义是说一个器官或物种会有不同的形态或阶段。这个定义现在也被应用在像Java这样的面向对象语言中。子类可以定义属于它们自己的独特的行为,但仍然保留父类中一些原始的功能。
  例如,Bicycle类中有一个方法printDescription:

public void printDescription(){
    System.out.println("Bike is in gear " + this.gear + " with a cadence of " + 
        this.cadence + " and travelling at a speed of " + this.speed + ". ");
}

  为了演示多态性,我们定义了两个Bicycle类的子类MountainBike和RoadBike。
  首先创建MountainBike类,这里我们给MountainBike类增加了一个suspension域,用来表示自行车的减震器类型:

public class MountainBike extends Bicycle {
    private String suspension;

    public MountainBike(
               int startCadence,
               int startSpeed,
               int startGear,
               String suspensionType){
        super(startCadence,
              startSpeed,
              startGear);
        this.setSuspension(suspensionType);
    }

    public String getSuspension(){
      return this.suspension;
    }

    public void setSuspension(String suspensionType) {
        this.suspension = suspensionType;
    }

    public void printDescription() {
        super.printDescription();
        System.out.println("The " + "MountainBike has a" +
            getSuspension() + " suspension.");
    }
} 

  注意被覆盖的方法printDescription。不但输出了继承自Bicycle类中的属性,还将suspension属性也添加到了输出中。
  接下来创建RoadBike类。因为公路自行车有很多非常细的轮胎,这里增加了一个tireWidth属性来表示轮胎的宽度:

public class RoadBike extends Bicycle{
    // In millimeters (mm)
    private int tireWidth;

    public RoadBike(int startCadence,
                    int startSpeed,
                    int startGear,
                    int newTireWidth){
        super(startCadence,
              startSpeed,
              startGear);
        this.setTireWidth(newTireWidth);
    }

    public int getTireWidth(){
      return this.tireWidth;
    }

    public void setTireWidth(int newTireWidth){
        this.tireWidth = newTireWidth;
    }

    public void printDescription(){
        super.printDescription();
        System.out.println("The RoadBike" + " has " + getTireWidth() +
            " MM tires.");
    }
}

  这里的printDescription方法和上面一样,不但输出了之前的属性,还输出了新增加的tireWidth属性。
  下面的测试程序创建了三个Bicycle变量,这三个变量分别引用了Bicycle、MountainBike和RoadBike类的实例:

public class TestBikes {
  public static void main(String[] args){
    Bicycle bike01, bike02, bike03;

    bike01 = new Bicycle(20, 10, 1);
    bike02 = new MountainBike(20, 10, 5, "Dual");
    bike03 = new RoadBike(40, 20, 8, 23);

    bike01.printDescription();
    bike02.printDescription();
    bike03.printDescription();
  }
}

  上面的程序输出如下:

Bike is in gear 1 with a cadence of 20 and travelling at a speed of 10. 

Bike is in gear 5 with a cadence of 20 and travelling at a speed of 10. 
The MountainBike has a Dual suspension.

Bike is in gear 8 with a cadence of 40 and travelling at a speed of 20. 
The RoadBike has 23 MM tires.

  JVM为每个对象调用合适的方法,而不是完全调用变量类型中定义的方法。此行为称为虚方法调用,它是Java语言中表现多态特征的一种方式。

四.super关键字

1.访问父类成员

  如果子类覆盖了父类的方法,可以通过super关键字去调用父类的方法。还可以使用super关键字去访问被隐藏的域(尽管不鼓励隐藏域)。
  考虑下面的SuperClass类:

public class Superclass {
    public void printMethod() {
        System.out.println("Printed in Superclass.");
    }
}

  下面是它的子类SubClass,它覆盖了printMethod方法:

public class Subclass extends Superclass {
    // overrides printMethod in Superclass
    public void printMethod() {
        super.printMethod();
        System.out.println("Printed in Subclass.");
    }
    public static void main(String[] args) {
        Subclass s = new Subclass();
        s.printMethod();    
    }
}

  在上面的例子中,虽然printMethod方法被覆盖,但仍然可以通过super关键字去调用它。这个程序的输出如下:

Printed in Superclass.
Printed in Subclass.

2.父类构造器

  可以通过super关键字来调用父类的构造方法。在上面的MountBike类的构造方法中,它先是调用了父类的构造方法,然后又添加了自己的初始化代码:

public MountainBike(int startHeight, int startCadence, int startSpeed, int startGear) {
    super(startCadence, startSpeed, startGear);
    seatHeight = startHeight;
}   

  调用父类构造器的语法如下:

super();

super(parameter list);

  super()将会调用父类的无参构造方法,super(parameter list)将会调用匹配参数列表的父类构造方法。调用父类构造方法的语句必须放在子类构造方法的第一行。
  如果子类的构造方法没有显式地调用父类的构造方法,编译器在编译时将会自动在子类构造方法的第一行插入对父类无参构造方法的调用。如果父类没有无参构造方法,则会产生编译错误。Object类有无参构造方法,如果Object类是唯一的父类,则不会出现这个问题。
  由于子类的构造方法会显式或隐式地调用父类的构造方法,而父类构造方法又会显式或隐式地调用它的父类的构造方法,这样最终一定会回到Object类的构造方法,我们将这种行为称为构造方法链。

五.final关键字

  可以将在方法前使用final关键字,这样这个方法将不能被子类重写。Object类中就有不少这样的方法,例如getClass方法:

public final Class<?> getClass()

  这意味着无法在任何一个类中重写getClass()方法。
  在构造方法中调用的方法通常应该生命为final。如果构造函数调用非final的方法,那么子类重写这个方法后可能会造成预想不到的结果。
  还可以将类声明为final。声明为final的类不能被继承。例如String类就是一个final类,没有任何一个类可以继承它。

六.抽象类和抽象方法

  抽象类是在类定义前使用了abstract关键字的类,它既可以包含也可以不包含抽象方法。抽象类无法实例化,但是可以被继承。
  抽象方法是指没有具体实现的方法,例如:

abstract void moveTo(double deltaX, double deltaY);

  抽象类中的抽象方法前面必须要使用abstract关键字,而接口中的抽象方法则不需要使用abstract关键字(可以使用,但没必要)。抽象方法前不可以使用private修饰符,因为抽象类的抽象方法是一定要被子类继承或重写的,如果使用private修饰符,那么子类将无法继承父类的抽象方法。正是因为抽象类的抽象方法一定要被子类继承或重写,因此protected修饰符也是没有意义的。此外,如果不适用权限修饰符,则抽象方法的权限是包私有的,这种写法在当前抽象类中虽然不会产生错误,但当子类与这个抽象类不在同一个包中时,子类无法继承父类的抽象方法,也会产生错误。综上所述,抽象类的抽象方法前必须使用public权限修饰符。
  如果某个类中包含抽象方法,那么这个类必须声明为abstract。子类会继承父类的抽象方法。只要子类中有没覆盖的抽象方法,那么子类也必须声明为abstract。
  一般来说,一个类在实现接口时,必须实现该接口的全部抽象方法。但是如果没有实现所有的抽象方法,则需要将这个类声明为抽象类。

抽象类与接口的比较

  下面从语法方面对抽象类与接口进行比较:

  1. 抽象方法:抽象类与接口中都既可以包含抽象方法,也可以不包含抽象方法。但不同的是,抽象类中的抽象方法前面必须要使用abstract关键字,而接口中的抽象方法则不需要;抽象类的抽象方法的权限修饰符必须使用public修饰符,而接口中的抽象方法则不需要,因为接口中的抽象方法默认是且必须是public的。
  2. 默认方法:从Java8开始,接口中可以为方法提供默认实现,这样的方法称为默认方法,默认方法前要使用default关键字。而抽象类中也可以为方法提供实现,不过我们并不将这样的方法称为默认方法,为了与抽象方法区分,我们将抽象类中除了抽象方法之外的方法称为具体方法。接口中的默认方法默认是且必须是public,而抽象类中的具体方法则可以使用任何权限修饰符。
  3. 静态方法:就像普通的类一样,抽象类中也可以有静态方法。从Java8开始,接口中也可以定义静态方法。抽象类中的静态方法可以使用任何权限修饰符,而接口中的静态方法则默认是且只能是public。
  4. :抽象类和接口中都可以定义域。不同的是,接口中的域默认都是public、static和final的,而抽象类则没有此限制,可以在抽象类中声明非public域、非static域和非final域。
  5. 继承:和普通类一样,抽象类只支持单继承,而接口则支持多继承。

  接下来,从设计层面上对抽象类与接口进行比较:

声明:以下内容节选自大神海子的《深入理解Java的接口和抽象类》一文,原文链接深入理解Java的接口和抽象类,如有侵权请联系本人删除。转载本文时请保留此段声明,否则保留追究其法律责任的权利。

  1. 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
  2. 设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。
    下面看一个网上流传最广泛的例子:门和警报的例子:门都有open( )和close( )两个动作,此时我们可以定义通过抽象类和接口来定义这个抽象概念:
abstract class Door {
    public abstract void open();
    public abstract void close();
}

或者:

interface Door {
    public abstract void open();
    public abstract void close();
}

  但是现在如果我们需要门具有报警alarm( )的功能,那么该如何实现?下面提供两种思路:
  1) 将这三个功能都放在抽象类里面,但是这样一来所有继承于这个抽象类的子类都具备了报警功能,但是有的门并不一定具备报警功能;
  2) 将这三个功能都放在接口里面,需要用到报警功能的类就需要实现这个接口中的open( )和close( ),也许这个类根本就不具备open( )和close( )这两个功能,比如火灾报警器。
  从这里可以看出, Door的open() 、close()和alarm()根本就属于两个不同范畴内的行为,open()和close()属于门本身固有的行为特性,而alarm()属于延伸的附加行为。因此最好的解决办法是单独将报警设计为一个接口,包含alarm()行为,Door设计为单独的一个抽象类,包含open和close两种行为。再设计一个报警门继承Door类和实现Alarm接口。

interface Alram {
    void alarm();
}
abstract class Door {
    void open();
    void close();
}
class AlarmDoor extends Door implements Alarm {
    void oepn() {
      //....
    }
    void close() {
      //....
    }
    void alarm() {
      //....
    }
}

  在了解了接口和抽象类的区别和联系后,我们在编写代码时就可以根据自己的需求来灵活选择使用接口还是抽象类。

posted @ 2019-01-11 11:08  maconn  阅读(662)  评论(0编辑  收藏  举报