幻灯片:cs61b 2021 lec10 inheritance3 - Google 幻灯片

Reading:4.3 Subtype Polymorphism vs. HOFs · Hug61B (gitbooks.io)

子类多态性

多态性的核心是“多种形式”。在 Java 中,多态性是指对象可以具有多种形式或类型。在面向对象编程中,多态性涉及如何将一个对象视为其自身类的实例、其超类的实例、其超类的超类的实例等。

考虑一个静态类型为deque的变量deque。调用dequeue .addFirst() 将在执行时确定,这取决于调用 addFirstdeque的运行时类型或动态类型。正如我们在上一章中看到的,Java使用动态方法选择来选择调用哪个方法。

  1. 运行时类型(Runtime Type): 运行时类型指的是在程序执行过程中,某个变量或对象的实际类型。这是由程序在运行时动态决定的。在一些面向对象的语言中,对象可能被声明为某个类型,但在运行时可能会被赋予该类型的子类型。因此,运行时类型是程序实际处理的对象类型,而不仅仅是在代码中声明的类型。如基类。
  2. 动态类型(Dynamic Type): 动态类型是指变量或对象在运行时的类型。与静态类型相对应,动态类型意味着类型信息是在运行时确定的,而不是在编译时。在一些动态语言中,变量可以在运行时引用不同的类型,因此变量的动态类型是可以变化的。

假设我们想编写一个 python 程序,该程序打印两个对象中较大的一个的字符串表示形式。有两种方法可以做到这一点。

  1. 显式 HoF 方法
1
2
3
4
def print_larger(x, y, compare, stringify):
if compare(x, y):
return stringify(x)
return stringify(y)
  1. 子类多态性方法
1
2
3
4
def print_larger(x, y):
if x.largerThan(y):
return x.str()
return y.str()

使用显式高阶函数方法,您有一种常用的方法来打印出两个对象中较大的一个。相反,在子类多态性方法中,对象本身做出选择。所调用 largerFunction 的取决于 x 和 y 的实际含义。

Max 函数

假设我们想编写一个 max 函数,该函数接受任何数组(无论类型如何)并返回数组中的最大项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Object max(Object[] items) {
int maxDex = 0;
for (int i = 0; i < items.length; i += 1) {
if (items[i] > items[maxDex]) {
maxDex = i;
}
}
return items[maxDex];
}

public static void main(String[] args) {
Dog[] dogs = {new Dog("Elyse", 3), new Dog("Sture", 9), new Dog("Benjamin", 15)};
Dog maxDog = (Dog) max(dogs);
maxDog.bark();
}

这里有一处很明显的错误:items[i] > items[maxDex],这会导致编译错误的原因是,此行假定 > 运算符使用任意 Object 类型,而实际上并非如此。

相反,我们可以在 Dog 类中定义一个函数,并放弃编写一个 maxDog 可以接受任意类型的数组的“一个真正的最大函数”。我们可以这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
public static Dog maxDog(Dog[] dogs) {
if (dogs == null || dogs.length == 0) {
return null;
}
Dog maxDog = dogs[0];
for (Dog d : dogs) {
if (d.size > maxDog.size) {
maxDog = d;
}
}
return maxDog;
}

虽然这在目前是可行的,但如果我们放弃制作广义 max 函数的梦想,让 Dog 类定义自己的 max 函数,那么我们将不得不对稍后定义的任何类做同样的事情。我们需要编写一个函数、一个函数、一个 maxCat maxPenguin maxWhale 函数等,导致不必要的重复工作和大量冗余代码。

导致这种情况的根本问题是 Objects 不能与 > 进行比较。这是有道理的,因为 Java 怎么知道它是否应该使用对象的 String 表示、大小或其他度量来进行比较?在 Python 或 C++ 中, > 运算符的工作方式可以重新定义,以便在应用于不同类型时以不同的方式工作。不幸的是,Java 没有这个功能。相反,我们求助于接口继承来帮助我们。

我们可以创建一个接口来保证任何实现类(如 Dog)都包含一个比较方法,我们称之为 compareTo

img

让我们编写我们的接口。我们指定一种方法compareTo

1
2
3
public interface OurComparable {
public int compareTo(Object o);
}

我们将这样定义它的行为:

  • 如果 this < o,则返回负数。
  • 如果 this 等于 o,则返回 0。
  • 如果 this > o,则返回正数。

