Skip to content

Files

Latest commit

8814ef5 · Oct 11, 2021

History

History
371 lines (265 loc) · 22 KB

File metadata and controls

371 lines (265 loc) · 22 KB

七、更加面向对象的 Java

在本章中,我们将通过创建一个超类和子类,并使用覆盖、数据结构、抽象方法和受保护方法等概念来理解它们之间的“is-a”关系,从而探索 Java 中的继承主题。我们还将深入研究抽象类的概念。

我们将详细介绍以下概念:

  • 遗产
  • 摘要

遗产

与其从一个高层次的描述开始,我认为最好是我们直接进入一个问题。

为了让我们开始,我创建了一个基本的 Java 程序,我们可以从给定的代码文件访问它。在这个程序中,我们声明了两个 Java 类:一个Book类和一个Poem类。BookPoem类都存储了许多属性;例如,书可以有标题、作者、出版商和流派。它将所有这些属性作为构造函数输入,并提供一个public方法;我们可以在主程序中使用Print方法打印出我们所创建的任何书籍的信息。

诗的方法做了一些非常相似的事情。它有两个属性和一个Print方法,我们通过它的构造函数设置它的属性。我已经创建了一个非常快速的主函数来利用BookPoem类。此函数创建一本新书和一首诗,然后将其打印出来:

package inheritance;
public class Inheritance {
    public static void main(String[] args) {
        Book a = new Book(
                "The Lord Of The Rings", 
                "J.R.R. Tolkein",
                "George Allen and Unwin", 
                "Fantasy");
        Poem b = new Poem(
                "The Iliad",
                "Homer",
                "Dactylic Hexameter");

        a.Print();
        b.Print();
    }
}

前面的程序工作得很好,但它比需要的复杂得多。

如果我们并排看一下我们的BookPoem类,只看它们的成员变量,我们会发现BookPoem都有两个成员变量,即titleauthor

它们对成员变量执行的操作,即将它们打印到屏幕上,在这两个类中以非常相似的方式执行和实现:

BookPoem继承自一个公共类,这是一个好迹象。当我们将书籍和诗歌视为它们所代表的实物时,我们就很容易看到这一点。我们可以说,书籍和诗歌都是文学形式。

创建超类

一旦我们得出结论,书籍和诗歌具有某些基本属性,即所有文学的属性,我们就可以开始将这些类别分解为组成部分。例如,我们的Book类有两个实变量。它有一个title变量和一个author变量,这是我们与所有文献相关联的属性。它也有一个 T3 的变量和一个 T4 的变量,它可能不是唯一的书,我们不一定认为所有形式的文学都有。那么我们如何利用这些知识呢?好吧,我们可以建立我们的BookPoem课程,让他们在基本层面上分享文学作品的本质。但要做到这一点,我们首先要教我们的节目什么是文学作品。以下是执行此操作的分步程序:

  1. 我们将创建一个全新的类,并将其命名为Literature
  2. 我们将为这个类指定文献片段共享的属性,到目前为止我们已经声明了这些属性。在我们的例子中,书籍和诗歌已经被宣布为作品,有共同的标题和作者。所有的文学作品都有一个标题和一个作者,这样的说法在逻辑上是合理的:
package inheritance;
public class Literature {
    protected String title;
    protected String author;
  1. 从这里开始,我们将像其他人一样充实我们的Literature课程。我们将给它一个构造函数;在这种情况下,我们的构造函数将接受两个变量:titleauthor。然后,我们将它们分配给字段,就像我们在PoemBook类中所做的那样:
package inheritance;
public class Literature {
  protected String title;
  protected String author;

