Lesson5 继承

1 概念

  • 被继承的类成为父类(superclass),继承后产生的类成为子类(subclass)
  • 单继承:如果子类只能有一个直接父亲,称为单继承
  • 多继承:如果子类可以有多个直接父类,称为多继承
  • 基类是对若干个派生类的抽象,派生类是基类的具体化
  • 抽象类:抽象基类中有些操作并未实现,在非抽象的派生类中实现抽象基类中未实现的操作
    • 抽象的方法没有 {},以 ; 结尾
abstract class Animal{
	public abstract void eat();
	public abstract void sleep();
}

2 语法&要点

  1. 类继承通过关键字extends实现,继承格式为
[修饰符] class 类名 extends 父类名{
	类体;
}

2.1 子类构造函数

当然,以下是一些使用Java语言的示例,以解释上述的规则:

  1. 构造函数不能被继承:
class Parent {
	public Parent() {
		// 父类的构造函数
	}
}
class Child extends Parent {
	// 子类不会继承父类的构造函数,而是定义自己的构造函数
	public Child() {
		// 子类的构造函数
	}
}

在这个例子中,Child 类不会继承 Parent 类的构造函数,而是需要定义自己的构造函数。

  1. 无参子类构造函数的编写:子类可以通过super()显方式调用父类无参的构造函数,也可以隐式调用
class Parent {
	public Parent() {
		System.out.println("Parent constructor");
	}
}
class Child extends Parent {
	public Child() {
		super(); // 显式调用父类无参构造函数
		System.out.println("Child constructor");
	}
}
	```
在这个例子中,`Child` 类的构造函数显式地调用了 `Parent` 类的无参构造函数。

隐式调用父类无参构造函数的例子:
```java
class Child extends Parent {
	public Child() {
		System.out.println("Child constructor");
		// 如果没有显式调用super(),编译器会默认插入super()调用
	}
}

在这个例子中,虽然没有显式调用 super(),但编译器会在 Child 构造函数的开始处自动插入对 Parent 类无参构造函数的调用。

  1. 有参子类构造函数的编写:初始化父类的成员变量;初始化子类的成员变量;必须显式调用父类有参构造函数
class Parent {
	private int value;
	public Parent(int value) {
		this.value = value;
		System.out.println("Parent constructor with value: " + value);
	}
}
class Child extends Parent {
	private int childValue;
	public Child(int value, int childValue) {
		super(value); // 必须显式调用父类有参构造函数
		this.childValue = childValue;
		System.out.println("Child constructor with childValue: " + childValue);
	}
}

在这个例子中,Child 类的构造函数必须首先调用 Parent 类的有参构造函数来初始化继承的成员变量。

  1. thissuper 调用必须是第一条可以执行语句:
    正确示例:
class Child extends Parent {
	public Child() {
		super(); // 第一个语句,正确
		// 其他代码...
	}
	public Child(int value) {
		this(); // 第一个语句,正确
		// 其他代码...
	}
}
	```
错误示例:
```java
class Child extends Parent {
	public Child() {
		// 其他代码...
		super(); // 不是第一个语句,错误
	}
}

在错误示例中,尝试在 super() 之前执行其他代码会导致编译错误,因为 super() 必须是构造函数中的第一个执行语句。

2.2 子类对象的生成

  1. 创建子类对象时,子类总是按层次结构从上到下的顺序调用所有超类的构造函数。如果继承和组合联用,要先构造基类的构造函数,然后调用组合对象的构造函数(组合按照声明的顺序调用)。
    例:
class Engine {
	public Engine() {
		System.out.println("Engine constructor");
	}
}
class Wheel {
	public Wheel() {
		System.out.println("Wheel constructor");
	}
}
class Vehicle {
	public Vehicle() {
		System.out.println("Vehicle constructor");
	}
}
class Car extends Vehicle {
	private Engine engine;
	private Wheel wheel;
	public Car() {
		super(); // 调用基类Vehicle的构造函数
		engine = new Engine(); // 调用组合对象Engine的构造函数
		wheel = new Wheel(); // 调用组合对象Wheel的构造函数
		System.out.println("Car constructor");
	}
}

