精通 OOP:继承、接口和抽象类实用指南

已发表: 2022-03-10
快速总结↬可以说,教授编程基础最糟糕的方法是描述某物是什么,而不提及如何或何时使用它。 在本文中,Ryan M. Kay 用最明确的术语讨论了 OOP 中的三个核心概念,因此您可能再也不会想知道何时使用继承、接口或抽象类。 提供的代码示例使用 Java 编写,其中一些参考了 Android,但只需具备 Java 的基本知识即可。

据我所知,软件开发领域的教育内容提供了理论和实践信息的适当混合是不常见的。 如果我猜为什么,我认为这是因为专注于理论的人倾向于进入教学,而专注于实践信息的人倾向于使用特定的语言和工具解决特定问题而获得报酬。

当然,这是一个广泛的概括,但如果我们为了论证而简单地接受它,那么许多担任教师角色的人(绝不是所有人)往往要么不擅长,要么完全没有能力解释与特定概念相关的实践知识。

在本文中,我将尽力讨论在大多数面向对象编程 (OOP) 语言中会发现的三种核心机制:继承接口(又名协议)和抽象类。 与其给你关于每种机制什么的技术性和复杂的口头解释,我会尽力关注它们的作用以及何时使用它们。

但是,在单独讨论它们之前,我想简要讨论一下给出理论上合理但实际上无用的解释意味着什么。 我希望您可以使用这些信息来帮助您筛选不同的教育资源,并避免在事情没有意义时责备自己。

跳跃后更多! 继续往下看↓

不同程度的认识

知道名字

知道某物的名称可以说是最肤浅的认识形式。 事实上,一个名称通常只有在许多人常用来指代同一事物和/或有助于描述事物的情况下才有用。 不幸的是,正如任何在该领域花费时间的人所发现的那样,许多人为同一事物使用不同的名称(例如接口协议),为不同的事物使用相同的名称(例如模块组件),或者对荒谬的点(例如Either Monad )。 归根结底,名称只是心理模型的指针(或引用),它们可能具有不同程度的有用性。

为了让这个领域更难研究,我大胆猜测,对于大多数人来说,编写代码是(或至少是)一种非常独特的体验。 更复杂的是理解代码如何最终编译成机器语言,并在物理现实中表示为一系列随时间变化的电脉冲。 即使人们能回忆起程序中使用的过程、概念和机制的名称,也不能保证一个人为这些事情创建的心智模型与另一个人的模型是一致的。 更不用说它们是否客观准确。

正是由于这些原因,加上我天生对行话没有很好的记忆力,我认为名字是了解某事最不重要的方面。 这并不是说名字没用,但我过去在项目中学习和使用了许多设计模式,只是几个月甚至几年后才知道常用的名字。

了解语言定义和类比

语言定义是描述新概念的自然起点。 但是,与名称一样,它们可能具有不同程度的有用性和相关性; 这在很大程度上取决于学习者的最终目标是什么。 我在语言定义中看到的最常见的问题是假设知识通常以行话的形式出现。

例如,假设我要解释线程进程非常相似,只是线程占用给定进程的相同地址空间。 对于已经熟悉进程地址空间的人,我基本上已经说过线程可以与他们对进程的理解相关联(即它们具有许多相同的特征),但可以根据不同的特征来区分它们。

对于不具备这种知识的人来说,我充其量没有任何意义,最坏的情况是让学习者在某种程度上因为不知道我认为他们应该知道的事情而感到不足。 公平地说,如果您的学习者真的应该拥有这样的知识(例如教研究生或经验丰富的开发人员),这是可以接受的,但我认为在任何入门级材料中这样做是一个巨大的失败。

当一个概念不同于学习者以前见过的任何其他东西时,通常很难对一个概念进行良好的口头定义。 在这种情况下,对于教师来说,选择一个普通人可能熟悉的类比是非常重要的,并且只要它传达了该概念的许多相同品质,它也是相关的。

例如,对于软件开发人员来说,了解软件实体(程序的不同部分)紧耦合松耦合时的含义至关重要。 在建造花园棚时,初级木匠可能会认为使用钉子而不是螺钉将其组装起来更快更容易。 直到发生错误或花园棚设计的更改需要重建棚屋的一部分之前,情况都是如此。

