CS61B学习笔记(七)-Rd4.1-导言和接口
4.1 Intro and interfaces · Hug61B (gitbooks.io)
方法重载(method overloading)
1 | public static String longest(SLList<String> list) |
这就是所谓的方法重载。当您调用 WordUtils.longest
时,Java 会根据您提供的参数类型知道要运行哪一个。如果为其提供 AList
,它将调用 AList
方法。与 SLList
相同。
Java 足够聪明,知道如何为不同类型的两种相同的方法处理,这很好,但重载有几个缺点:
- 这是超级重复和丑陋的,因为你现在有两个几乎相同的代码块。
- 它需要更多的代码来维护,这意味着如果你想对方法进行一些小的更改,例如更正一个错误,你需要在
longest
方法中为每种类型的列表进行更改。 - 如果我们想创建更多的列表类型,我们必须为每个新的列表类复制该方法。
上位词、下位词和接口继承
Hypernyms, Hyponyms, and Interface Inheritance
狗是贵宾犬、雪橇犬、哈士奇等的上位词。相反,贵宾犬、雪橇犬和哈士奇是狗的下位词。
这些词构成了“is-a”关系的层次结构:
- a poodle “is-a” dog
贵宾犬“is-a”狗 - a dog “is-a” canine
一只狗“是”犬 - a canine “is-a” carnivore
犬科动物“IS-A”食肉动物 - a carnivore “is-an” animal
食肉动物“is-an”动物
同样的层次结构也适用于 SLLists 和 ALists!SLList 和 AList 都是更一般的列表的下位词。
我们将在 Java 中形式化这种关系:如果 SLList 是 List61B 的下位词,那么 SLList 类是 List61B 类的子类,而 List61B 类是 SLList 类的超类。
图 4.1.1 :
在 Java 中,为了表达这个层次结构,我们需要做两件事:
- 第 1 步:为常规列表超义词定义一个类型——我们将选择名称 List61B。
- 第 2 步:指定 SLList 和 AList 是该类型的下位词。
新的 List61B 是 Java 所说的**接口(interface)**。它本质上是一个指定列表必须能够执行的操作的协定,但它不为这些行为提供任何实现。
这是我们的 List61B 接口。至此,我们已经完成了建立关系层次结构的第一步:创建超义词。
1 | public interface List61B<Item> { |
现在,要完成步骤 2,我们需要指定 AList 和 SLList 是 List61B 类的下位词。在 Java 中,我们在类定义中定义了这种关系。
我们将添加
1 | public class AList<Item> {...} |
定义关系的词:implements。
1 | public class AList<Item> implements List61B<Item>{...} |
implements List61B<Item>
本质上是一个承诺。AList 说“我保证我将拥有并定义 List61B 接口中指定的所有属性和行为”
现在我们可以编辑我们 longest
的方法 WordUtils
以接收 List61B。因为 AList 和 SLList 共享“is-a”关系。
方法覆盖(@Override)
在子类中实现所需的函数时,在方法签名的顶部包含 @Override
标记很有用(实际上在 61B 中也是必需的)。在这里,我们只用一种方法做到了这一点。
1 |
|
需要注意的是,即使您不包含此标记,您仍然会覆盖该方法。所以从技术上讲,你不必包括它。但是,包含标记可以作为程序员的一种保护措施,提醒编译器您打算重写此方法。你问为什么这会有帮助?嗯,这有点像有一个校对员!编译器会告诉您过程中是否出现问题。
为什么我们要用Override呢?:
- 主要原因:防止打字错误。
- 如果你说@Override,但如果这个方法没有覆盖任何东西,你会得到一个编译错误。
- 例如public void addLats(项目x)
- 提醒程序员,方法定义来自继承层次结构中更高的位置。
接口继承(Interface Inheritance)
接口继承是指子类继承超类的所有方法/行为的关系。正如我们在 Hyponyms 和 Hypernyms 部分中定义的 List61B 类一样,该接口包括所有方法签名,但不包括实现。由子类实际提供这些实现。
这种继承也是多代的。这意味着,如果我们有一长串的超类/子类关系,如图 4.1.1 所示,AList 不仅继承了 List61B 的方法,而且还继承了它上面的所有其他类,一直到最高超类 AKA AList 继承自 Collection。
GRoE
回想一下我们在第一章中介绍的平等黄金法则。这意味着每当我们进行赋值时 a = b
,我们都会将 b 中的位复制到 a 中,并要求 b 与 a 的类型相同。你不能分配 Dog b = 1
OR Dog b = new Cat()
,因为 1 不是狗,猫也不是。
让我们尝试将此规则应用于我们之前在本章中编写 longest
的方法。
public static String longest(List61B<String> list)
接受 List61B。我们说过这也可以接受 AList 和 SLList,但由于 AList 和 List61B 是不同的类,这怎么可能呢?好吧,回想一下,AList 与 List61B 共享“is-a”关系,这意味着 AList 应该能够放入 List61B 框中!
1 | public static void main(String[] args) { |
当它运行时,将创建 SLList,并且其地址存储在 someList 变量中。然后将字符串“elk”插入到 addFirst 引用的 SLList 中。
实现继承
以前,我们有一个接口 List61B,它只有标识 List61B 应该做什么的方法标头。但是,现在我们将看到我们可以在 List61B 中编写已经填写了实现的方法。这些方法确定 List61B 的上位词应如何表现。
为此,必须在方法签名中包含 default
关键字。
如果我们在 List61B 中定义此方法
1 | default public void print() { |
那么所有实现 List61B 类的东西都可以使用该方法!
然而,这种方法对于SLList来说过于慢,因为他是基于链表实现的列表,这样子相当于运用了两次for循环
我们希望 SLList 的打印方式与其接口中指定的方式不同。为此,我们需要覆盖它。在 SLList 中,我们实现了这种方法;
1 |
|
现在,每当我们在 SLList 上调用 print() 时,它都会调用此方法而不是 List61B 中的方法。
动态类型以及动态方法选择
您可能想知道,Java 如何知道要调用哪个 print()?问得好。Java 之所以能够做到这一点,是因为有一种叫做动态方法选择的东西。
我们知道 java 中的变量有一个类型。
1 | List61B<String> lst = new SLList<String>(); |
In the above declaration and instantiation, lst is of type “List61B”. This is called the “static type”
在上面的声明和实例化中,lst 的类型为“List61B”。这称为“静态类型”
但是,对象本身也具有类型。LST 指向的对象类型为 SLList。尽管这个对象本质上是一个 SLList(因为它被声明为 SLList),但它也是一个 List61B,因为我们之前探讨过的“is-a”关系。但是,由于对象本身是使用 SLList 构造函数实例化的,因此我们将其称为“动态类型”。
动态类型:
- 这是在实例化时指定的类型(例如在使用new时)。
- 等于所指向的对象的类型。
当 Java 运行被重写的方法时,它会在其动态类型中搜索适当的方法签名并运行它。
重要提示:这不适用于重载方法!
假设同一类中有两个方法
1 | public static void peek(List61B<String> list) { |
然后运行此代码
1 | SLList<String> SP = new SLList<String>(); |
对 peek() 的第一次调用将使用第二个 peek 方法,该方法接受 SLList。对 peek() 的第二次调用将使用第一个 peek 方法,该方法接受 List61B。这是因为两个重载方法之间的唯一区别是参数的类型。当 Java 检查要调用的方法时,它会检查静态类型并使用相同类型的参数调用该方法。
动态方法不适用于方法重载,方法重载只考虑签名,不考虑指向
这里的SP和LP的动态类型都是SLList(因为都指向SLList)
接口继承 vs 实现继承
- 接口继承(Interface Inheritance):
- 接口继承是指一个类使用另一个类的接口(方法签名)而不继承其实现。
- 在接口继承中,子类仅继承父类的方法签名,但并不继承实际的实现代码。
- 接口继承的主要目的是为了定义一组共享的方法规范,以确保实现这些接口的类都有相似的行为。
- 多个类可以实现同一个接口,从而达到代码的可复用性和灵活性。
- 实现继承(Implementation Inheritance):
- 实现继承是指一个类从另一个类直接继承实现代码,包括属性和方法。
- 在实现继承中,子类不仅继承了父类的方法签名,还继承了具体的方法实现。
- 实现继承用于在子类中重用和扩展父类的功能,但可能导致较强的耦合性和继承链的脆弱性。
总的来说,区分接口继承和实现继承的关键在于是否继承了具体的实现代码。接口继承注重于定义规范,而实现继承注重于代码的重用和共享。在面向对象设计中,通常倡导使用接口继承来实现松耦合的设计,避免过度依赖具体的实现。
创建这些层次结构时,请记住,子类和超类之间的关系应为“is-a”关系。AKA Cat 应该只实现 Animal Cat is an Animal。您不应该使用“has-a”关系来定义它们。Cat 有爪子,但 Cat 绝对不应该实现 Claw。
最后,实现继承听起来不错,但也有一些缺点:
- 我们是容易犯错的人,我们无法跟踪所有事情,所以你有可能推翻了一种方法,但忘记了你做了。
- I如果两个接口提供冲突的默认方法,则可能很难解决冲突。
- 它鼓励过于复杂的代码