读书笔记--Java语言程序设计

第13章 抽象类和接口 

13.1 引言

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

+6的共同行为(包括非相关类)。

13.2 抽象类

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

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

方法实现取决于对象的具体类型,而不能在父类中实现,这样的方法称为抽象方法,在方法头中使用abstract修饰符表示。在父类定义抽象方法后,,父类就成为一个抽象类,在类头使用abstract修饰符表示该类为抽象类。在UML图形记号中,抽象类和抽象方法的名字用斜体表示。

抽象类和常规类很像,但是不能使用new操作符创建它的实例。抽象方法只有定义而没有实现。它的实现由子类提供。一个包含抽象方法的类必须声明为抽象类。

抽象类的构造方法定义为protected,因为它只被子类使用。创建一个具体子类的实例时,它的父类的构造方法被调用以初始化父类中定义的数据域。

13.2.2 抽象类的几点说明

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

抽象方法不能包含在非抽象类中。如果抽象父类的子类不能实现所有的抽象方法,那么子类也必须定义为抽象的。换句话说,在抽象类扩展的非抽象子类中,必须实现所有的抽象方法。还要注意到,抽象方法是非静态的。

抽象类是不能使用new操作符来初始化的。但是,仍然可以定义它的构造方法,这个构造方法在它的子类的构造方法中调用。例如,GeometricObject类的构造方法可以在Circle类和Rectangle类中调用。

包含抽象方法的类必须是抽象的。但是,可以定义一个不包含抽象方法的抽象类。在这种情况下,不能使用new操作符创建该类的实例。这种类是用来定义新子类的基类的。

即使子类的父类是具体的,这个子类也可以是抽象的。例如,Object类是具体的,但是它的子类如GeomtricObject可以是抽象的。

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

GeometricObject[] objects = new GeometricObject[10];

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

objects[0] = new Circle();

 13.3 示例学习:抽象的Number类

Number类是数值包装类、BigInteger以及BigDecimal的抽象父类。

10.7节介绍了数值包装类,10.9节介绍了BigInteger以及BigDecimal类。这写类有共同的方法byteValue()、shortValue()、intValue()、longValue()、floatValue()和doubleValue(),分别从这些类的对象返回byte、short、int、long、float、以及double值。这些共同的方法实际上在Number类中定义,该类是数值包装类、BigIntegr和BigDecimal类的父类,如图所示。

读书笔记--Java语言程序设计_第1张图片 Number类是Double、Float,...等类的抽象父类

由于intValue()、longValue()、floatValue()以及doubleValue()等方法不能在Number类中给出实现,它们在Number类被定义为抽象方法。因此Number类是一个抽象类。byteValue()和shortValue()方法的实现从intValue()方法而来,如下所示:

public byte byteValue() {
    return (byte)intValue();
}

public short shortValue() {
    return (short)intValue();
}

Number定义为数值类的父类,这样可以定义方法来执行数值的共同操作。

13.4 示例学习:Calendar和GregorianCalendar

GregorianCalendar是抽象类Calendar的一个具体子类。

一个java.util.Date的实例表示以毫秒为精度的特定时刻。java.util.Calendar是一个抽象的基类,可以提取出详细的日历信息,例如,年、月、日、小时、分钟和秒。Calendar类的子类可以实现特定的日历系统,例如,公历(Gregorian历)、农历和犹太历。目前,java支持公历类java.util.GregorianCalendar,如图所示。Calendar类类中的add方法是抽象的,因为它的实现依赖于某个具体的日历系统。

读书笔记--Java语言程序设计_第2张图片 抽象的Calendar类定义了各种日历的共同特点

 

可以使用new GregorianCalendar()利用当前时间构造一个默认的GregorianCalendar对象,可以使用new GregorianCalendar(year, month, date)利用指定的year(年)、month(月)和date(日)构造一个GregorianCalendar对象。参数month是基于0的,即0代表1月(January)。

在Calendar类中定义的get(int field)方法在Calendar类中提取日期和时间信息方面是很有用的,日期和时间域都被定义为常量,如表所示。

Calendar类的域常量
常量 说明
YEAR 日历的年份
MONTH 日历的月份,0表示一月
DATE 日历的天
HOUR 日历的小时(12小时制)
HOUR_OF_DAY 日历的小时(24小时制)
MINUTE 日历的分钟
SECOND 日历的秒
DAY_OF_WEEK 一周的天数,1是星期日
DAY_OF_MONTH 和DATE一样
DAY_OF_YEAR 当前年的天数,1是一年的第一天
WEEK_OF_MONTH 当前月内的星期数,1是该月的第一个星期
WEEk_OF_YEAR 当前 年内的星期数,1是该年的第一个星期
AM_PM 表明是上午还是下午(0表示上午,1表示下午)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