  public Literature(String title, String author)
  {
     this.title = title;
     this.author = author;
   }
  1. 当我们开始时,让我们给Literature一个类似的Print方法,就像我们分配给BookPoem类的方法一样:
public void Print()
{
   System.out.println(title);
   System.out.println("\tWritten By: " + author);
 }

现在,如果我们愿意,我们可以转到main方法并声明Literature类的对象,但这不是重点。这不是我们创建Literature类的原因。相反,我们的目标是利用这个Literature类作为基础,我们将在此基础上声明更具体的文学类型,如诗歌或书籍。为了利用我们的Literature类,让我们看看它如何应用于现有的Poem类。

这是一种关系

我们的Literature类包含管理一篇文献的标题和作者所需的声明和所有功能。如果我们让 Java 知道PoemLiterature之间存在继承关系,我们应该能够从以下Poem类的标题和作者中删除所有真实引用:

package inheritance;
public class Poem extends Literature{
    private String title;
    private String author;
    private String style;

首先,让我们谈谈我们修改过的Poem类的声明。当我们说一个类扩展另一个类时,我们是说它们之间存在一种 is-a 关系,这样我就可以逻辑地说,“一首诗就是一篇文学作品。”用更多的 Java 术语来说,Poem子类扩展或继承了Literature类。这意味着当我们创建一个Poem对象时,它将拥有它扩展的类的所有成员和功能:

package inheritance;
public class Poem extends Literature {
    private String style;

    public Poem(String title, String author, String style)

在我们的例子中,其中两个成员是titleauthorLiterature类声明这些成员,并在整个类的功能中很好地管理它们。因此,我们可以从Poem类中删除这些成员,我们仍然可以在Poem类的方法中访问它们。这是因为Poem类只是从Literature继承了它的声明。不过,我们需要做一些轻微的修改。当我们构造从另一个类继承的类的对象时,默认情况下,子类的构造函数(如前面的屏幕截图所示)将首先调用超类的构造函数:

package inheritance;
public class Literature {
    protected String title;
    protected String author;

    public Literature(String title, String author)
    {
         this.title = title;
         this.author = author;
    }

这让 Java 感到困惑,因为我们现在已经设置好了它,因为Poem构造函数需要三个变量作为输入,而Literature构造函数只需要两个变量。要解决此问题,请使用以下步骤显式调用Poem构造函数中的Literature构造函数:

  1. 当我们在一个子类中时,我们可以使用super关键字调用我们的超类的方法。在本例中,我们将通过调用super构造函数或Literature构造函数开始Poem构造函数,并向其传递我们希望它了解的属性:
public Poem(String title, String author, String style)
{
     super(title, author);
     this.style = style;
 }
  1. 我们可以在Print方法中做一些非常类似的事情,因为我们的Literature类,我们的超类,已经知道如何打印标题和作者。Poem类没有理由实现此功能:
 public void Print()
 {
      super.Print();
      System.out.println("\tIn The Style Of: " + style);
 }

如果我们通过调用super.Print开始Print方法,而不是前面屏幕截图中显示的原始显式打印行,那么我们将从Print方法获得相同的行为。现在,当 Poem 的Print方法运行时,它将首先调用超类的,即Literature.java类的Print方法。它将最终打印出Poem课程的风格,这不是所有文学作品都能共享的。

虽然我们的Poem构造函数和Literature构造函数有不同的名称,甚至有不同的输入样式,但PoemLiterature之间共享的两个Print方法完全相同。稍后我们将进一步讨论这个问题,但现在您应该知道,我们在这里使用了一种称为覆盖的技术。

最重要的

当我们声明一个子类有一个与它的一个超类方法相同的方法时,我们已经重写了超类方法。当我们这样做时,最好使用 JavaOverride指示器:

@Override public void Print()

这是对未来的程序员和我们的编译套件中一些更神秘的元素的一个指示,在前面的屏幕截图中给出了这个特定的方法,它隐藏了一个方法。当我们实际运行代码时,Java 会优先选择方法的最低版本或子类版本。

那么,让我们看看我们是否成功地宣布了我们的PoemLiterature关系。让我们回到Inheritence.java类中程序的main方法,看看这个程序的诗部分是否像以前一样执行:

当我们运行这个程序时,我们得到了与之前完全相同的输出,这是一个很好的迹象,表明我们已经将Poem类设置为以逻辑方式从Literature继承。

现在我们可以跳到我们的Book课了。我们将使用以下步骤将其设置为BookLiterature类之间的 is-a 关系:

