CS61B 课程笔记(Lecture 09 Extends, Casting, Higher Order Functions)
CS61B 课程笔记(Lecture 09 Extends, Casting, Higher Order Functions)
继承与类型转换:高阶函数
继承
记住,继承允许子类重用已经定义类的代码。
1 | public class RotatingSLList<Item> extends SLList<Item> |
extends
关键字让我们保留 SLList
的原有功能,同时允许我们进行修改和添加额外的功能。
通过使用 extends
关键字,子类继承父类的所有成员。
“成员”包括:
- 所有实例和静态变量
- 所有方法
- 所有嵌套类
请注意,构造函数不被继承,且私有成员无法被子类直接访问。
构造函数:
- 构造函数不被继承,子类必须定义自己的构造函数。
- 子类的构造函数可以使用
super
关键字调用父类的构造函数,以确保父类的初始化逻辑被执行。
1
2
3
4
5 public class RotatingSLList<Item> extends SLList<Item> {
public RotatingSLList() {
super(); // 调用 SLList 的构造函数
}
}私有成员:
- 子类无法直接访问父类中的私有成员(即以
private
关键字定义的变量和方法)。- 若需要访问父类的私有成员,通常可以通过公共方法(如 getter 和 setter)来实现。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 public class SLList<Item> {
private Item head;
// 公共方法可以访问私有成员
public Item getHead() {
return head; // 子类可以通过此方法访问 head
}
}
public class RotatingSLList<Item> extends SLList<Item> {
public void someMethod() {
Item item = getHead(); // 可以通过公共方法访问父类的私有成员
}
}
VengefulSLList
1 | public class VengefulSLList<Item> extends SLList<Item> { |
构造函数不被继承
虽然构造函数不被继承,但 Java
要求所有构造函数必须以调用其超类的构造函数开始。
因此,我们可以显式调用超类的构造函数,使用 super
关键字,或者,如果我们选择不这样做,Java
会自动调用超类的无参数构造函数。
Object 类
Java 中的每个类都是 Object 类的后代,或者说扩展了 Object 类。
Object 类提供了每个对象应该能够执行的操作,比如
.equals(Object obj)
、.hashCode()
和
.toString()
。
Object
类提供了几个核心方法。
equals(Object obj)
功能:比较两个对象的内容是否相等。
默认实现:比较两个对象的内存地址(即它们是否是同一个对象)。
常用场景:在类中重写该方法以实现逻辑相等比较。
示例:
1
2
3
4
5
6
7
public boolean equals(Object obj) {
if (this == obj) return true; // 同一对象
if (!(obj instanceof MyClass)) return false; // 类型检查
MyClass other = (MyClass) obj;
return this.field.equals(other.field); // 自定义比较逻辑
}
hashCode()
功能:返回对象的哈希码,通常用于在哈希表中定位对象。
说明:如果重写
equals
方法,通常也应该重写hashCode
方法,以确保相等的对象具有相同的哈希码。示例:
1
2
3
4
public int hashCode() {
return Objects.hash(field); // 根据字段生成哈希码
}
toString()
功能:返回对象的字符串表示,通常用于调试和日志记录。
默认实现:返回对象的类名及其哈希码的十六进制表示。
常用场景:重写该方法以提供更具可读性的对象信息。
示例:
1
2
3
4
public String toString() {
return "MyClass{" + "field='" + field + '\'' + '}';
}
getClass()
功能:返回运行时类的
Class
对象,表示对象的类型。示例:
1
Class<?> clazz = obj.getClass(); // 获取对象的类
clone()
功能:创建并返回对象的副本(浅拷贝)。
说明:实现
Cloneable
接口的类可以使用此方法。默认实现是浅拷贝。示例:
1
2
3
4
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 调用父类的 clone 方法
}
is-a vs. has-a
extends
关键字定义了“是一个”(is-a)或超义词关系。
一个常见的错误是把它用于“拥有一个”(has-a)或部分关系。
has-a
关系表示一个类包含另一个类的实例,即类与类之间的组合关系。这通常用于表示“拥有”的属性,而不是继承。
封装
封装是面向对象编程的基本原则之一,也是我们作为程序员抵抗最大敌人:复杂性的方法之一。
此外,隐藏其他人不需要的信息也是管理大型系统时的一种基本方法。
封装的根本在于隐藏外部信息的概念。
在计算机科学中,模块可以定义为一组共同工作以执行任务或一组相关任务的方法。
如果一个模块的实现完全隐藏,并且只能通过文档化的接口访问,则称其为封装。
一些管理复杂性的工具:
分层抽象:
- 通过创建多个抽象层,将系统分为不同的层次,明确各层之间的责任和接口。这使得开发人员可以在不同层次上进行工作,而不必考虑整个系统的细节。
“为变化设计”(D. Parnas):
- 设计时考虑到未来可能的变化,通过将可变部分与固定部分分离,使得系统能够适应变化而不需要进行大规模重构。
以对象为中心组织程序:
- 将程序结构围绕对象进行组织,利用对象的封装特性来实现数据和方法的结合,从而提高系统的模块化。
让对象决定如何处理事情:
- 对象应当负责处理自己的状态和行为,而不是依赖外部代码进行操作。这种自我管理的能力提升了代码的独立性和可维护性。
隐藏其他人不需要的信息:
- 通过访问修饰符(如
private
和protected
)控制对类内部数据的访问,只暴露必要的公共方法,减少外部对内部状态的影响。
抽象边界
使用 Java 的 private
关键字,几乎不可能查看对象内部,确保底层复杂性不暴露给外部世界。
以下是关于继承如何破坏封装的详细笔记,包括代码示例和解释。
继承如何破坏封装
概述
在面向对象编程中,继承允许子类重用父类的代码,这在很多情况下是有益的。然而,继承也可能导致封装被破坏,特别是在方法重写和调用链中,可能会引发意想不到的问题,例如无限循环。以下通过代码示例来说明这一点。
示例代码分析
- 基础方法定义
1 | public void bark() { |
- 在这个示例中,
bark()
方法简单地打印 "bark"。 barkMany(int N)
方法调用bark()
方法,重复N
次,确保不会有循环调用的问题。
- 方法重写导致的无限循环
1 | public void bark() { |
- 在这里,
bark()
方法被重写,直接调用barkMany(1)
。 - 这样一来,
barkMany(1)
内部又会调用bark()
,导致了无限循环。
- 覆盖方法的影响
1 |
|
- 此方法重写了父类中的
barkMany
方法,并在内部调用了bark()
。 - 当
bark()
被调用时,它又会调用重写后的barkMany(1)
,从而导致无限循环。
封装破坏的原因
- 方法调用链:当子类重写父类的方法时,如果不清楚父类方法的实现,可能会意外地引入无限循环。
- 对内部状态的依赖:继承使得子类可以访问父类的私有成员,这种可见性可能导致子类在设计时无意中依赖于父类的内部实现,破坏了封装。
- 代码可读性和维护性:由于继承和方法重写的复杂性,代码的可读性和可维护性受到影响,容易产生混淆。
解决方法
- 使用组合而不是继承:通过组合方式来构建功能,避免子类过度依赖父类的实现。
- 谨慎使用方法重写:在重写父类的方法时,确保不会引入对父类实现的依赖,特别是涉及到方法调用的场景。
- 明确方法调用的意图:在设计类的接口时,清晰地记录方法的预期行为和调用关系,防止不必要的循环调用。
类型检查与类型转换
请记住,编译器根据对象的静态类型确定某事是否有效。
由于编译器只看到 sl 的静态类型是 SLList,它不允许 VengefulSLList “容器”
持有它。
1 | VengefulSLList<Integer> vsl = new VengefulSLList<Integer>(9); |
表达式
使用 new
关键字的表达式也有编译时类型。
1 | SLList<Integer> sl = new VengefulSLList<Integer>(); |
表达式右侧的编译时类型是 VengefulSLList。编译器检查 VengefulSLList 是否“是一个” SLList,并允许此赋值。
1 | VengefulSLList<Integer> vsl = new SLList<Integer>(); |
表达式右侧的编译时类型是 SLList。编译器检查 SLList 是否“是一个” VengefulSLList,但并非在所有情况下都如此,因此会导致编译错误。
方法调用的编译时类型等于其声明的类型。
1 | public static Dog maxDog(Dog d1, Dog d2) { ... } |
类型转换
通过类型转换,我们可以告诉编译器将一个表达式视为不同的编译时类型。
1 | Poodle largerPoodle = (Poodle) maxDog(frank, frankJr); |
类型转换是一种强大但危险的工具。
本质上,类型转换是在告诉编译器不要执行类型检查,让它相信你并按你想要的方式工作。
1 | Poodle frank = new Poodle("Frank", 5); |
当 maxDog
在运行时返回 Malamute,而我们尝试将 Malamute
转换为 Poodle 时,遇到运行时异常 -
ClassCastException
。
你需要确保你要转换的类型是可以且确实会发生的。有一些规则可以使用。
高阶函数
一流函数
高阶函数是将其他函数视为数据的函数。
在老版 Java(Java 7
及之前),内存盒(变量)无法包含指向函数的指针。
为了绕过这一点,我们可以利用接口继承。
1 | public interface IntUnaryFunction { |
继承速查表
继承关系
- 当我们定义一个类
VengefulSLList
继承自SLList
时,表示VengefulSLList
是SLList
的一种特殊化,即“是一个”关系。
1 | public class VengefulSLList<Item> extends SLList<Item> |
继承的成员
VengefulSLList
继承了 SLList
的所有成员,包括:
- 变量:所有实例变量和静态变量
- 方法:所有实例方法和静态方法
- 嵌套类:所有嵌套类
注意事项
- 构造函数:构造函数不被继承,子类的构造函数必须首先调用超类的构造函数。可以通过
super
关键字来实现这一点。
1 | public VengefulSLList() { |
覆盖方法
当子类重写(覆盖)父类的方法时,有两个重要的规则:
编译器的保守策略:
- 编译器在进行类型检查时,仅依据静态类型来决定允许的操作。静态类型是指声明时的类型。
1
SLList<Item> sl = new VengefulSLList<Item>(); // sl 的静态类型是 SLList
动态类型:
- 对于覆盖的方法(与重载的方法不同),实际调用的方法取决于调用表达式的动态类型。动态类型是指在运行时对象的实际类型。
1
2
3
4
5
6
7
8
9
10
11public void bark() {
System.out.println("Dog barks");
}
public void bark() {
System.out.println("Vengeful dog barks");
}
SLList<Item> sl = new VengefulSLList<Item>();
sl.bark(); // 实际调用的是 VengefulSLList 的 bark 方法
类型转换
- 类型转换:可以使用类型转换来超越编译器的类型检查。通过强制转换,程序员可以告诉编译器将一个对象视为另一种类型。
1 | VengefulSLList<Item> vsl = (VengefulSLList<Item>) sl; // 强制转换 |
- 风险:使用类型转换时要确保转换的对象确实是目标类型,否则会在运行时抛出
ClassCastException
。