在这一点上,使用钉子将花园棚的各个部分紧密连接在一起的决定使整个施工过程变得更加困难,可能更慢,并且用锤子提取钉子存在损坏结构的风险。 相反,螺钉可能需要一些额外的时间来组装,但它们很容易拆卸,并且几乎没有损坏棚屋附近部分的风险。 这就是我所说的松散耦合。 当然,在某些情况下你真的只需要一颗钉子,但这个决定应该以批判性思维和经验为指导。

正如我稍后将详细讨论的,将程序的各个部分连接在一起有不同的机制,它们提供了不同程度的耦合; 就像钉子螺丝一样。 虽然我的类比可能帮助你理解了这个至关重要的术语的含义,但我没有告诉你如何在建造花园棚的范围之外应用它。 这使我获得了最重要的知识,也是深入理解任何研究领域中模糊和困难概念的关键; 尽管我们将坚持在本文中编写代码。

了解代码

在我看来,严格来说,就软件开发而言,了解一个概念最重要的形式来自能够在工作应用程序代码中使用它。 只需编写大量代码并解决许多不同的问题即可获得这种形式的知识。 不需要包括行话名称和口头定义。

以我自己的经验,我记得通过一个接口解决了与远程数据库和本地数据库通信的问题(如果你还不知道,你很快就会知道这意味着什么); 而不是需要显式调用远程和本地(甚至是测试数据库)的客户端(与接口对话的任何类)。 事实上,客户端并不知道界面背后是什么,所以无论它是在生产应用程序中运行还是在测试环境中运行,我都不需要更改它。 在我解决这个问题大约一年后,我遇到了术语“外观模式”,并且在“存储库模式”这个术语之后不久,这两个名称都是人们用于前面描述的解决方案的名称。

所有这些前言都是希望阐明一些在解释诸如继承接口抽象类等主题时最常出现的缺陷。 在这三种方法中,继承可能是最容易使用和理解的一种。 根据我作为一名编程学生和一名教师的经验,除非特别注意避免前面讨论的错误,否则其他两个几乎总是对学习者来说是一个问题。 从这一点开始,我将尽我所能使这些主题尽可能简单,但不会更简单。

关于示例的注释

我本人最精通Android移动应用程序开发,我将使用来自该平台的示例,以便在介绍Java语言特性的同时教您构建GUI应用程序。 但是,我不会详细介绍这些示例,以使粗略了解 Java EE、Swing 或 JavaFX 的人无法理解这些示例。 我讨论这些主题的最终目标是帮助您理解它们在解决几乎任何类型的应用程序中的问题时的含义。

亲爱的读者,我还想警告你,有时我似乎对特定的词及其定义过于哲学和迂腐。 这样做的原因是,确实需要深刻的哲学基础来理解具体(真实)与抽象(不如真实事物详细)之间的区别。 这种理解适用于计算领域之外的许多事物,但对于任何软件开发人员来说,掌握抽象的本质都特别重要。 无论如何,如果我的话让你失望,代码中的示例希望不会。

继承与实现

在使用图形用户界面 (GUI) 构建应用程序时,继承可以说是使快速构建应用程序成为可能的最重要的机制。

尽管稍后将讨论使用继承的好处鲜为人知,但主要好处是在类之间共享实现。 至少就本文的目的而言,“实施”一词具有独特的含义。 用英语给这个词一个一般的定义,我可能会说实现某事,就是让它成为现实

