跳转至

6.0 Inheritance, Method Overriding

Inheritance

  • code reuse and abstraction
  • A subclass inherits all the members (fields and methods, not constructors) of its superclass

BasicSafeArray

public class BasicSafeArray {
    protected int size;
    protected int[] array;

    public BasicSafeArray(int size) {
        // see full code
    }

    public int size() {
        // see full code
    }

    protected boolean isValidIndex(int index) {
        // see full code
    }

    public int get(int index) {
        // see full code
    }

    public void set(int index, int value) {
        // see full code
    }
}

AppendableSafeArray

public class AppendableSafeArray extends BasicSafeArray {
    // Constructor
    AppendableSafeArray(int size) {
        super(size);
    }

    // Method to append a value
    public void append(int value) {
        int newSize = size + 1;
        int[] newArray = new int[newSize];

        // Copying old array elements to new array
        for (int i = 0; i < size; i++) {
            newArray[i] = array[i];
        }

        // Adding the new value
        newArray[size] = value;

        // Updating size and array reference
        size = newSize;
        array = newArray;
    }

    // Main method for testing
    public static void main(String[] args) {
        AppendableSafeArray safeArray = new AppendableSafeArray(5);
        safeArray.append(10);
        // You can add more test cases here
    }
}
  • extends 关键词继承非 private 方法与字段
  • 第三行可以看到需要自己重写 constructor
  • 使用 super 调用父类 constructor 实现初始化 field size

protected

protectedis similar to private, but can be accessed by subclasses

但是有一些例外,讨论包时再详细讨论,另外类、方法、字段如果不写权限默认私有

Field Initialisation

即使没有 constructor手动初始化 Java 也会自动加一个,因此打印出来字段值是零

  • 字段的工作方式和数组差不多。在对象构建之前,它们会被初始化成一些默认值
  • 所有数值类型(比如int、long、char、double、float 等等)都会被初始化成零
  • 布尔值会被设置为 false
  • 对象和数组会被设置为 null

Superclass Constructors

public class AppendableSafeArray extends BasicSafeArray {
    AppendableSafeArray(int size) {
        this.size = size;
        this.array = new int[size];
    }
}
AppendableSafeArray.java:2: error: constructor BasicSafeArray in
class BasicSafeArray cannot be applied to given types; ... etc
  • Java 有个规矩:当你创建子类实例时,它必须去调用父类的一个构造函数来完成父类部分的初始化。如果你不明确告诉它调用哪个,Java 就默认去调用父类的无参数构造函数(就是 BasicSafeArray() 这种括号里啥也没有的)。
  • 这段代码里的父类 BasicSafeArray 很可能没有提供一个无参数的构造函数,或者它有一个需要参数的构造函数,导致 Java 找不到它想默认调用的那个 BasicSafeArray()。所以就报错说:“父类的构造函数没法用啊!”
  • 为了解决上面的问题,你可以在父类 BasicSafeArray 里面明确地添加一个无参数的构造函数
public BasicSafeArray() {
    this(0);
}
  • this(0); 特殊用法,就是去调用同一个类 BasicSafeArray 中那个需要一个整数(这里是0)作为参数的构造函数。这是一种常见的技巧,用一个构造函数去调用另一个,避免代码重复。
  • 如果你在一个构造函数里想用 this(...)(调用自己类的其他构造函数)或者 super(...)(调用父类的构造函数),那么这句调用必须写在这个构造函数的第一行!不能在它前面写任何其他的代码

Hiding Memebrs

  • super 来访问超类的构造函数、成员变量或方法
  • 在你想覆盖子类中隐藏的字段或方法时很有用
  • 如果我们子类中重新声明了一个字段或方法,它会隐藏从父类继承的成员
  • 一般来说,我不建议对字段这么做,这通常意味着你的类设计得不好
public class HiddenThing extends Thing {
    public int x; // Hides super.x
    protected float y; // Hides super.y

    public void setSuperTo10() {
        super.x = 10;
        super.y = 10;
    }

    public void setTo100() {
        x = 100;
        y = 100;
    }

    public void print() {
        // see full code
    }

    public void printSuper() {
        // see full code
    }

    public static void main(String[] args) {
        HiddenThing t = new HiddenThing();
        t.setTo100();
        t.setSuperTo10();
        t.print();
        t.printSuper();
    }
}

super in Inheritance Chains

class A {
    public void doSomething() {
        System.out.println("A.doSomething()");
    }
}

class B extends A {
    public void doSomething() {
        System.out.println("B.doSomething()");
    }
}

class C extends B {
    public void doSomething() {
        super.doSomething();
    }
}

public class SuperclassMembers {
    public static void main(String[] args) {
        C c = new C();
        c.doSomething();
    }
}

第十五行的 super 指向的到底是哪个 class