在这个例子中,当创建一个Car对象时,构造函数的调用顺序如下:

  1. Vehicle构造函数(继承)
  2. Engine构造函数(组合,按照声明顺序)
  3. Wheel构造函数(组合,按照声明顺序)
  4. Car构造函数(当前类的构造函数)
    输出将会是:
Vehicle constructor
Engine constructor
Wheel constructor
Car constructor
  1. 如果父类没有不带参数的构造方法,则在子类的构造方法中必须明确的告诉调用父类的某个带参数的构造方法,通过super关键字,这条语句还必须出现在构造方法的第一句。
    例:
class Parent {
	private int value;
	// 父类有一个带参数的构造方法,没有无参构造方法
	public Parent(int value) {
		this.value = value;
		System.out.println("Parent constructor with value: " + value);
	}
}
class Child extends Parent {
	// 子类的构造方法必须调用父类的带参数构造方法
	public Child(int value) {
		super(value); // 必须是第一句
		System.out.println("Child constructor");
	}
}

在这个例子中,Child 类继承自 Parent 类,并且 Parent 类只有一个带参数的构造方法。因此,在Child类的构造方法中,我们使用 super(value); 来调用 Parent 类的带参数构造方法,并且这必须是构造方法中的第一句。如果我们不这样做,编译器会报错,因为 Parent
类没有无参构造方法可供隐式调用。

  1. 子类创建对象时,子类的构造方法总是先调用父类的某个构造方法,完成父类部分的创建;然后再调用子类自己的构造方法,完成子类部分的创建
  2. 如果子类的构造方法没有明显地指明使用父类的哪个构造方法,子类就调用父类的不带参数的构造方法 。
  3. 子类在创建一个子类对象时,不仅子类中声明的成员变量被分配了内存,而且父类的所有的成员变量也都分配了内存空间。

2.3 private与protected

  • 将基类数据成员定义成 private,派生类中通过相应的访问函数进行访问
  • protcted会破坏基类的封装性:派生类可以直接访问和修改
  • 关于保护成员的使用
    • 如果基类仅向其派生类提供服务,而不对其他客户提供该服务,使用protected成员访问说明符是合适的
    • 例如Owner和Son、Wife、Daughter是父子关系,Owner的车和房产可以被子类直接访问, 但是不对其他类开放,那么车和房产可以声明为protected的权限。

2.4 向上转型(upcasting)

向上转型(Upcasting)是面向对象编程中的一个概念,它指的是将子类对象引用转换为父类对象引用的过程。向上转型是安全的,因为它是一种隐式转换,不需要显式代码来实现。向上转型之所以被称为“向上”,是因为在继承层次结构中,父类位于子类的上方。

以下是向上转型的几个关键点:

  1. 继承关系:向上转型发生在继承关系中,其中子类是父类的特化。
  2. 类型兼容:由于子类继承了父类的方法和属性,因此子类对象可以被视为父类对象。
  3. 隐式转换:向上转型通常是隐式发生的,不需要任何特殊的语法。

例:

class Animal {
    void sound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    void sound() {
        System.out.println("Dog says: Bow Wow");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog(); // 创建一个Dog对象
        Animal animal = dog; // 向上转型:将Dog对象赋值给Animal类型的引用
        animal.sound(); // 输出 "Dog says: Bow Wow"
    }
}

在上面的例子中:

  • Animal 是一个父类,它有一个名为 sound 的方法。
  • Dog 是 Animal 的子类,它重写了 sound 方法,提供了更具体的实现。
  • 在 main 方法中,我们创建了一个 Dog 对象,并将其赋值给了一个 Animal 类型的引用。这个过程就是向上转型。