13.5 接口

接口是一种与类相似的结构,只包含常量和抽象方法。

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

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

修饰符  interface 接口名 {
/** 常量声明 */
/** 方法签名 */
}

下面是一个接口的例子:

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

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

可以使用Edible接口来明确一个对象是否是可食用的。这需要使用implements关键字让对象的类实现这个接口来完成。类和接口之间的关系称为接口继承(interface inheritance)。因为接口继承和类继承本质上是相同的,所以我们将它们都简称为继承。

由于接口中所有的数据域都是public static final而且所有的方法都是public abstract,所以Java允许忽略这些修饰符。因此,下面的接口定义是等价的:

public interface T {
  public static final int k = 1;

  public abstract void p();
}

等价于

public interface T {
  int k;

  void p();
}

13.6 Comparable接口

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

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

// Interface for comparing objects, defined in java.lang
package java.lang

public interface Comparable {
  public int compareTo(E o);
}

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

Comparable接口是一个泛型接口。在实现该接口时,泛型类型E被替换成一种具体的类型。Java类库中的许多类实现了Comparable接口以定义对象的自然顺序。Byte、Short、Integer、Long、Float、Double、Character、BigInteger、BigDecimal、Calendar、String已经Date类都实现了Comparable接口。例如,在Java API中,Integer、BigInteger、String以及Date类都如下定义:

public class Integer extends Numbers
    implements Comparable {
  // class body omitted

  @Override
  public int compareTo(Integer o) {
    // Implementation omitted
  }
}

public class BigInteger extends Number
    implements Comparable {
  // class body omitted

  @Override
  public int compareTo(BigInteger o) {
    // Implementation omitted
  }
}

public class String extends Object
    implements Comparable {
  // class body omitted

  @Override
  public int compareTo(String o) {
    // Implementation omitted
  }
}

public class Date extends Object
    implements Comparable {
  // class body omitted

  @Override
  public int compareTo(Date o) {
    // Implementation omitted
  }
}

因此,数字是可比较的,字符串是可比较的,日期也是如此。可以使用compareTo方法来比较两个数字、两个日期。例如,下面代码

1. System.out.println(new Integer(3).compareTo(new Integer(5)));
2. System.out.println("ABC".compareTo("ABE"));
3. java.util.Date date1 = new java.util.Date(2013, 1, 1);
4. java.util.Date date2 = new java.util.Date(2012, 1, 1);
5. System.out.println(date1.compareTo(date2));

显示

-1

-2

1

第一行显示一个负数,因为3小于5.第二行显示一个负数,因为ABC小于ABE。第五行显示一个正数,因为date1大于date2。

将n赋值为一个Integer对象,s为一个string对象,d为一个Date对象。下面的所有表达式都为true。

n instanceof Integer
n instanceof Object
n instanceof Comparable

s instanceof String
s instanceof Object
s instanceof Comparable

d instanceof java.util.Date
d instanceof Object
d instanceof Comparable

由于所有Comparable对象都有compareTo方法,如果对象是Comparable接口类型的实例的话,Java API中的java.util.sort(Object[])方法就可以使用compareTo方法来对数组中的对象进行比较和排序。

接口提供通用程序设计的另一种形式。

Object类包含equals方法,它的目的就是为了让Object类的子类来覆盖它,以比较对象的内容是否相同。假设Object类包含一个类似于Comparable接口中所定义的compareTo方法,那么sort方法可以用来比较一组任意的对象。Object类中是否应该包含ygcompareTo方法尚有争论。由于在Object类中没有定义compareTo方法,所以Java中定义了Comparable接口,以便能够对两个Comparable接口的实例进行比较。强烈建议(尽管不要求)conpareTo应该与equals保持一致。也就是说,对于两个对象o1和o2,应该确保当且仅当o1.equals(o2)为true是o1.compareTo(o2) == 0成立。

13.7 cloneable接口

Cloneable接口给出了一个可克隆的对象。

经常会出现需要创建一个对象拷贝的情况。为了实现这个目的,需要使用clone方法并理解Cloneable接口。

接口包括常量和抽象方法,但是Cloneable接口是一个特殊情况。在java.util.lang包中的Cloneable接口的定义如下所示:

package java.lang

public interface Cloneable {
}

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

Java库中的很多类(例如,Date、Calendar和ArrayList)实现Cloneable。这样,这些类的实例可以被克隆。例如,下面的代码

