4.1 Intro and interfaces · Hug61B (gitbooks.io)

方法重载(method overloading)

1
2
public static String longest(SLList<String> list)
public static String longest(AList<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”动物

hierarchy

同样的层次结构也适用于 SLLists 和 ALists!SLList 和 AList 都是更一般的列表的下位词。

我们将在 Java 中形式化这种关系:如果 SLList 是 List61B 的下位词,那么 SLList 类是 List61B 类的子类,而 List61B 类是 SLList 类的超类。

图 4.1.1 :

subclass

在 Java 中,为了表达这个层次结构,我们需要做两件事

  • 第 1 步:为常规列表超义词定义一个类型——我们将选择名称 List61B。
  • 第 2 步:指定 SLList 和 AList 是该类型的下位词。

新的 List61B 是 Java 所说的**接口(interface)**。它本质上是一个指定列表必须能够执行的操作的协定,但它不为这些行为提供任何实现。

这是我们的 List61B 接口。至此,我们已经完成了建立关系层次结构的第一步:创建超义词。

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

现在,要完成步骤 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
2
3
4
@Override
public void addFirst(Item x) {
insert(x, 0);
}

需要注意的是,即使您不包含此标记,您仍然会覆盖该方法。所以从技术上讲,你不必包括它。但是,包含标记可以作为程序员的一种保护措施,提醒编译器您打算重写此方法。你问为什么这会有帮助?嗯,这有点像有一个校对员!编译器会告诉您过程中是否出现问题。

为什么我们要用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
2
3
4
public static void main(String[] args) {
List61B<String> someList = new SLList<String>();
someList.addFirst("elk");
}

当它运行时,将创建 SLList,并且其地址存储在 someList 变量中。然后将字符串“elk”插入到 addFirst 引用的 SLList 中。

实现继承

以前,我们有一个接口 List61B,它只有标识 List61B 应该做什么的方法标头。但是,现在我们将看到我们可以在 List61B 中编写已经填写了实现的方法。这些方法确定 List61B 的上位词应如何表现。

为此,必须在方法签名中包含 default 关键字

如果我们在 List61B 中定义此方法

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

那么所有实现 List61B 类的东西都可以使用该方法!

然而,这种方法对于SLList来说过于慢,因为他是基于链表实现的列表,这样子相当于运用了两次for循环

我们希望 SLList 的打印方式与其接口中指定的方式不同。为此,我们需要覆盖它。在 SLList 中,我们实现了这种方法;

1
2
3
4
5
6
@Override
public void print() {
for (Node p = sentinel.next; p != null; p = p.next) {
System.out.print(p.item + " ");
}
}

现在,每当我们在 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时)。
  • 等于所指向的对象的类型。

image-20240129204744765

image-20240129204833057

当 Java 运行被重写的方法时,它会在其动态类型中搜索适当的方法签名并运行它。

重要提示:这不适用于重载方法!

假设同一类中有两个方法

1
2
3
4
5
6
public static void peek(List61B<String> list) {
System.out.println(list.getLast());
}
public static void peek(SLList<String> list) {
System.out.println(list.getFirst());
}

然后运行此代码

1
2
3
4
5
6
7
SLList<String> SP = new SLList<String>();
List61B<String> LP = SP;
SP.addLast("elk");
SP.addLast("are");
SP.addLast("cool");
peek(SP);
peek(LP);

对 peek() 的第一次调用将使用第二个 peek 方法,该方法接受 SLList。对 peek() 的第二次调用将使用第一个 peek 方法,该方法接受 List61B。这是因为两个重载方法之间的唯一区别是参数的类型。当 Java 检查要调用的方法时,它会检查静态类型并使用相同类型的参数调用该方法。

动态方法不适用于方法重载,方法重载只考虑签名,不考虑指向

这里的SP和LP的动态类型都是SLList(因为都指向SLList)

接口继承 vs 实现继承

  1. 接口继承(Interface Inheritance):
    • 接口继承是指一个类使用另一个类的接口(方法签名)而不继承其实现。
    • 在接口继承中,子类仅继承父类的方法签名,但并不继承实际的实现代码。
    • 接口继承的主要目的是为了定义一组共享的方法规范,以确保实现这些接口的类都有相似的行为。
    • 多个类可以实现同一个接口,从而达到代码的可复用性和灵活性。
  2. 实现继承(Implementation Inheritance):
    • 实现继承是指一个类从另一个类直接继承实现代码,包括属性和方法。
    • 在实现继承中,子类不仅继承了父类的方法签名,还继承了具体的方法实现。
    • 实现继承用于在子类中重用和扩展父类的功能,但可能导致较强的耦合性和继承链的脆弱性。

总的来说,区分接口继承和实现继承的关键在于是否继承了具体的实现代码。接口继承注重于定义规范,而实现继承注重于代码的重用和共享。在面向对象设计中,通常倡导使用接口继承来实现松耦合的设计,避免过度依赖具体的实现。

创建这些层次结构时,请记住,子类和超类之间的关系应为“is-a”关系。AKA Cat 应该只实现 Animal Cat is an Animal。您不应该使用“has-a”关系来定义它们。Cat 有爪子,但 Cat 绝对不应该实现 Claw。

最后,实现继承听起来不错,但也有一些缺点:

  • 我们是容易犯错的人,我们无法跟踪所有事情,所以你有可能推翻了一种方法,但忘记了你做了。
  • I如果两个接口提供冲突的默认方法,则可能很难解决冲突。
  • 它鼓励过于复杂的代码