向上转型的用途

  • 多态性:向上转型是实现多态性的基础。通过将子类对象引用赋给父类引用,可以编写只依赖于父类接口的代码,这样的代码可以处理任何实现了该父类的子类对象
  • 方法参数:在需要父类类型作为参数的方法中,可以传入任何子类对象,这增加了代码的灵活性和可重用性。

注意事项

  • 向上转型后,只能调用父类中定义的方法,即使子类可能有更多方法也是如此。
  • 如果子类重写了父类的方法,那么调用该方法时会执行子类的实现
  • 向上转型是安全的,但如果是向下转型(将父类引用转换为子类引用),则需要显式转换,并且必须确保引用的实际对象类型与目标子类兼容,否则会抛出  ClassCastException

2.5 隐藏和覆盖

  • 变量隐藏:在子类对父类的继承中,如果子类的成员变量和父类的成员变量同名,此时称为子类隐藏(override)了父类的成员变量。
    • 子类若要引用父类的同名变量。要用 super 关键字做前缀加圆点操作符引用, 即 super.变量名
  • 方法覆盖:名称、参数、返回类型与父类方法完全相同
    • 私有方法、静态方法不能被覆盖
    • fianl 声明的成员方法是最终方法,最终方法不能被子类覆盖(重写)
  • 方法隐藏:名称相同
public class other {  
    public static void main(String[] args) {  
        Subclass s = new Subclass();  
        System.out.println(s.x + " " + s.y + " " + s.z);  
        System.out.println(s.method());  
        Base b2 = s;        // 正确  
        Base b3 = (Base)s;  // 正确  
        System.out.println(b2.x + " " + b2.y + " " + b2.z);  
        System.out.println(b2.method());  
    }  
}  
  
class Base {  
    int x = 1;  
    static int y = 2;  
    int z = 3;  
      
    int method() {  
        return x;  
    }  
}  
  
class Subclass extends Base {  
    int x = 4;  
    int y = 5;  
    static int z = 6;  
      
    int method() {  
        return x;  
    }  
}

【输出】
4 5 6
4
1 2 3
4
class Planet{  
    public static void hide(){  
        System.out.println("The hide method in Planet");  
    }  
    public void override(){  
        System.out.println("The override method in Planet");  
    }  
}  
  
public class Earth extends Planet{  
    public static void hide(){  
        System.out.println("The hide method in Earth");  
    }  
    public void override(){  
        System.out.println("The override method in Earth");  
    }  
    public static void main(String[] args) {  
        Earth myEarth = new Earth();  
        Planet myPlanet = (Planet) myEarth;
        myPlanet.hide();  
        myPlanet.override();  
    }  
}

【输出】
The hide method in Planet
The override method in Earth
  1. myPlanet.hide(); 调用的是 Planet 类的静态方法 hide。由于静态方法是绑定到类而不是对象的,所以即使 myPlanet 引用的是一个 Earth 类型的对象,它仍然会调用 Planet 类的 hide 方法。因此,输出是 “The hide method in Planet”。
  2. myPlanet.override(); :由于 myPlanet 是一个 Planet 类型的引用,但它指向的是一个 Earth 类型的对象,它将调用 Earth 类中覆盖的 override 方法。这是因为 override 是一个非静态方法,Java 使用动态绑定(也称为运行时绑定)来确定调用哪个方法。如果 myPlanet 实际上指向的是一个 Earth 对象,那么调用 override 方法时,会调用 Earth 类中的版本,输出是 “The override method in Earth”。

3 继承的优缺点

3.1 优点

  • 实现代码共享,减少创建类的工作量,使子类可以拥有父类的方法和属性。
  • 提高代码维护性和可重用性
  • 提高代码的可扩展性,更好的实现父类的方法。

3.2 缺点

  • 继承是侵入性的。只要继承,就必须拥有父类的属性和方法。
  • 降低代码灵活性。子类拥有父类的属性和方法后多了些约束。
  • 增强代码耦合性(开发项目的原则为高内聚低耦合)。当父类的常量、变量和方法被修改时,需要考虑子类的修改,有可能会导致大段的代码需要重构。
Built with MDFriday ❤️