为了给出一个特定于软件开发的技术定义,我可以说实现一个软件,就是编写满足该软件要求的具体代码行。 例如,假设我正在编写一个sum 方法private double sum(double first, double second){

 private double sum(double first, double second){ //TODO: implement }

上面的代码片段,即使我已经编写了返回类型( double )和指定参数( first, second )和可用于调用所述方法的名称( sum )的方法声明,它也有没有被执行。 为了实现它,我们必须像这样完成方法体

 private double sum(double first, double second){ return first + second; }

当然,第一个示例不会编译,但我们会立即看到接口是一种我们可以编写这些类型的未实现函数而不会出错的方式。

Java中的继承

据推测,如果您正在阅读这篇文章,那么您至少已经使用过extends Java 关键字一次。 这个关键字的机制很简单,最常使用示例来描述不同种类的动物或几何形状; DogCat扩展Animal ,依此类推。 我假设我不需要向您解释基本的类型理论,所以让我们通过extends关键字直接了解 Java 中继承的主要好处。

用 Java 构建一个基于控制台的“Hello World”应用程序非常简单。 假设您拥有 Java 编译器 ( javac ) 和运行时环境 ( jre ),您可以编写一个包含如下函数的类:

 public class JavaApp{ public static void main(String []args){ System.out.println("Hello World"); } }

在几乎所有主要平台(Android、企业/Web、桌面)上用 Java 构建 GUI 应用程序,在 IDE 的帮助下生成新应用程序的框架/样板代码,也相对容易,这要归功于extends关键字。

假设我们有一个名为activity_main.xml的 XML 布局(我们通常在 Android 中通过布局文件以声明方式构建用户界面),其中包含一个名为tvDisplayTextView (如文本标签):

 <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> </FrameLayout>

另外,假设我们希望tvDisplay说“Hello World!” 为此,我们只需要编写一个使用extends关键字从Activity类继承的类:

 import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((TextView)findViewById(R.id.tvDisplay)).setText("Hello World"); }

通过快速查看其源代码,可以最好地了解继承Activity类的实现的效果。 如果一个人需要实现与系统交互所需的 8000 多行代码中的一小部分,我非常怀疑 Android 是否会成为主导的移动平台,只是为了生成一个带有一些文本的简单窗口。 继承使我们不必从头开始重建Android 框架或您碰巧使用的任何平台。

继承可用于抽象

就它可以用于跨类共享实现而言,继承相对容易理解。 然而,还有另一种重要的方式可以使用继承,它在概念上与我们将很快讨论的接口抽象类相关。

如果您愿意,请在接下来的一小段时间里假设在最一般意义上使用的抽象是对事物的不太详细的表示。 与其用冗长的哲学定义来限定它,我将尝试指出抽象在日常生活中是如何工作的,然后不久就软件开发方面明确讨论它们。

假设您正在澳大利亚旅行,并且您知道您所访问的地区是内陆大班蛇密度特别高的地区(它们显然非常有毒)。 您决定咨询维基百科,通过查看图像和其他信息来了解更多关于它们的信息。 通过这样做,你现在敏锐地意识到了一种你以前从未见过的特定种类的蛇。

抽象、想法、模型或任何你想称呼它们的东西,都是对事物的不太详细的表示。 重要的是它们没有真实的东西那么详细,因为真正的蛇会咬你; 维基百科页面上的图像通常不会。 抽象也很重要,因为计算机和人脑存储、交流和处理信息的能力都是有限的。 有足够的细节以实用的方式使用这些信息,而不占用太多的内存空间,这使得计算机和人脑能够解决问题。

为了将其与继承联系起来,我在这里讨论的所有三个主要主题都可以用作抽象抽象机制。 假设在我们的“Hello World”应用程序的布局文件中,我们决定添加一个ImageViewButtonImageButton

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <Button android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageButton android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageView android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>

还假设我们的 Activity 已经实现View.OnClickListener来处理点击:

 public class MainActivity extends Activity implements View.OnClickListener { private Button b; private ImageButton ib; private ImageView iv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //... b = findViewById(R.id.imvDisplay).setOnClickListener(this); ib = findViewById(R.id.btnDisplay).setOnClickListener(this); iv = findViewById(R.id.imbDisplay).setOnClickListener(this); } @Override public void onClick(View view) { final int id = view.getId(); //handle click based on id... } }

这里的关键原则是ButtonImageButtonImageView继承自View类。 结果是这个函数onClick可以通过将不同的(尽管是分层相关的)UI 元素引用为它们不太详细的父类来接收点击事件。 这比必须编写一个独特的方法来处理Android 平台上的每种小部件(更不用说自定义小部件)要方便得多。

接口和抽象

即使您理解我选择它的原因,您也可能发现前面的代码示例有点乏味。 能够跨类的层次结构共享实现非常有用,我认为这是继承的主要用途。 至于允许我们将具有共同父类的一组类视为类型相同(即作为父类),继承这一特性的用途有限。

受限于,我说的是子类必须在同一个类层次结构中才能被引用,或者称为父类。 换句话说,继承是一种非常严格的抽象机制。 事实上,如果我假设抽象是在不同细节(或信息)级别之间移动的频谱,我可能会说继承是 Java 中抽象程度最低的抽象机制。

在继续讨论接口之前,我想提一下,从Java 8开始,接口中添加了两个称为默认方法静态方法的特性。 我最终会讨论它们,但目前我希望我们假装它们不存在。 这是为了让我更容易解释使用接口的主要目的,它最初是,并且可以说仍然是Java 中最抽象的抽象机制

更少的细节意味着更多的自由

在关于继承的部分中,我给出了实现这个词的定义,这是为了与我们现在将要讨论的另一个术语进行对比。 需要明确的是,我不在乎这些词本身,也不在乎你是否同意它们的用法; 只是您了解它们在概念上指向的内容。

继承主要是一种在一组类之间共享实现的工具,而我们可以说接口主要是一种在一组类之间共享行为的机制。 在这个意义上使用的行为实际上只是抽象方法的一个非技术词。 抽象方法一种实际上不包含方法体的方法:

 public interface OnClickListener { void onClick(View v); }

对我和我辅导过的一些人来说,在第一次看到一个接口之后,自然的反应是想知道只共享一个返回类型方法名称参数列表可能会有什么用处。 从表面上看,这似乎是一种为自己或其他人可能正在编写implements接口的类创建额外工作的好方法。 答案是,接口非常适合您希望一组类以相同方式运行的情况(即它们拥有相同的公共抽象方法),但您希望它们以不同的方式实现行为

举一个简单但相关的例子,A​​ndroid 平台有两个类,主要用于创建和管理部分用户界面的业务: ActivityFragment 。 因此,这些类通常需要侦听单击小部件(或以其他方式与用户交互)时弹出的事件。 为了论证的缘故,让我们花一点时间来理解为什么继承几乎永远不会解决这样的问题:

 public class OnClickManager { public void onClick(View view){ //Wait a minute... Activities and Fragments almost never //handle click events exactly the same way... } }

让我们的活动片段继承自OnClickManager会使我们无法以不同的方式处理事件,而且更糟糕的是,如果我们愿意,我们甚至无法做到这一点。 ActivityFragment都已经扩展了一个父类,Java 不允许有多个父类。 所以我们的问题是我们希望一组类以相同的方式表现,但我们必须在类如何实现行为方面具有灵活性。 这让我们回到前面的View.OnClickListener示例:

 public interface OnClickListener { void onClick(View v); }

这是实际的源代码(嵌套在View类中),这几行代码允许我们确保跨不同小部件(视图)和 UI 控制器(活动、片段等)的行为一致。

抽象促进松耦合

希望我已经回答了关于 Java 中为什么存在接口的一般性问题。 在许多其他语言中。 从一个角度来看,它们只是在类之间共享代码的一种方式,但是为了允许不同的实现,它们故意不那么详细。 但是正如继承可以用作共享代码和抽象的机制(尽管对类层次结构有限制),接口提供了一种更灵活的抽象机制。

在本文的前面部分,我通过类比使用钉子螺钉构建某种结构的区别来介绍松/紧耦合的主题。 回顾一下,基本思想是,在可能发生更改现有结构(可能是由于修复错误、设计更改等)的情况下,您将需要使用螺钉。 当您只需要将结构的一部分固定在一起并且并不特别担心在不久的将来将它们拆开时,可以使用钉子

钉子螺钉类似于类之间的具体抽象引用(术语依赖项也适用)。 为了避免混淆,以下示例将说明我的意思:

 class Client { private Validator validator; private INetworkAdapter networkAdapter; void sendNetworkRequest(String input){ if (validator.validateInput(input)) { try { networkAdapter.sendRequest(input); } catch (IOException e){ //handle exception } } } } class Validator { //...validation logic boolean validateInput(String input){ boolean isValid = true; //...change isValid to false based on validation logic return isValid; } } interface INetworkAdapter { //... void sendRequest(String input) throws IOException; }

在这里,我们有一个名为Client的类,它拥有两种引用。 请注意,假设Client与创建它的引用没有任何关系(它确实不应该),它与任何特定网络适配器的实现细节是分离的。

这种松散耦合有几个重要的含义。 对于初学者,我可以在绝对隔离INetworkAdapter的任何实现的情况下构建Client 。 想象一下,您在一个由两个开发人员组成的团队中工作; 一个构建前端,一个构建后端。 只要两个开发人员都知道将他们各自的类耦合在一起的接口,他们就可以几乎独立地继续工作。

其次,如果我要告诉您两个开发人员都可以验证他们各自的实现是否正常运行,并且彼此独立于彼此的进度,该怎么办? 使用接口很容易; 只需构建一个implements适当接口Test Double

 class FakeNetworkAdapter implements INetworkAdapter { public boolean throwError = false; @Override public void sendRequest(String input) throws IOException { if (throwError) throw new IOException("Test Exception"); } }

原则上,可以观察到使用抽象引用打开了增加模块化、可测试性和一些非常强大的设计模式的大门,例如外观模式观察者模式等。 它们还可以让开发人员在基于行为Program To An Interface )设计系统的不同部分之间找到一个快乐的平衡,而不会陷入实现细节的困境。

关于抽象的最后一点

抽象具体事物的存在方式不同。 这反映在 Java 编程语言中,抽象类接口可能不会被实例化。

例如,这绝对不会编译:

 public class Main extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { //ERROR x2: Foo f = new Foo(); Bar b = new Bar() } private abstract class Foo{} private interface Bar{} }

事实上,期望未实现的接口抽象类在运行时起作用的想法与期望 UPS 统一在交付包裹时浮动一样有意义。 抽象背后必须有具体的东西才能有用; 即使调用类不需要知道抽象引用背后的实际内容。

抽象类:把它们放在一起

如果你已经做到了这一步,那么我很高兴地告诉你,我没有更多的哲学切线或行话要翻译了。 简单地说,抽象类是一种在一组类之间共享实现行为的机制。 现在,我会马上承认,我发现自己并不经常使用抽象类。 即便如此,我希望在本节结束时,您将确切知道何时需要它们。

锻炼日志案例研究

用 Java 构建 Android 应用程序大约一年后,我从头开始重新构建我的第一个 Android 应用程序。 第一个版本是您期望自学成才的开发人员几乎没有指导的那种可怕的大量代码。 当我想添加新功能时,很明显,我专门用钉子构建的紧密耦合结构已经无法维护,我必须完全重建它。

该应用程序是一个锻炼日志,旨在轻松记录您的锻炼,并能够将过去锻炼的数据输出为文本或图像文件。 在没有深入细节的情况下,我构建了应用程序的数据模型,以便有一个Workout对象,该对象由一组Exercise对象(以及与本讨论无关的其他字段)组成。

当我实现将锻炼数据输出到某种视觉媒体的功能时,我意识到我必须处理一个问题:不同类型的锻炼需要不同类型的文本输出。

为了给你一个粗略的想法,我想根据练习的类型来改变输出,如下所示:

  • 杠铃:10 REPS @ 100 LBS
  • 哑铃:10 REPS @ 50 LBS x2
  • 体重:10 REPS @ 体重
  • 体重 +:10 REPS @ 体重 + 45 磅
  • 定时:60 秒 @ 100 磅

在我继续之前,请注意还有其他类型(工作可能会变得复杂),并且我将展示的代码已被修剪并更改为很好地适合一篇文章。

按照我之前的定义,编写抽象类的目标是实现抽象类中所有子类共享的所有内容(甚至是变量常量状态)。 然后,对于在所述子类中发生变化的任何内容,创建一个抽象方法

 abstract class Exercise { private final String type; protected final String name; protected final int[] repetitionsOrTime; protected final double[] weight; protected static final String POUNDS = "LBS"; protected static final String SECONDS = "SEC "; protected static final String REPETITIONS = "REPS "; public Exercise(String type, String name, int[] repetitionsOrTime, double[] weight) { this.type = type; this.name = name; this.repetitionsOrTime = repetitionsOrTime; this.weight = weight; } public String getFormattedOutput(){ StringBuilder sb = new StringBuilder(); sb.append(name); sb.append("\n"); getSetData(sb); sb.append("\n"); return sb.toString(); } /** * Append data appropriately based on Exercise type * @param sb - StringBuilder to Append data to */ protected abstract void getSetData(StringBuilder sb); //...Getters }

我可能会说显而易见的,但是如果您对抽象类中应该或不应该实现什么有任何疑问,关键是查看在所有子类中重复的实现的任何部分。

现在我们已经确定了所有练习中的共同点,我们可以开始为每种字符串输出创建具有特化的子类:

杠铃练习:

 class BarbellExercise extends Exercise { public BarbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append("\n"); } } }

哑铃练习:

 class DumbbellExercise extends Exercise { private static final String TIMES_TWO = "x2"; public DumbbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append(TIMES_TWO); sb.append("\n"); } } }

体重锻炼:

 class BodyweightExercise extends Exercise { private static final String BODYWEIGHT = "Bodyweight"; public BodyweightExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(BODYWEIGHT); sb.append("\n"); } } }

我确信一些精明的读者会发现可以以更有效的方式抽象出来的东西,但是这个例子的目的(已经从原始来源简化)是为了展示一般的方法。 当然,没有可以执行的东西,没有任何编程文章是完整的。 如果您想对其进行测试,可以使用几个在线 Java 编译器来运行此代码(除非您已经有 IDE):

 public class Main { public static void main(String[] args) { //Note: I actually used another nested class called a "Set" instead of an Array //to represent each Set of an Exercise. int[] reps = {10, 10, 8}; double[] weight = {70.0, 70.0, 70.0}; Exercise e1 = new BarbellExercise( "Barbell", "Barbell Bench Press", reps, weight ); Exercise e2 = new DumbbellExercise( "Dumbbell", "Dumbbell Bench Press", reps, weight ); Exercise e3 = new BodyweightExercise( "Bodyweight", "Push Up", reps, weight ); System.out.println( e1.getFormattedOutput() + e2.getFormattedOutput() + e3.getFormattedOutput() ); } }

Executing this toy application yields the following output: Barbell Bench Press

 10 REPS @ 70.0LBS 10 REPS @ 70.0LBS 8 REPS @ 70.0LBS Dumbbell Bench Press 10 REPS @ 70.0LBSx2 10 REPS @ 70.0LBSx2 8 REPS @ 70.0LBSx2 Push Up 10 REPS @ Bodyweight 10 REPS @ Bodyweight 8 REPS @ Bodyweight

进一步的考虑

Earlier, I mentioned that there are two features of Java interfaces (as of Java 8) which are decidedly geared towards sharing implementation , as opposed to behavior . These features are known as Default Methods and Static Methods .

I have decided not to go into detail on these features for the reason that they are most typically used in mature and/or large code bases where a given interface has many inheritors. Despite the fact that this is meant to be an introductory article, and I still encourage you to take a look at these features eventually, even though I am confident that you will not need to worry about them just yet.

I would also like to mention that there are other ways to share implementation across a set of classes (or even static methods ) in a Java application that does not require inheritance or abstraction at all. For example, suppose you have some implementation which you expect to use in a variety of different classes, but does not necessarily make sense to share via inheritance . A common pattern in Java is to write what is known as a Utility class, which is a simple class containing the requisite implementation in a static method :

 public class TimeConverterUtil { /** * Accepts an hour (0-23) and minute (0-59), then attempts to format them into an appropriate * format such as 12, 30 -> 12:30 pm */ public static String convertTime (int hour, int minute){ String unformattedTime = Integer.toString(hour) + ":" + Integer.toString(minute); DateFormat f1 = new SimpleDateFormat("HH:mm"); Date d = null; try { d = f1.parse(unformattedTime); } catch (ParseException e) { e.printStackTrace(); } DateFormat f2 = new SimpleDateFormat("h:mm a"); return f2.format(d).toLowerCase(); } }

Using this static method in an external class (or another static method ) looks like this:

 public class Main { public static void main(String[] args){ //... String time = TimeConverterUtil.convertTime(12, 30); //... } }

备忘单

We have covered a lot of ground in this article, so I would like to spend a moment summarizing the three main mechanisms based on what problems they solve. Since you should possess a sufficient understanding of the terms and ideas I have either introduced or redefined for the purposes of this article, I will keep the summaries brief.

I Want A Set Of Child Classes To Share Implementation

Classic inheritance , which requires a child class to inherit from a parent class , is a very simple mechanism for sharing implementation across a set of classes. An easy way to decide if some implementation should be pulled into a parent class , is to see whether it is repeated in a number of different classes line for line. The acronym DRY ( Don't Repeat Yourself ) is a good mnemonic device to watch out for this situation.

While coupling child classes together with a common parent class can present some limitations, a side benefit is that they can all be referenced as the parent class , which provides a limited degree of abstraction .

I Want A Set Of Classes To Share Behavior

Sometimes, you want a set of classes to be capable of possessing certain abstract methods (referred to as behavior ), but you do not expect the implementation of that behavior to be repeated across inheritors.

By definition, Java interfaces may not contain any implementation (except for Default and Static Methods ), but any class which implements an interface , must supply an implementation for all abstract methods, otherwise, the code will not compile. This provides a healthy measure of flexibility and restriction on what is actually shared and does not require the inheritors to be of the same class hierarchy .

I Want A Set Of Child Classes To Share Behavior And Implementation

Although I do not find myself using abstract classes all over the place, they are perfect for situations when you require a mechanism for sharing both behavior and implementation across a set of classes. Anything which will be repeated across inheritors may be implemented directly in the abstract class , and anything which requires flexibility may be specified as an abstract method .