现在我们已经创建了 OurComparable 接口,我们可以要求 Dog 类实现该 compareTo 方法。首先,我们将 Dog 的类头更改为 implements OurComparable ,然后根据上面定义的行为编写 compareTo 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Dog implements OurComparable {
private String name;
private int size;

public Dog(String n, int s) {
name = n;
size = s;
}

public void bark() {
System.out.println(name + " says: bark");
}

public int compareTo(Object o) {
Dog uddaDog = (Dog) o;
return this.size - uddaDog.size;
}
}

请注意,由于 compareTo 接受任何任意对象 o,因此我们必须将输入转换为 Dog 才能使用 size 实例变量进行比较。

现在,我们可以将我们在练习 4.3.1 中定义的 max 函数推广为,而不是接受任何任意的对象数组,而是接受 OurComparable 对象 - 我们肯定知道所有对象都实现了该 compareTo 方法。

1
2
3
4
5
6
7
8
9
10
public static OurComparable max(OurComparable[] items) {
int maxDex = 0;
for (int i = 0; i < items.length; i += 1) {
int cmp = items[i].compareTo(items[maxDex]);
if (cmp > 0) {
maxDex = i;
}
}
return items[maxDex];
}

现在,我们的函数可以接受任何 OurComparable 类型对象的数组, max 并返回数组中的最大对象。

