第十三章 抽象类和接口

第十三章 抽象类和接口

13.1 引言

父类中定义了相关子类中的共同行为。接口可以用于定义类的共同行为(包括非相关的类)。

可以使用 java.util.Arrays.sort 方法来对数值和字符串进行排序。

13.2 抽象类

抽象类不可以用于创建对象。抽象类可以包含抽象方法,这些方法将在具体的子类中实现。

在继承的层次结构中,每个新的子类都使类变得更加明确和具体。如果从一个子类向父类追溯,类就会变得更通用、更加不明确。类的设计应该确保父类包含它的子类的共同特征。有时候,一个父类设计得非常抽象,以至于它都没有任何具体的实例。这样的类称为抽象类(abstract class)。

GeometricObject 类定义成 Circle 类和 Rectangle 类的父类。Geometric-Object 类对几何对象的共同特征进行了建模。Circle 类和 Rectangle 类都包含分别用于计算圆和矩形的面积和周长的 getArea() 方法和getPerimeter() 方法。因为可以计算所有几何对象的面积和周长,所以最好在 GeometricObject 类中定义getArea()getPerimeter() 方法。但是,这些方法不能在 GeometricObject 类中实现,因为它们的实现取决于几何对象的具体类型。这样的方法称为抽象方法(abstract method),在方法头中使用 abstract 修饰符表示。在 GeometricObject 类中定义了这些方法后,GeometricObject 就成为一个抽象类。

在类的头部使用 abstract 修饰符表示该类为抽象类。在UML图形记号中,抽象类和抽象方法的名字用斜体表示,如下图所示。如下图给出了新的 Geometricobject 类的源代码。

请添加图片描述

请添加图片描述

抽象类和常规类很像,但是不能使用new操作符创建它的实例。抽象方法只有定义而没有实现。它的实现由子类提供。一个包含抽象方法的类必须声明为抽象类。抽象类的构造方法定义为protected,因为它只被子类使用。创建一个具体子类的实例时,其父类的构造方法被调用以初始化父类中定义的数据域。

13.2.1 为何使用抽象方法

为什么在 GeometricObject 类中定义方法 getArea()getPerimeter() 为抽象的而不是在每个子类中定义它们会有什么好处。如下图所示中,就能看出在 Geometricobject 中定义它们的好处。程序创建了两个几何对象:一个圆和一个矩形,调用 equalArea 方法来检查它们的面积是否相同,然后调用 displayGeometricObject 方法来显示它们。

请添加图片描述

13.2.2 抽象类的几点说明

下面是关于抽象类值得注意的几点:

  • 抽象方法不能包含在非抽象类中。如果抽象父类的子类不能实现所有的抽象方法,那么子类也必须定义为抽象的。换句话说,在继承自抽象类的非抽象子类中,必须实现所有的抽象方法。还要注意到,抽象方法是非静态的。
  • 抽象类不能使用 new 操作符来初始化。但是,仍然可以定义它的构造方法,这个构造方法在它的子类的构造方法中调用。例如,GeometricObject 类的构造方法在 Circle 类和 Rectange 类中调用。
  • 包含抽象方法的类必须是抽象的。然而,可以定义一个不包含抽象方法的抽象类。这个抽象类用于作为定义新子类的基类。
  • 子类可以重写父类的方法并将它定义为抽象的。这很少见,但是它在当父类的方法实现在子类中变得无效时是很有用的。在这种情况下,子类必须定义为抽象的。即使子类的父类是具体的,这个子类也可以是抽象的。例如,Object 类是具体的但是它的子类如 GeometricObject 可以是抽象的。

不能使用 new 操作符从一个抽象类创建一个实例,但是抽象类可以用作一种数据类型。因此,下面的语句创建一个元素是 GeometricObject 类型的数组是正确的:

GeometricObject[] objects = new GeometricObject[10];

然后可以创建一个 Geometricobject 的实例,并将它的引用赋值给数组,如下所示:

objects[0] = new Circle();

13.5 接口

接口是一种与类相似的结构,用于为对象定义共同的操作。

