继承

Java继承是指Java中一个类能够从另一个类继承的能力。在Java中,这也被称为扩展类。一个类可以扩展另一个类,从而继承这个类。

当一个类继承另一个类时,这两个类将承担某些角色。扩展的类(从另一个类继承)是子类,被扩展的类(被继承的类)是超类。换句话说,子类扩展了超类。或者,子类继承超类。

另一个常用的继承术语是专门化和泛化。子类是超类的专门化,而超类是一个或多个子类的泛化。

继承是实现代码重利用的一种方式

继承可以是在一些具有共同特征的类之间共享代码的一种有效方法,当然也允许这些类具有一些不同的部分。

下面是一个Vehicle的类,它有两个子类Car和Truck。

Car类和Truck类继承自Vehicle类

Vehicle类是Car 和 Truck的超类。Car和Truck是Vehicle的子类。Vehicle类可以包含所有车辆所需的字段和方法(例如车牌,车主等),而Car和Truck可以包含特定于汽车和卡车的字段和方法。

注意:有些人声称继承是一种根据它们是什么来分类你的类的方法。一辆小轿车(Car)是一辆车(Vehicle)。一辆卡车(Truck)是一辆车(Vehicle)。然而,在实践中,并不能通过它来确定应用程序需要具有哪些超类和子类。这通常取决于你如何在应用程序中使用它们。

例如,你是否需要将Car和Truck对象归结到Vehicle对象?你需要同时处理Car和Truck对象吗?如果是,那么为这两个类别设置一个共同的Vehicle超类是有意义的。如果你不会以同样的方式处理Car和Truck对象,那么除了共享它们之间的代码(避免编写重复的代码)之外,为它们建立一个共同的超类是没有意义的。

类层次结构

超类和子类形成一个继承结构,也称为类层次结构。在类层次结构的顶部,是超类。在类层次结构的底部,是子类。

一个类层次结构可能有多个层次,意味着多层次的超类和子类。一个子类本身可能是其他子类的超类

Java继承基础

当一个类从一个超类继承时,它继承了超类方法和字段的一部分。子类也可以覆盖(重新定义)继承来的方法。字段不能被覆盖,但可以在子类中“隐藏”。所有这些工作将在本文后面介绍。

什么会被继承?

当一个子类在Java中扩展一个超类时,超类的所有protected和public字段和方法都由子类继承。继承是指这些字段和方法也是子类的一部分,就好像子类已经声明了它们自己一样。 protected和public字段可以像在子类中直接声明的一样被调用和引用。

具有默认(包)访问修饰符的字段和方法只有在子类与超类位于同一个包中时才能被子类访问。超类的private字段和方法永远不能被子类直接引用。但是,它们可以通过子类可访问的方法间接引用(例如默认(包),protected和public方法)。

构造方法不会被子类继承,但是子类构造方法必须调用在超类中的构造方法。这将在后面的章节中详细解释。

Java只支持单继承

Java继承机制只允许Java类从单个超类继承(单继承)。在一些编程语言中,如C ++,子类可以从多个超类继承(多重继承)。由于多重继承会产生一些奇怪的问题,例如,超类包含了具有相同名称和参数的方法,Java中舍弃了多重继承。

声明继承

在Java中,继承是使用extends关键字声明的。你声明类时通过在类定义中使用extends关键字来扩展另一个类。示例如下:

public class Vehicle {
    protected String licensePlate = null;

    public void setLicensePlate(String license) {
        this.licensePlate = license;
    }
}
public class Car extends Vehicle {
    int numberOfSeats = 0;

    public String getNumberOfSeats() {
        return this.numberOfSeats;
    }
}

本例中的Car类扩展了Vehicle类,这意味着Car类继承自Vehicle类。

由于Car类扩展了Vehicle类,因此Vehicle类的protected字段licensePlate将被Car类继承。当licensePlate字段被继承时,它在Car实例内是可访问的。

在上面代码中licensePlate字段实际上并没有在Car类引用,但是如果我们愿意的话,它也是可以的。示例如下:

public class Car extends Vehicle {
    int numberOfSeats = 0;

    public String getNumberOfSeats() {
        return this.numberOfSeats;
    }

    public String getLicensePlate() {
        return this.licensePlate;
    }
}

引用发生在getLicensePlate()方法内部。

在许多情况下,将getLicensePlate()方法放置在licensePlate字段所在的Vehicle类中是有意义的。这里我在Car类中放置getLicensePlate()方法只是用来说明在Car类中可以访问licensePlate字段。

继承和类型转换

可以将子类引用为其超类之一的实例。例如,使用上一节示例中的类定义,可以将Car类的实例作为Vehicle类的实例进行引用。因为Car类延伸(继承自Vehicle类),所以也被称为Vehicle。示例如下:

Car     car     = new Car();
Vehicle vehicle = car;

首先创建一个Car实例。然后,Car实例被分配给一个Vehicle类型的变量。现在Vehicle变量(引用)指向Car实例,这是可以的,因为Car类继承自Vehicle类。