使用继承,我们能够推广我们的最大化函数。这种方法有什么好处?

  • 不需要每个类中的求最大代码(即不需要 Dog.maxDog(Dog[]) 函数
  • 我们有代码可以优雅地对多种类型(大多数)进行操作

Comparables

我们刚刚构建的 OurComparable 接口可以工作,但并不完美。以下是它的一些问题:

  • 对象间尴尬的类型转换
  • 我们编写了它。
    • 没有现有类继承 OurComparable (例如 String 等)
    • 没有现有类使用 OurComparable(例如,没有使用 OurCompare 的内置 max 函数)

解决方案是什么?我们将利用一个名为 Comparable . Comparable 已经由 Java 定义,并被无数库使用。

Comparable 看起来与我们制作的 OurComparable 接口非常相似,但有一个主要区别。你能发现它吗?

img

请注意, Comparable<T> 这意味着它采用泛型类型。这将帮助我们避免将对象转换为特定类型!现在,我们将重写 Dog 类以实现 Comparable 接口,确保将泛型类型 T 更新为 Dog:

1
2
3
4
5
6
public class Dog implements Comparable<Dog> {
...
public int compareTo(Dog uddaDog) {
return this.size - uddaDog.size;
}
}

现在剩下的就是将 Maximizer 类中的每个 OurComparable 实例更改为 Comparable。看着最大的狗说吠叫:

我们现在使用真正的内置接口,而不是使用我们个人创建的接口 OurComparableComparable 因此,我们可以利用所有已经存在的库并使用 Comparable .

img

Comparator

我们刚刚了解了可比较的接口,它为每只狗嵌入了将自己与另一只狗进行比较的能力。现在,我们将介绍一个看起来非常相似的新接口,称为 Comparator .

让我们从定义一些术语开始。

  • 自然顺序 - 用于指特定类 compareTo 的方法中隐含的顺序。

例如,如前所述,狗的自然排序是根据大小的值定义的。如果我们想以不同于狗的自然顺序的方式对狗进行排序,例如按它们名字的字母顺序排序,该怎么办?

Java 实现这一点的方式是使用 Comparator。由于 Comparator 是一个对象,我们将使用 Comparator 的方法是通过在 Dog 类内部编写一个实现 Comparator 接口的嵌套类。

但首先,这个接口里面有什么?

1
2
3
public interface Comparator<T> {
int compare(T o1, T o2);
}

这表明 Comparator 接口要求任何实现类实现该 compare 方法。的规则 compare 就像: compareTo

  • 如果 o1 < o2,则返回负数。
  • 如果 o1 等于 o2,则返回 0。
  • 如果 o1 > o2,则返回正数。

让我们给 Dog 一个 NameComparator。为此,我们可以简单地遵循 String 已经定义 compareTo 的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Comparator;

public class Dog implements Comparable<Dog> {
...
public int compareTo(Dog uddaDog) {
return this.size - uddaDog.size;
}

private static class NameComparator implements Comparator<Dog> {
public int compare(Dog a, Dog b) {
return a.name.compareTo(b.name);
}
}

public static Comparator<Dog> getNameComparator() {
return new NameComparator();
}
}

请注意,我们已将 NameComparator 声明为静态类。这是一个细微的差异,但我们这样做是因为我们不需要实例化 Dog 来获取 NameComparator。让我们看看这个比较器是如何工作的。

正如你所看到的,我们可以像这样检索我们的 NameComparator:

1
Comparator<Dog> nc = Dog.getNameComparator();

总而言之,我们有一个 Dog 类,它有一个私有的 NameComparator 类和一个返回 NameComparator 的方法,我们可以用它来按名称的字母顺序比较狗。

让我们看看继承层次结构中的一切是如何工作的——我们有一个内置于 Java 的比较器接口,我们可以实现它来在 Dog 中定义我们自己的比较器( NameComparatorSizeComparator 等)。

img

总而言之,Java 中的接口为我们提供了进行回调的能力。有时,一个函数需要另一个可能尚未编写的函数的帮助(例如 max needs compareTo )。回调函数是帮助函数(在方案中为 compareTo )。在某些语言中,这是使用显式函数传递来实现的;在 Java 中,我们将所需的函数包装在一个接口中。

一个可比者说,“我想将自己与另一个对象进行比较”。它嵌入在对象本身中,它定义了类型的自然顺序。另一方面,比较器更像是将两个对象相互比较的第三方机器。由于只有一种方法 compareTo 的空间,如果我们想要多种方法进行比较,我们必须求助于比较器。

a.max(b)和max(a, b)的区别

总结

接口为我们提供了进行回调的能力:

  • 有时候,一个函数需要另一个可能尚未编写的函数的帮助。 例如:max 需要 compareTo。 这种帮助函数有时被称为“回调”。
  • 一些编程语言使用显式的函数传递来处理这种情况。
  • 在Java中,我们通过将所需的函数包装在接口中来实现这一点(例如,Arrays.sort 需要在比较器接口中定义的 compare)。
  • Arrays.sort 在需要进行比较时会“回调”相应的函数。 类似于在需要信息时将你的号码提供给别人。

概念

  1. Comparables(可比较的):
    • 可比较的对象通常实现Comparable接口,该接口定义了compareTo方法,规定了对象的自然顺序。
    • 自然顺序是对象的默认排序方式。
    • 在子类中实现compareTo方法,以便对象可以在不同场景中进行比较,例如在排序中使用。
  2. Comparators(比较器):
    • 比较器是实现了Comparator接口的对象,该接口定义了compare方法,允许两个对象进行自定义的比较。
    • 比较器允许在不修改对象类本身的情况下定义多种比较方式。
    • 在需要多种比较方式时,可以创建不同的比较器类,例如NameComparatorSizeComparator
  3. 子类多态性:
    • 多态性涉及对象在程序执行过程中可以具有多种形式或类型的概念。
    • 在Java中,多态性表现为对象可以被视为其自身类的实例、其超类的实例等。
    • 子类多态性通过继承和接口实现,使得代码更加灵活,允许处理不同类型的对象。
  4. 接口和回调函数:
    • 接口提供了进行回调的能力,允许一个函数在需要时调用另一个可能尚未编写的函数。
    • 在Java中,回调函数通常通过将函数包装在接口中实现,例如compareTo方法在OurComparable接口中定义。
    • 使用接口和比较器,可以实现多种比较方式,使得对象在排序等场景中更加灵活。

关系

  1. Comparables 和 Comparators:
    • ComparablesComparators 都是用于进行对象比较的机制,但它们的使用场景和实现方式略有不同。
    • Comparables 用于定义对象的自然顺序,要求对象自身实现 Comparable 接口,并重写 compareTo 方法来规定比较规则。
    • Comparators 用于实现自定义的比较逻辑,可以为同一类对象定义多个不同的比较方式,而无需修改对象类本身。比较器实现 Comparator 接口,并重写 compare 方法。
  2. 子类多态性和接口的关系:
    • 子类多态性涉及对象在程序执行过程中可以具有多种形式或类型的概念。在文章中,子类多态性通过继承和接口实现,使得对象可以具有不同的形式。
    • 接口在这里起到了关键作用。例如,OurComparable 接口定义了 compareTo 方法,通过让类实现这个接口,可以在不同类型的类之间实现相似的比较行为,实现多态性。
  3. 使用 Comparables 和 Comparators 实现灵活性:
    • 通过使用 Comparables 接口,对象可以定义它们的自然顺序,使得它们可以参与排序等操作。
    • 使用 Comparators 接口,可以实现多种比较方式,而不必修改对象的类。这提供了灵活性,允许在不同的上下文中使用不同的比较规则。

总体而言,Comparables 和 Comparators 提供了不同层次的比较机制,使得代码更加灵活,同时子类多态性通过接口的实现使得对象可以以多种形式存在。这些概念相互协作,为Java中的对象比较和多态性提供了强大的工具。

对抽象的意义

  1. 抽象接口提高通用性:
    • 接口(比如 ComparableComparator)提供了抽象的规范,定义了在不同类之间进行比较的通用方式。
    • 这种抽象性使得可以编写通用的算法和方法,而无需关心具体类的实现细节,从而提高了代码的通用性。
  2. 多态性提高扩展性:
    • 使用 ComparablesComparators 的多态性,代码可以处理不同类型的对象,并根据实际情况选择适当的比较方式。
    • 新的类可以轻松地实现相应的接口或比较器,而不需要修改现有的代码,从而提高了代码的扩展性。
  3. 减少耦合度:
    • 通过接口和多态性,实现了对象之间的松散耦合。例如,一个算法可能仅关心对象是否可比较,而不关心对象的具体类型。
    • 这种减少耦合度的设计使得代码更加灵活,减少了对具体实现的依赖,提高了系统的可维护性。
  4. 支持算法的通用性:
    • 通过接口和抽象的比较机制,可以编写通用的排序、搜索等算法,这些算法可以在不同类型的对象上工作,只要它们实现了相应的接口或比较器。
    • 这种通用性使得可以更容易地共享和重用算法,同时减少了代码冗余。
  5. 更好的代码组织和可读性:
    • 使用抽象接口和多态性,可以更清晰地组织代码,将通用的比较逻辑与具体类的实现分离。
    • 这提高了代码的可读性,使得代码更易于理解和维护。

总体而言,通过在面向对象的设计中引入抽象接口和多态性,可以使代码更加灵活、通用和可扩展。这种实现对于处理不同类型的对象、支持通用算法以及减少代码耦合度都具有重要的意义。

思维流程

  1. 我们希望编写一个 max 函数,该函数接受任何数组(无论类型如何)并返回数组中的最大项。因为目前我们只有Dog类,所以我们在Dog类中编写了一个MaxDog的方法。

  2. 虽然这样子可以实现我们的目的,但是有个问题,当我们有其他类想要比较时,这个Max函数没有办法比较,导致这种情况的根本问题是 Objects 不能与 > 进行比较。于是我们想创建一个接口来保证任何实现类(如 Dog)都包含一个比较方法,我们称之为 compareTo

  3. 于是我们创建了一个OurComparable接口,里面规定了compareTo方法。我们在Dog类中实现后,把max方法修改成了这样

  4. image-20240130034614544

  5. 但是这样子又有些问题:没有现有类继承 OurComparable, 没有现有类使用 OurComparable。解决方案是什么?我们将利用一个名为 Comparable . Comparable 已经由 Java 定义,并被无数库使用。

  6. 如前所述,狗的自然排序是根据大小的值定义的。如果我们想以不同于狗的自然顺序的方式对狗进行排序,例如按它们名字的字母顺序排序,该怎么办?

  7. Java 实现这一点的方式是使用 Comparator。由于 Comparator 是一个对象,我们将使用 Comparator 的方法是通过在 Dog 类内部编写一个实现 Comparator 接口的嵌套类。

  8. import java.util.Comparator;
    
    public class Dog implements Comparable<Dog> {
        ...
        public int compareTo(Dog uddaDog) {
            return this.size - uddaDog.size;
        }
    
        private static class NameComparator implements Comparator<Dog> {
            public int compare(Dog a, Dog b) {
                return a.name.compareTo(b.name);
            }
        }
    
        public static Comparator<Dog> getNameComparator() {
            return new NameComparator();
        }
    }
    
  9. 这样子就能实现自定义排序

  10. image-20240130032414722