  1. 首先,我们将声明Book扩展了Literature类;然后,我们将在Book类中删除对 title 和 author 的引用,因为Literature类,即超类,将处理以下内容:
        package inheritance;
        public class Book extends Literature{
        private String publisher;
        private String genre;
  1. Poem类一样,我们需要显式调用Literature类的构造函数并将titleauthor传递给它:
        public Book(String title, String author, String publisher, String
        genre)
        {
             super(title, author);
             this.publisher = publisher;
             this.genre = genre;
         }
  1. 然后,我们可以利用超类的Print方法简化Book类的打印:
        @Override public void Print()
        {
             super.Print();
             System.out.println("\tPublished By: " + publisher);
             System.out.println("\tIs A: " + genre);
  1. Once again, let's jump back to our main method and run it to make sure that we've done this successfully:

我们开始:The Lord of the Rings输出,就像我们以前看到的一样。就风格而言,这种变化真的很棒。通过添加Literature类,然后将其子类化以创建BookPoem类,我们已经使BookPoem类变得更容易理解,程序员也更容易理解发生了什么。

然而,这种变化并不是纯粹的风格上的。通过声明BookPoemLiterature类之间的 is-a 关系,其中BookPoem继承自Literature类,我们为自己提供了以前没有的实际功能。让我们来看看这个动作。如果我们回到我们的main方法,让我们假设,我们处理的不是单个BookPoem类,而是需要存储在某种数据结构中的庞大网络。对于我们最初的实现,这将是一个真正的挑战。

数据结构

没有一种易于访问的数据结构可以同时存储书籍和诗歌。我们可能必须使用两种数据结构或打破强类型,这是 Java 的全部要点:

Book[] books = new Book[5];

然而,在我们的新实现中,BookPoem都继承自Literature,我们可以将它们存储在相同的数据结构中。这是因为继承是一种“是”与“是”的关系,这意味着一旦我们继承了某样东西,我们就可以提出诸如书是文学,诗也是文学之类的主张。如果这是真的,那么Literature对象数组应该能够在其中存储BookPoem。让我们通过以下步骤来说明这一点:

  1. 创建一个Literature对象数组:
 Literature[] lits = new Literature[5];
 lits[0] = a;
 lits[1] = b;

我们在构建这个项目时没有遇到编译错误这一事实是一个很好的迹象,表明我们正在做一些合法的事情。

  1. 为了便于演示,让我们在这里充实我们的数组,以包含书籍和诗歌的数量:
 Literature[] lits = new Literature[5];
 lits[0] = a;
 lits[1] = b;
 lits[2] = a;
 lits[3] = b;
 lits[4] = a;

我们将修改我们的main方法,直接从数组中打印出来。现在,当我们使用我们的子类时,就好像它们是它们的超类的对象一样,我们必须意识到我们现在引用它们作为那个超类的对象。例如,当我们通过Literature数组获取一个元素时,无论该元素是否为Book类,我们仍然无法访问其genre字段等内容,即使该字段为public

 Literature[] lits = new Literature[5];
 lits[0] = a;
 lits[1] = b;
 lits[2] = a;
 lits[3] = b;
 lits[4] = a;
 for(int i=0; i< lits.length; i++)
 {
      lits[i].Print(); 
 }

这是因为我们现在用作对象的Literature类(如前面的屏幕截图所示)没有genre成员变量。但是我们可以做的是从我们的超类调用被子类覆盖的方法。

  1. 我们可以在for循环中调用Literature类的Print方法。Java 将优先考虑我们子类的Print方法:
for(int i=0; i< lits.length; i++)
{
     lits[i].Print(); 
 }

这意味着,当我们运行这个程序时,我们仍然会得到我们归因于BookPoem的特殊格式的输出,而不是我们存储在Literature类中的简化版本:

public void Print()
{
     System.out.println(title);
     System.out.println("\tWritten By: " + author);
 }

抽象方法

我们有时可能会看到存在的方法只会被子类重载。这些方法什么都不做,我们可以在超类(Literature.java中用abstract关键字标记它们,即public abstract void Print()。当然,如果一个类声明了方法abstract,这可能是一个很好的迹象,表明永远不应该显式创建这样一个类的实例。如果我们的Literature类的Print方法是抽象的,那么我们永远不应该声明只属于文献的对象。我们应该只使用属于Literature子类之一的对象。如果我们要沿着这条路线走,我们也应该宣布Literatureabstract类:

package inheritance;
public abstract class Literature {

如果我们这样做了,当然,我们必须去掉对文学类超级方法的引用,所以现在让我们撤销这些更改。

让我们先来看看这个程序最初所犯的一个小错误。在创建文学类时,我们声明标题和作者是public成员变量。您可能知道,如果没有充分的理由,通常我们不会将成员变量声明为公共。对于一篇文献来说,在它已经被声明之后改变它的作者是没有多大意义的,所以authortitle应该是Literature类的构造函数中设置的private成员变量,其值永远不应该改变。不幸的是,如果我们对文学课做了这样的改变,我们将限制诗歌和书籍课的功能。

比如说,我们想修改Poem类的Print函数,这样它就不必显式调用Literature类的Print函数:

@Override public void Print()
{
     System.out.println(title);
     System.out.println("\tWritten By: " + author);
     System.out.println("\tIn The Style Of: " + style);
 }

也许我们想先告诉它我们在这里声明一个Poem类:

System.out.println("POEM: " + title);

不幸的是,由于我们将titleauthor设置为Literature类的私有,Poem类即使是Literature的子类,也无法在显式代码中访问这些成员变量。这有点烦人,似乎在privatepublic之间有某种保护设置,除了类的子类之外,每个类都是私有的。事实上,有一个可用的保护设置。

保护方法

protected方法为保护设置。如果我们声明成员变量为protected,则意味着它们是私有的,除类及其子类外,任何人都无法访问:

package inheritance;
public class Literature {
    protected String title;
    protected String author;

让我们自己放心,我们在这里所做的一切都是合法的。让我们再次运行我们的程序,并确保输出看起来良好,情况就是这样。在这之后,我们应该对继承有一个相当好的把握。我们可以开发许多真正模仿真实世界的系统,我们可以使用继承和小型类编写非常优雅的功能性代码,这些类本身不会做很多复杂的事情。

摘要

在本节中,我们将快速了解一个与 Java 中的继承相关的重要思想。为了让我们了解我们将要讨论的内容,我认为最好是在系统中启动一个现有的项目。让我们来看一下代码文件中的代码。

到目前为止,我们已经完成了以下工作:

  • 我们程序的main方法创建一个对象列表。这些对象属于BookPoem类型,但我们将它们放在Literature对象列表中,这让我们相信BookPoem类必须继承或扩展Literature类。
  • 一旦我们构建了这个数组,我们只需使用for循环遍历它,并在每个对象上调用for循环的Print方法。
  • 在这一点上,我们将对象视为Literature对象,而不是它们处于最低层次的书籍或诗歌。这让我们相信Literature类本身必须实现Print方法;如果我们跳进课堂,我们会发现这是真的。

然而,如果我们运行我们的程序,我们将很快看到书籍和诗歌执行它们的Print方法略有不同,为它们各自显示不同的信息。当我们看BookPoem类时,我们可以解释这一点,它们确实扩展了Literature类,但是这些类中的每一个都覆盖Literature类的Print方法来提供自己的功能。这一切都很好,是一个非常优雅的解决方案,但有一个有趣的案例,我们应该看看并讨论。因为Literature本身就是一个类,所以我们可以声明一个新的Literature对象,就像我们可以声明BookPoem一样。Literature类的构造函数首先期望文献的title,然后是author。一旦我们创建了Literature类的新实例,我们就可以将该实例放入Literature类列表中,就像我们对BookPoem类的实例所做的那样:

Literature l= new Literature("Java", "Zach");
Literature[] lits = new Literature[5];
lits[0] = a;
lits[1] = b;
lits[2] = l;
lits[3] = b;
lits[4] = a;
for(int i=0; i< lits.length; i++)
{
     lits[i].Print(); 
 }

当我们这样做并运行程序时,我们将看到Literature类的Print方法被执行,我们创建的新Literature对象将显示在我们的书籍和诗歌列表旁边:

那么这里有什么问题?好吧,这取决于我们试图设计的软件的真实性质,这可能有意义,也可能没有意义。假设我们是作为图书馆系统的一部分来做这件事的,只向某人提供一个叫做 Java 的东西是由一个叫 Zach 的人写的,而不告诉他们这是一本书还是一首诗,或者我们决定与特定类型的文学相联系的任何其他信息。这可能根本没有用,而且是永远不应该做的事情。

如果是这样的话,Java 为我们提供了一个创建类的系统,我们可以将这些类用于继承目的,但我们永远无法像以前那样合法地自己实例化它们。如果我们想将一个类标记为该类型,我们将其称为abstract类,在该类的声明中,我们只需使用abstract关键字:

public abstract class Literature {

一旦我们标记了一个类abstract,实例化这个类就不再是合法的操作。从表面上看,这是一件非常简单的事情,主要是“保护我们的代码不受我们自己和其他程序员的影响”,但这并不完全正确;相反,这是正确的,但它不是将类声明为abstract的唯一目的。

一旦我们告诉 Java,我们永远不能只创建Literature的实例,即使用Literature作为超类的类的实例,我们在设置Literature类时就不再受到限制。因为我们已经声明了Literature是一个抽象类,所以我们和 Java 都知道Literature永远不会单独被实例化,只有当它是被实例化的类的超类时。在这种情况下,我们可以不使用这个类中大多数 Java 类必须拥有的部分。例如,我们实际上不需要为Literature声明构造函数。如果Literature是一个标准的 Java 类,Java 就不会同意这样做,因为如果我们尝试实例化Literature,它将不知道该做什么。没有可调用的构造函数。但由于Literature是抽象的,我们可以确信文学的子类将有自己的构造函数。当然,如果我们做了这样的更改,我们必须在我们的子类中去掉对Literature构造函数的引用,也就是说,从子类中删除super方法。因此,这一变化肯定有一个权衡。它需要我们的子类中有更多的代码,才能在Literature超类中有更少的代码。在这个特殊的例子中,这种折衷可能不值得,因为我们在复制BookPoem构造函数之间的代码,但是如果可以假设Literature子类的构造函数做了非常不同的事情,那么不声明公共基构造函数就很有意义了。

因此,简言之,当我们构建程序或更大的解决方案时,我们应该将这些类声明为abstract,这样我们的类对于架构目的来说非常有意义,但永远不应该单独创建。有时候,当某些常见的类功能(比如有构造函数)对这个类没有意义时,我们就会知道我们遇到了这样的类。

总结

在本章中,我们通过创建一个称为超类和子类的东西并在它们之间建立“is-a”关系,精确地使用继承的概念,了解了面向对象编程的一些复杂之处。我们还讨论了一些关键方面的用法,例如重写子类和超类、数据结构和protected方法。我们还详细了解了abstract方法的工作原理。

在下一章中,您将学习有用的 Java 类。