接口在许多方面都与抽象类很相似,但是它的目的是指明相关或者不相关类的对象的共同行为。例如,使用适当的接口,可以指明这些对象是可比较的、可食用的或者可克隆的。

为了区分接口和类,Java采用下面的语法来定义接口:

modifier interface InterfaceName {
    /** Constant declarations */
    /** Abstract method signatures */
}

下面是一个接口的例子:

public interface Edible {
	/** Describe how to eat */
    public abstract String howToEat();
}

在Java中,接口被看作是一种特殊的类。就像常规类一样,每个接口都被编译为独立的字节码文件。使用接口或多或少有点像使用抽象类。例如,可以使用接口作为引用变量的数据类型或类型转换的结果等。与抽象类相似,不能使用 new 操作符创建接口的实例。

可以使用 Edible 接口来指定一个对象是否是可食用的。这需要使用 implements 关键字让对象所属的类实现这个接口。例如,如下图中的Chicken类和Fruit类(第20和39行)实现了Edible接口。类和接口之间的关系称为接口继承(interface inheritance)。因为接口继承和类继承本质上是相同的,所以我们将它们都简称为继承

请添加图片描述

请添加图片描述

这个例子使用了多个类和接口。它们的继承关系如下图所示。

请添加图片描述

13.6 Comparable 接口

Comparable 接口定义了 compareTo 方法,用于比较对象。

假设要设计一个找出两个相同类型对象中较大者的通用方法。这里的对象可以是两个学生、两个日期、两个圆、两个矩形或者两个正方形。为了实现这个方法,这两个对象必须是可比较的。因此,这两个对象都该有的共同行为就是 comparable (可比较的)。为此,Java提供了 Comparable 接口。接口的定义如下所示:

请添加图片描述

compareTo 方法判断这个对象相对于给定对象o的顺序,并且当这个对象小于、等于或大于给定对象o时,分别返回负整数、0或正整数。Comparable 接口是一个泛型接口。

13.7 Cloneabe 接口

Cloneable接口指定了一个对象可以被克隆。

经常希望创建一个对象的拷贝。为了实现这个目的,需要使用 clone 方法并理解 Cloneable 接口。接口包括常量和抽象方法,但是 Cloneable 接口是一个特殊情况。在java.lang 包中的 Cloneable 接口的定义如下所示:

package java.lang;
public interface Cloneable {
}

这个接口是空的。一个方法体为空的接口称为标记接口(marker interface)。一个标记接口既不包括常量也不包括方法。它用来表示一个类拥有某些希望具有的特征。实现 Cloneable 接口的类标记为可克降的,而且它的对象可以使用在Object 类中定义的 clone() 方法克隆。

13.8 接口与抽象类

一个类可以实现多个接口,但是只能继承一个父类。

接口的使用和抽象类的使用基本类似,但是,定义一个接口于定义一个抽象类有所不同。下表总结了这些不同点。

请添加图片描述

Java 只允许为类的继承做单一继承,但是允许使用接口做多重继承。例如,

public class NewCass extends BaseClass implements Interface1,..., InterfaceN{...}

利用关键字 extends,接口可以继承其他接口。这样的接口称为子接口(subinterface)。例如,在下面代码中,NewInterfaceInterface1,…,InterfaceN 的子接口。

public interface NewInterface extends Interface1...InterfaceN {
	//constants and abstract methods
}

13.10 类的设计原则

类的设计原则有助于设计出合理的类。

13.10.1 内聚性

类应该描述一个单一的实体,而所有的类操作应该在逻辑上相互契合来支持一个一致的目的。例如: 可以设计一个类用于学生,但不应该将学生与教职工组合在同一个类中,因为学生和教职工是不同的实体。

如果一个实体承担太多的职责,就应该按各自的职责分成几个类。例如stringStringBuffer 类和 StringBuilder 类都用于处理字符串,但是它们的职责不同。string 类处理不可变字符串,StringBuilder 类创建可变字符串,StringBufferStringBuilder 类似,只是 StringBuffer类还包含更新字符串的同步方法。

13.10.2 一致性