正如你所看到的,有可能使用某个子类的一个实例,就像它是它的超类的一个实例一样。这样,你不需要确切知道对象的子类是一个实例。你可以把Truck和Car实例作为Vehicle实例。

将类的对象引用为与类本身不同的类型的过程称为类型转换。你把一个对象从一个类型转换到另一个类型。

向上转换和向下转换

你总是可以把一个子类的对象转换成它的一个超类。这被称为向上转换(从子类型到超类)。

只有当对象确实是该子类的实例(或该子类的子类的实例)时,才可能将对象从超类型转换为子类型。这被称为向下转换(从超类型到子类)。因此,下面这个向下转换的例子是有效的:

Car     car     = new Car();

// 向上转换成 Vehicle
Vehicle vehicle = car;

// 向下转换成  car 
Car     car2    =  (Car) vehicle;

但是,下面的向下转换t示例是无效的。 Java编译器会接受这种写法,但是在运行时执行代码时,代码将抛出一个ClassCastException异常。

Truck   truck   = new Truck();

// 向上转换成Vehicle
Vehicle vehicle = truck;

// 向下转换成Car
Car     car     =  (Car) vehicle;

Truck对象可以向上转换成Vehicle对象,但是转成Vehicle对象不能向下转换成Car对象。这将导致ClassCastException。

重写方法

在子类中,你可以覆盖(重新定义)超类中定义的方法。下面是一个Java方法覆盖的例子:

public class Vehicle {

    String licensePlate = null;

    public void setLicensePlate(String licensePlate) {
        this.licensePlate = licensePlate;
    }
}
public class Car extends Vehicle {

    public void setLicensePlate(String license) {
        this.licensePlate = license.toLowerCase();
    }

}

注意Vehicle类和Car类如何定义一个名为setLicensePlate()的方法。现在,无论何时在Car对象上调用setLicensePlate()方法,都是调用的Car类中定义的方法。超类中的方法被忽略。

要覆盖一个方法,子类中的方法签名必须和超类中的相同。这意味着子类中的方法定义必须和超类方法具有完全相同的名称和相同的参数数量和类型,并且参数的顺序也要一致。否则,子类中的方法将被视为一个单独的方法。

在Java中,你不能覆盖超类的private方法。如果父类从其他方法内部调用私有方法,即使在子类中创建了具有相同签名的私有方法,它仍将继续从父类调用该方法。

@override注解

如果在子类中重写方法,如果超类中该方法突然被删除或重命名,或者改变了签名,那么子类中的方法不再覆盖超类中的方法。但你怎么能够知道父类方法的变动?如果编译器可以告诉你这个就显得非常好。

这就是Java @override注解的用途。可以将@override注解放在重写的方法的上方。示例如下:

public class Car extends Vehicle {

    @Override
    public void setLicensePlate(String license) {
        this.licensePlate = license.toLowerCase();
    }

}

调用父类方法

如果在子类中已经重写了父类的某个方法,但是你又需要调用父类的该方法,怎么办?你可以使用super关键字实现该目的,示例如下:

public class Car extends Vehicle {

    public void setLicensePlate(String license) {
        super.setLicensePlate(license);
    }

}

在上面的代码示例中,Car类中的方法setLicensePlate()调用了Vehicle类中的setLicensePlate()方法。

你可以从子类中的任何方法中调用超类实现的方法,你也可以从Car类中名为updateLicensePlate()的方法调用super.setLicensePlate(),该方法不会覆盖setLicensePlate()方法。

instanceof运算符

Java包含一个名为instanceof的运算符。instanceof运算符可以确定给定对象是否是某个类的实例。示例如下:

Car car = new Car();

boolean isCar = car instanceof Car;

这段代码执行后,isCar变量将包含值true。

instanceof指令也可以用来确定一个对象是否是它的类的一个超类的一个实例。下面是一个检查Car对象是否为Vehicle实例的例子:

Car car = new Car();

boolean isVehicle = car instanceof Vehicle;

假设Car类扩展(继承自)Vehicle类,则在执行此代码后,isVehicle变量将包含true值。 Car对象也是Vehicle对象,因为Car是Vehicle的子类。

正如你所看到的,instanceof指令可以用来检测继承层次结构。

与instanceof指令一起使用的变量类型不会影响其结果。看看这个例子:

Car car = new Car();

Vehicle vehicle = car;

boolean isCar = vehicle instanceof Car;

即使Car变量是Vehicle类型,它在本例中指向的对象也是Car对象。因此, vehicle instanceof Car 结果为true。

这里是相同的instanceof例子,但是使用Truck对象而不是Car对象:

Truck truck = new Truck();

Vehicle vehicle = truck;

boolean isCar = vehicle instanceof Car;

执行此代码后,isCar将包含值false。 Truck对象不是Car对象。

字段与继承

