CS61B 课程笔记(Lecture 08)

继承与接口

方法重载

在 Java 中,方法重载是指在同一个类中定义多个同名但参数不同的方法。编译器根据传入参数的数量和类型来选择正确的方法。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MathUtils {
public int add(int a, int b) {
return a + b;
}

public double add(double a, double b) {
return a + b;
}

public int add(int a, int b, int c) {
return a + b + c;
}
}

在上面的代码中,add 方法被重载了三次,分别接受不同数量和类型的参数。

超类与子类

在面向对象编程中,超类(hypernym)是指更通用的类别,而下类(hyponym)则是更具体的类别。例如,"狗"是一个超类,而"贵宾犬"、"哈士奇"和"马拉穆特犬"都是它的下类。它们之间存在“是一个”的关系(is-a relationship)。

接口定义

在 Java 中,接口用于定义类应该实现的一组方法。下面是一个接口 List61B 的定义:

1
2
3
4
5
6
7
8
9
10
public interface List61B<Item> {
void addFirst(Item x);
void addLast(Item y);
Item getFirst();
Item getLast();
Item removeLast();
Item get(int i);
void insert(Item x, int position);
int size();
}

这里,我们定义了一个通用的列表接口 List61B,列出了所有需要实现的方法。注意,这里没有提供具体的实现。

第二步:实现接口的类,如 SLListAList,需要声明它们实现该接口:

1
2
3
public class AList<Item> implements List61B<Item> {
// 方法的具体实现
}

方法重写

在子类中实现接口的方法时,建议在方法签名上方添加 @Override 注解,以提高代码可读性,并确保你正确地重写了方法。示例代码如下:

1
2
3
4
@Override
public void addFirst(Item x) {
insert(x, 0);
}

使用 @Override 注解可以帮助编译器在编译时检查你是否真正重写了父类或接口中的方法。

接口继承

接口只包含方法签名,子类必须实现接口中定义的所有方法。如果没有实现,将导致编译错误。

实现继承(默认方法)

Java 8 引入了默认方法,允许在接口中定义方法的默认实现。这样,子类可以继承这些实现,而不必每次都重新实现。下面是一个例子:

1
2
3
4
5
6
7
8
public interface List61B<Item> {
default void print() {
for (int i = 0; i < size(); i++) {
System.out.print(get(i) + " ");
}
System.out.println();
}
}

在这里,print 方法提供了一个默认实现,子类可以选择重写这个方法或直接使用默认实现。

重写默认方法

如果子类需要自定义默认方法的实现,可以简单地重写它。例如:

1
2
3
4
5
6
public class MyList<Item> extends AList<Item> {
@Override
public void print() {
System.out.println("My custom print method.");
}
}

静态类型与动态类型

每个变量都有静态类型和动态类型:

  • 静态类型(Static Type):在编译时确定的类型。例如,List61B<String> lst 的静态类型是 List61B
  • 动态类型(Dynamic Type):在运行时确定的类型。例如,如果 lst 是通过 new SLList<String>() 创建的,则它的动态类型是 SLList

动态方法选择

在运行时,Java 使用动态类型来选择方法实现。规则如下:

  • 如果静态类型 X 和动态类型 Y,且 Y 重写了 X 的方法,则调用 Y 中的方法。
  • 否则,调用 X 中的方法。

这种机制使得子类可以提供自己的实现,增强了灵活性。

重载与动态方法选择

对于重载方法,Java 在编译时根据静态类型选择调用哪个方法。这意味着,重载和动态方法选择是两个独立的概念。

类型规则

  • 编译器允许内存框持有任何子类型:例如,一个类型为 List61B 的变量可以持有任何实现了该接口的对象,如 AListSLList
  • 方法调用基于静态类型:编译器根据声明的类型决定调用哪个方法。
  • 重写的非静态方法在运行时根据动态类型选择:如果对象的动态类型重写了某个方法,那么在运行时将调用该实现。
  • 重载的方法在编译时被选择:重载的选择只依赖于静态类型和方法参数。

接口继承(what)

示例:假设我们有一个 Shape 接口,定义了一些基本的几何形状应具备的方法:

1
2
3
4
public interface Shape {
double area(); // 计算面积
double perimeter(); // 计算周长
}

在这个接口中,areaperimeter 方法定义了所有实现该接口的类应具备的功能。具体的实现则由每个具体形状(如 CircleRectangle 等)来决定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Circle implements Shape {
private double radius;

public Circle(double radius) {
this.radius = radius;
}

@Override
public double area() {
return Math.PI * radius * radius;
}

@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}

实现继承(how)

示例:假设我们有一个 Animal 类,提供了一些基本的功能实现,并包含一个默认方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Animal {
public void eat() {
System.out.println("This animal is eating.");
}

public void sleep() {
System.out.println("This animal is sleeping.");
}

public void makeSound() {
System.out.println("Some generic animal sound.");
}
}

然后,我们可以创建一个 Dog 类,继承自 Animal,并选择重写 makeSound 方法:

1
2
3
4
5
6
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Bark!");
}
}

在这个例子中,Animal 类提供了方法的具体实现,子类 Dog 复用了 Animal 中的 eatsleep 方法,但重写了 makeSound 方法以提供特定的行为。这种方式减少了代码重复,同时给了设计者更多的控制权。

注意:子类与超类之间的关系应为“is-a”关系,这样才能正确地利用继承。

实现继承的缺点

  • 追踪实现变得复杂:可能会遗忘自己重写了某个方法。
  • 冲突解决规则复杂:当多个父类有同名方法时,处理起来可能比较麻烦。
  • 代码复杂性增加:过度使用实现继承可能导致代码难以理解。
  • 破坏封装性:如果不小心重写了父类的方法,可能会影响到原本的封装性和行为。