CS61B 课程笔记(Lecture 16 Encapsulation, Lists, Delegation vs Extension)

高效编程

  • 名言:“一个工程师会以十美分的代价做任何傻瓜愿意花一美元做的事。” — 保罗·希尔芬格

效率的两种含义

  1. 编程成本
    • 开发时间:程序的开发需要多长时间?
    • 可读性与可维护性:代码的阅读、修改和维护难易程度。
  2. 执行成本(将在下周讨论):
    • 执行时间:程序的执行时间。
    • 内存需求:程序运行时所需的内存量。

本日重点

  • 重点在于降低编程成本,以便快速编写代码,并减少开发过程中的挫败感。开发人员在沮丧时效率会降低。

Java中的一些有用特性

    • 优点:有助于组织代码、实现包私有性。
    • 缺点:特定性强,限制了使用灵活性。
  • 静态类型检查
    • 优点:早期发现错误,代码阅读更像故事。
    • 缺点:灵活性不足,例如强制类型转换。
  • 继承
    • 优点:可以重用代码,避免重复。
    • 缺点:可能导致调试路径复杂,无法实例化抽象类或实现接口的所有方法。

封装

术语定义

  • 模块:一组协作的方法,用于完成某个任务或相关任务。
  • 封装:模块的实现完全隐藏,访问仅通过文档化的接口。

API(应用程序编程接口)

  • ADT的API包含构造函数和方法的列表,以及每个方法的简要描述。
  • API由语法语义规范组成:
    • 语法:编译器检查是否满足。
    • 语义:测试确保功能实现正确。

ADT(抽象数据类型)

  • ADT定义为基于行为而非实现的高层次类型。
    • 例如,Proj1中的双端队列(Deque)是一个ADT,具有某些行为(如addFirst、addLast),但实现可为ArrayDeque或LinkedListDeque。

练习 8.1.1

任务:使用链表实现栈类,需实现push(Item x)方法,确保类是泛型的。

三种实现方式:

  1. 扩展(Extension)

    1
    2
    3
    4
    5
    public class ExtensionStack<Item> extends LinkedList<Item> {
    public void push(Item x) {
    add(x); // 直接使用LinkedList的方法
    }
    }
    • 这种解决方案采用了扩展,借用LinkedList的方法并将其作为自己的方法。
  2. 委托(Delegation)

    1
    2
    3
    4
    5
    6
    public class DelegationStack<Item> {
    private LinkedList<Item> L = new LinkedList<Item>();
    public void push(Item x) {
    L.add(x); // 调用LinkedList的方法
    }
    }
    • 委托通过创建一个LinkedList对象并调用其方法来实现目标。
  3. 适配器模式(Adapter)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class StackAdapter<Item> {
    private List<Item> L; // 使用List接口
    public StackAdapter(List<Item> worker) {
    L = worker; // 接收实现List接口的类
    }

    public void push(Item x) {
    L.add(x); // 调用List的方法
    }
    }
    • 此方法类似于委托,但可以使用任何实现了List接口的类(如LinkedList、ArrayList等)。

委托与扩展的区别

  • “是一个” vs “拥有一个”
    • 一只猫有爪子(拥有一个)。
    • 一只猫是一种猫科动物(是一个)。
  • 委托:通过传入类的方法实现功能。
  • 扩展:通过继承父类的方法实现功能。

视图

  • 视图是现有对象的替代表示,限制用户对底层对象的访问。通过视图的更改将影响原始对象。

示例代码:

1
2
3
4
5
List<String> L = new ArrayList<>();
L.add("at");
L.add("ax");
List<String> SL = L.subList(1, 4); // 获取索引1到4的子列表
SL.set(0, "jug"); // 修改子列表的元素

反转子列表

  • 要反转部分列表的方法可通过创建一个通用反转函数来实现,因视图会修改其表示的底层对象。
  • 通过子列表操作直接影响原始列表。

注意事项

  • subList方法返回的是列表类型,并且通过偏移量访问原始列表的元素。
  • 使用访问方法确保子列表操作对原始列表的修改。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Item> sublist(int start, int end) {
return new this.Sublist(start, end); // 返回子列表
}

private class Sublist extends AbstractList<Item> {
private int start, end;
Sublist(int start, int end) { ... }

public Item get(int k) {
return AbstractList.this.get(start + k); // 使用外部类的get方法
}

public void add(int k, Item x) {
AbstractList.this.add(start + k, x); // 添加元素到外部类
end += 1;
}
}
  • AbstractList.this用于从内部类中引用外部类的实例。

总结

  • 设计API是复杂的,但保持一致的设计理念会使代码更清晰易于维护。
  • 虽然继承很诱人,但应谨慎使用,仅在对类的属性有充分了解时才使用。