1. Calendar calendar = new GregorianCalendar(2013, 2, 1);
2. Calendar calendar1 = calendar;
3. Calendar calendar2 = (Calendar)calendar.clone();
4. System.out.println("calendar == calendar1 is " +
5.    (calendar == calendar1));
6. System.out.println("calendar == calendar2 is " +
7.    (calendar == calendar2));
8. System.out.println("calendar.equals(calendar2) is " +
9.    calendar.equals(calendar2));

显示

calendar == calendar1 is true

calendar == calendar2 is false

calendar.equals(calendar2) is true

在前面的代码中,第2行将calendar的引用复制给calendar1,所以calendar和calendar1都指向相同的Calendar对象。第3行创建一个新对象,它是calendar的克隆,然后将这个新对象的引用赋值给calendar2。calendar2和calendar是内容相同的不同对象。

下面的代码。

1. ArrayList list1 = new ArrayList<>();
2. list1.add(1.5);
3. list1.add(2.5);
4. list1.add(3.5);
5. ArrayList list2 = (ArrayList)list1.clone();
6. Arraylist list3 = list1;
7. list2.add(4.5);
8. list3.remove(1.5);
9. System.out.println("list1 is " + list1);
10. System.out.println("list2 is " + list2);
11. System.out.println("list2 is " + list3);

显示

list1 is [2.5, 3.5]

list2 is [1.5, 2.5, 3.5, 4.5]

list3 is [2.5, 3.5]

前面的代码中,第5行创建了一个新对象作为list1的克隆,并且将新对象的引用赋值给list2。list2和list1是具有同样内容的不同对象。第6行复制list1的引用给list3,因此list1和list3指向同一个ArrayList对象。第7行将4.5添加到list2中。第8行从list3中移除1.5。由于list1和list3指向同一个ArrayList,第9行和第11行显示同样的内容。

可以调用clone方法克隆一个数组。例如,下面的代码

1  int[] list1 = {1, 2};
2  int[] list2 = list1.clone();
3  list[0] = 7;
4  list[1] = 8;
5  System.out.println("list1 is " + list1[0] + ", " + list1[1]);
6  System.out.println("list1 is " + list2[0] + ", " + list2[1]);

显示

list1 is 7,2

list2 is 1,8

为了定义一个实现Cloneable接口,这个类必须覆盖Object类中的clone()方法。

浅复制意味着如果数据域是对象类型,那么复制的是对象的引用,而不是它的内容。

13.8 接口与抽象类

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

接口与抽象类
  变量 构造方法 方法
抽象类 无限制 子类通过构造方法链调用构造方法,抽象类不能用new操作符实例化 无限制
接口 所有的变量必须是public static final 没有构造方法。接口不能用new操作符实例化 所有方法必须是公共的抽象实例方法

 

 

 

 

 

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

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

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

public interface NewInterface extends Interface1, ..., Interface {
  // constans and abstract methods
}

一个实现NewInterface的类必须实现在NewInterface,Interface1,…,InterfaceN中定义的抽象方法。接口可以扩展其他接口而不是类。一个类可以扩展它的父类同时实现多个接口。

所有的类共享同一个根类Object,但是接口没有共同的根。与类相似,接口也可以定义一种类型。一个接口类型的变量可以引用任何实现该接口的实例。如果一个类实现了一个接口,那么这个接口就类似于该类的一个父类。可以将接口当作一种数据类型使用,将接口类型的变量转换为它的子类,反过来也可以。例如,假设c是图中Class2的一个实例,那么c也是Object、Class1、Interface1、Interface1_1、Interface1_2、Interface2_1和Inetrface2_2的实例。

读书笔记--Java语言程序设计_第3张图片

 

 

 

 

类名是一个名词。接口名可以是形容词或名词。

抽象类和接口都是用来明确多个对象的共同特征的。那么该如何确定在什么情况下应该使用接口,什么情况一个使用类呢?一般来说,清晰描述父子关系的强的“是一种”的关系(strong is-a relationship)应该使用类建模。例如,因为公历是一种日历,所以类java.util.GregorianCalendar和java.util.Calendar使用类继承建模的。弱的“是一种”的关系(weak is-a relationship)也称为类属关系(is-kind-of relationship),它表明对象拥有属性,可以用接口来建模。例如,所有的字符串都是可比较的,因此,String类实现Comparable接口。

通常,推荐使用接口而非抽象类,因为接口定义非相关类共有的父类型。接口比类工具灵活。考虑Animal类。假设Animal类类中定义了howToEat方法,如下所示:

abstract class Animal
  public abstract String howToEat();
}

Animal的两个子类定义如下:

class Chicken extends Animal {
  @Override
  public String howToEat() {
    return "Fry it";
  }
}