如前所述,Java字段不能在子类中重写。如果你在子类中定义了一个与超类中的字段名称相同的字段,则子类中的字段将掩蔽(遮蔽)超类中的字段。如果子类尝试访问该字段,它将访问子类中的字段。

但是,如果子类调用超类中的方法,并且该方法访问与子类中名称相同的字段,则它是被访问的超类中的字段。

下面是Java继承的例子,它演示了超类中的影子字段:

public class Vehicle {

    String licensePlate = null;

    public void setLicensePlate(String licensePlate) {
        this.licensePlate = licensePlate;
    }

    public String getLicensePlate() {
        return licensePlate;
    }
}
public class Car extends Vehicle {

    protected String licensePlate = null;

    @Override
    public void setLicensePlate(String license) {
        super.setLicensePlate(license);
    }

    @Override
    public String getLicensePlate() {
        return super.getLicensePlate();
    }

    public void updateLicensePlate(String license){
        this.licensePlate = license;
    }
}

注意这两个类如何定义一个licensePlate字段的。

Vehicle类和Car类都有setLicensePlate()和getLicensePlate()方法。 Car类中的方法调用了Vehicle类中相应的方法。结果是,最终这两组方法访问Vehicle类中的licensePlate字段。

然而,Car类中的updateLicensePlate()方法直接访问licensePlate字段。因此,它访问Car类的licensePlate字段。因此,如果在调用updateLicense()方法时调用setLicensePlate(),则不会得到相同的结果。

看看下面这段代码:

Car car = new Car();

car.setLicensePlate("123");
car.updateLicensePlate("abc");

System.out.println("license plate: "
        + car.getLicensePlate());

这段Java代码将打印出123。

updateLicensePlate()方法设置Car类中licensePlate字段的值。然而,getLicensePlate()方法返回Vehicle类中的licensePlate字段的值。而通过setLicensePlate()方法将会为Vehicle类中的licensePlate字段设置值123。所以会打印出123

构造方法与继承

Java继承机制不包含构造方法。换句话说,超类的构造方法不会被子类继承。子类仍然可以使用super()调用超类中的构造方法。事实上,需要子类中的构造方法首先就要求调用一个超类中的构造方法:

public class Vehicle {

    public Vehicle() {
    }
}
public class Car extends Vehicle{

    public Car() {
        super();

        //其他初始化操作
    }
}

注意Car构造方法内部对super()的调用。这个super()调用执行Vehicle类中的构造方法。

你可能以前看到过一些Java类,其中的子类构造方法似乎没有调用超类中调用构造方法。甚至超类中没有构造方法。但是,在这种情况,子类构造方法仍然调用超类构造方法。你只是看不到它。让我解释一下为什么:

如果一个类没有定义任何明确的构造方法,那么Java编译器会插入一个隐含的无参数构造方法。因此,一个类总是有一个构造方法。因此,以下这段代码与上面的等同:

public class Vehicle {
}

其次,如果构造方法没有显式调用超类中的构造方法,那么Java编译器就会隐式调用超类中的无参数构造方法。这意味着以下版本的Car类实际上等同于前面所示的版本:

public class Car extends Vehicle{

    public Car() {
    }
}

事实上,由于构造方法现在是空的,所以我们可以将其去掉,Java编译器将其插入,并向超类中的无参数构造函数插入一个隐式调用。

public class Vehicle {
}
public class Car extends Vehicle{
}

即使在这两个类中没有声明构造方法,它们都会得到一个无参数构造方法,而Car类中的无参数构造方法将调用Vehicle类中无参数构造方法。

如果Vehicle类没有没有参数的构造方法,但有另一个构造方法接受参数,那么Java编译器就会提醒你。 Car类将被要求声明一个构造方法,并在构造方法中调用Vehicle类中的构造方法。

嵌套类与继承

嵌套类也适用于上述Java继承规则。被声明为private的嵌套类不会被继承。如果子类与超类位于同一个包中,则只有子类可以访问具有默认(包)访问修饰符的嵌套类。具有受保护或公共访问修饰符的嵌套类总是由子类继承。

下面是一个嵌套的类继承的例子:

class MyClass {

    class MyNestedClass {

    }
}
public class MySubclass extends MyClass {

    public static void main(String[] args) {
        MySubclass subclass = new MySubclass();

        MyNestedClass nested =  subclass.new MyNestedClass();
    }
}

请注意,上面代码如何实现通过子类(MySubclass)实例来引用父类中的嵌套类的。

final类与继承

类也可以声明成final

public final class MyClass {

}

一个final类不能被扩展,换言之,不能创建final类的子类

抽象类与继承

在Java中,类可以被声明为抽象的。

抽象类是一个不包含完整实现的类。因此,它不能被实例化。换句话说,你不能创建一个抽象类的对象。

在Java中,抽象类是通过扩展来创建一个完整的实现。因此,完全可以扩展一个抽象类。抽象类和非抽象类的Java继承规则是相同的。

下一篇:嵌套类

results matching ""

    No results matching ""