super always refers to the immediate superclass 也就是永远是 extends 参数后面的那个类

Multiple Subclasses

一个类可以被多个类拓展,我们可以创建任意复杂的继承树,而不仅仅是链式结构

public class BasicSafeArray {
    // see full code
}

public class AppendableSafeArray extends BasicSafeArray {
    public void append(int value) {
        // see full code
    }
}

public class DeletableSafeArray extends BasicSafeArray {
    public DeletableSafeArray(int size) {
    super(size);

    public void delete(int index) {
        // todo
        }
    }
}

那么在正式软件工程中可以模拟系统发育树 Phylogenetic Tree 的层级进行类的组织

Superclass References

Java 作为强类型语言如果数据类型不匹配会报错,除非有自动或强制类型转换

但是 X myObj = new Y() 如果 Y extends X 就不会报错,这是为什么

  • When we say Y extends X we’re say that Y is a X
  • This is a relationship is really what extends captures

Method Overriding

由此引出 polymorphism 多态性

class Animal {
    public void talk() {
        System.out.println("*Generic animal sounds*");
    }
}

class Goose extends Animal {
    public void talk() {
        super.talk();
        System.out.println("Honk!");
    }
}

public class SuperGoose {
    public static void main(String[] args) {
        Goose a = new Goose();
        a.talk();
    }
}

The talk method is getting hidden

Dynamic Dispatch

动态分派是一种在运行时确定要调用的方法的技术。它允许程序根据对象的实际类型而不是声明的类型来选择正确的方法。这使得代码更灵活,支持多态性,并允许在运行时更改行为。

public class AnimalOverride {
    public static void main(String[] args) {
        Animal a = new Animal();
        a.talk();
        a = new Goose();
        a.talk();
        a = new Dog();
        a.talk();
    }
}

由于 Goose, Dog 都是 Animal 因此这样分配没有任何问题,会在执行时自动根据对象类型寻找方法,这实现了多态性

Expression

这道题就是 Test1 中的,可以用于编写简易编程语言

public class Expression {
    public void describe() {
        System.out.println("unknown");
    }

    public int evaluate() {
        return 0;
    }

    public static void main(String[] args) {
        Expression expr = new Multiply(new Value(2), new Value(3));
        expr.describe();
        System.out.println(" = " + expr.evaluate());
        expr = new Add(new Value(2), new Value(3));
        expr.describe();
        System.out.println(" = " + expr.evaluate());
        expr = new Add(new Multiply(new Value(2), new Value(3)), new Value(4));
        expr.describe();
        System.out.println(" = " + expr.evaluate());
        expr = new Value(1);
        for (int i = 2; i <= 10; i++)
            expr = new Add(expr, new Value(i));
        expr.describe();
        System.out.println(" = sum of 1 to 10 = " + expr.evaluate());
    }
}

可以用其多态性的应用去实现,这道题上课的时候 11 实际上花费了大量时间讲解

Multiply
class Multiply extends Expression {
    private Expression left;
    private Expression right;

    public Multiply(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    public void describe() {
        System.out.print("(");
        left.describe();
        System.out.print(" * ");
        right.describe();
        System.out.print(")");
    }

    public int evaluate() {
        return left.evaluate() * right.evaluate();
    }
}
Add
class Add extends Expression {
    private Expression left;
    private Expression right;

    public Add(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    public void describe() {
        System.out.print("(");
        left.describe();
        System.out.print(" + ");
        right.describe();
        System.out.print(")");
    }

    public int evaluate() {
        return left.evaluate() + right.evaluate();
    }
}
expr = new Add(new Multiply(new Value(2), new Value(3)), new Value(4));
expr.describe();
System.out.println(" = " + expr.evaluate());

这样的调用方式会启用 stack tree 完成

@Override

class Animal {
    public void talk() {
        System.out.println("*Generic animal sounds*");
    }
}

class Goose extends Animal {
    public void tallk() { // Oops, typo!
        System.out.println("Honk!");
    }
}

public class AnimalTypo {
    public static void main(String[] args) {
        Animal a = new Goose();
        a.talk();
    }
}

重载的问题就在于如果一不小心打错字那么编译器不会帮我们识别,因此加一个 annotation 后编译器会直接返回错误,这种方法更为严谨符合软件工程

SDLC

image-20250502101533697

这就是软件开发生命周期,讲述了软件开发是怎么样进行的,循环的本质反映了开发软件通常是迭代的。Java 和类很适合 SDLC 的开发流程:

  • 设计阶段需要把问题分解成一个个类。
  • 实现阶段就是编写这些类。
  • 类为我们提供了清晰的、可测试的代码“单元”,这使得测试更容易。
  • 数据隐藏和抽象让我们在不改变接口的情况下更新实现,从而使维护更容易。