class Duck extends Animal
  @Override
  public String howToEat() {
    return "Roast it";
  }
}

假设给定这个继承体系结构多态会让你在一个类型为Animal的变量中保存Chicken对象或Duck对象的引用,如下面代码所示:

public static void main(String[] args) {
  Animal animal = new Chicken();
  eat(animal);

  animal = new Duck();
  eat(animal);
}

public static void eat(Animal animal) {
  animal.howToEat();
}

JVM会基于调用方法时所用的确切对象来动态地决定调用哪个howToEat方法。

可以定义Animal的一个子类。但是,这里有个限制条件。该子类必须是另一种动物(例如,Turkey)。

接口就无此限制。接口比类拥有更多的灵活性,因为不用使所有东西都属于同一个类型的类。可以定义接口中的howToEat()方法,然后把它当作其他类的公用父类型。

public static void main(String args[]) {
  Edible stuff = new Chicken();
  eat(stuff);

  stuff = new Duck();
  eat(stuff);

  stuff = new Broccoli();
  eat(stuff);
}
public static void eat(Edible stuff) {
  stuff.howToEat();
}

interface Edible {
  public String howToEat();
}

class Chicken implements Edible {
  @Override
  public String howToEat() {
    return "Fly it";
  }
}

class Duck implements Edible {
  @Override
  public String howToEat() {
    return "Roast it";
 }
}

class Broccoli implements Edible {
  @Override
  public String howToEat() {
    return "Stir-fly it";
  }
}

为了定义表示可食用对象的一个类,只须让该类实现Edible接口即可。选择,这个类就成为Edible类型的子类型。任何Edible对象都可以传递以调用howToEat方法。

13.10 类的设计原则

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

13.10.1 内聚性

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

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

13.10.2 一致性

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

选择名字要保持一致。给类似的操作选择不同的名字并非良好的实践。例如,length()方法返回String、StringBuilder和StringBUffer的大小。如果在这些类中给这个方法用不同的名字就不一致了。

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

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

13.10.3 封装性

一个类应该使用private修饰符隐藏其数据,以免用户直接访问它。这使得类更易于维护。

只在希望数据域可读的情况下,才提供get方法;也只在希望数据域可更新的情况下,才提供set方法。例如:Rational类为numerator和denominator提供了get方法,但是没有提供set方法,因为Rational对象是不可改变的。

13.10.4 清晰性

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

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

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

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

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

  ...
}

13.10.5 完整性

类是为许多不同的用户使用而设计的。为了能在一个广泛的应用中使用,一个类一个通过属性和方法提供多种方案以适应用户的不同需求。例如,为满足不同的应用需求,String类包含了40多种很实用的方法。

13.10.6 实例和静态

依赖于类的具体实例的变量或方法必须是一个实例变量或方法。如果一个变量被类的所有实例所共享,那就应该将它声明为静态的。例如:在程序清单9-8中,CircleWithPrivateDataFields中的变量numberOfObjects被CircleWithPrivateDateFields类的所有对象共享。因此,它被声明为静态的。如果方法不依赖于某个具体的实例,那就应该将它声明为静态方法。例如:CricleWithPrivateDataFields中的getNumberOfObjects()方法没有绑定到任何具体实例,因此,他被声明为静态方法。

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

不要从构造方法中参入参数来初始化静态数据域。最好使用set方法改变静态数据域。图a中的类最好用图b中的代替。

public class SomeThing {
  private int t1;
  private static int t2;

  public SomeThing(int n1, int n2) {
    ...
  }
}
public class SomeThing {
  private int t1;
  private static int t2;

  public SomeThing(int t1) {
    ...
  }

  public static void setT2(int t2) {
    SomeThing.t2 = t2;
  }
}

实例和静态是面向程序设计不可或缺的部分。数据域或方法要么是实例的,要么是静态的。不要错误的忽视了静态数据域或方法。常见的设计错误是将本应该声明为静态方法的方法声明为实例方法。例如:用于计算n的阶乘factional(int n)方法一个定义为静态的,因为它不依赖于任何具体实例。

构造方法永远都是实例方法,因为它是用来创建具体实例的。一个静态变量或方法可以从实例方法中调用,但是不能从静态方法中调用实例变量或方法。

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接口。

接口比抽象类更加灵活,因为一个子类只能继承一个父类,但是却可以实现任意个数的接口。然而,接口不能具有具体的方法。可以结合接口和抽象类的优点,创建一个接口,使用一个抽象类来实现它。可以使其方便使用接口或者抽象类。

你可能感兴趣的:(读书笔记--Java语言程序设计)