遵循标准Java程序设计风格和命名习惯。为类、数据域和方法选取传递信息的名字。通常的风格是将数据声明置于构造方法之前,并且将构造方法置于普通方法之前。

选择名字要保持一致。给类似的操作选择不同的名字并非好的做法。例如: length() 方法返回 StringStringBuilderStringBuffer 的大小。如果在这些类中给这个方法用不同的名字就不一致了。

一般来说,应该具有一致性地提供一个公共无参构造方法,用于构建默认实例。如果一个类不支持无参的构造方法,要用文档写下原因。如果没有显式地定义构造方法,则会提供一个具有空方法体的公有默认无参构造方法。

如果不想让用户创建类的对象,可以在类中声明一个私有的构造方法,Math 类和 GuessDate 类就是如此。

13.10.3 封装性

一个类应该使用 private 修饰符隐藏其数据,以免用户直接访问它。这使得类更易于维护。只在希望数据域可读的情况下,才提供获取方法; 也只在希望数据域可更新的情况下才提供设置方法。例如: Rational 类为numeratordenominator 提供了获取方法,但是没有提供设置方法,因为 Rational 对象是不可改变的。

13.10.4 清晰性

为使设计清晰,内聚性、一致性和封装性都是很好的设计原则。除此之外,类应该有一个很清晰的合约,从而易于解释和易于理解。

用户可以以各种不同组合、不同顺序,以及在各种环境中结合使用多个类。因此,在设计一个类时,这个类不应该限制用户如何以及何时使用该类;设计属性时,应该允许用户按任何顺序和任何组合来设置值;设计方法应该使得功能的实现与它们出现的顺序无关。例如: Loan 类包含属性 loanAmountnumberOfYearsannualInterestRate,这些属性的值可以按任何顺序来设置。

方法应在不产生混淆的情况下进行直观定义。例如: string 类中的 substring (int beginIndex, int endIndex) 方法就有点容易混淆。这个方法返回从 beginIndexendIndex-1 而不是 endIndex 的子串。该方法应该返回从 beginIndexendIndex 的子字符串,从而更加直观。

不应该声明一个可以从其他数据域推导出来的数据域。例如,下面的 Person 类有两个数据域: birthDateage。由于age可以从 birthDate 导出,所以 age 不应该声明为数据域。

public class Person{
	private java.uti1.Date birthDate;
	private int age;
}

13.10.5 完整性

类是为许多不同用户的使用而设计的。为了能在大范围的应用中使用,一个类应该通过属性和方法提供各种自定义功能的实现方式。例如: string类包含了40多个实用的方法来用于各种应用。

13.10.6 实例和静态

依赖于类的具体实例的变量或方法必须是一个实例变量或方法。

应该总是使用类名 (而不是引用变量) 引用静态变量和方法,以增强可读性并避免错误。

13.10.7 继承和聚合

继承和聚合之间的差异,就是 is-a ( 是一种 ) 和 has-a ( 具有 ) 之间的关系。例如,苹果是一种水果,因此,可以使用继承来对 Apple 类和 Fruit 类之间的关系进行建模。人具有名字,因此,可以使用聚合来对 Person 类和 Name 类之间的关系建模。

13.10.8 接口和抽象类

接口和抽象类都可以用于为对象指定共同的行为。如何决定是采用接口还是类呢?通常,比较强的 is-a (是一种)关系清晰地描述了父子关系,应该采用类来建模。例如,因为橘子是一种水果,它们的关系就应该采用类的继承关系来建模。弱的is-a 关系,也称为 is-kind-of (是一类)关系,表明一个对象拥有某种属性。弱的is-a关系可以使用接口建模。例如,所有的字符串都是可以比较的,因此String类实现了Comparable接口。圆或者矩形是一个几何对象,因此Circle可以设计为Geometricobject的子类。有不同的半径,并且可以基于半径进行比较,因此Circle可以实现Comparable接口。

接口比抽象类更加灵活,因为一个子类只能继承一个父类,但是却可以实现任意个数的接口。然而,接口不能包含数据域。Java8中,接口可以包含默认方法和静态方法,这对简化类的设计非常有用。

  • 30
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值