第十三章 抽象类和接口
课本笔记 第十三章
13.1 引言
- 要点提示:父类中定义类相关子类中的共同行为。接口可以用于定义类的共同行为(包括非相关的类)。
- 可以使用 java.util.Arrays.sort 方法来对数值和字符串进行排序。那么可以应用同样的sort方法对一个几何对象的数组进行排序吗?为了编写这样的代码,必须要了解接口。接口用于定义多个类(包括非相关的类)的共同行为。在讨论接口之前,我们必须介绍一个密切相关的主题:抽象类。
13.2 抽象类
- 要点提示:抽象类不可以用于创建对象。抽象类可以包含抽象方法,这些方法将在具体的子类中实现。
- 在继承的层次结构中,每个新的子类都使类变得更加明确和具体,如果从一个子类向父类追溯,类就会变得更通用、更加不明确。类的设计应该确保父类包含它的子类的共同特征。有时候,一个父类设计得非常抽象,以至于它都没有任何具体的实例。这样的类称为抽象类(abstract class)。
- 在第11章中,GeometricObject 类定义成Circle 类和Rectangle 类的父亲。GeometricObject 类对几何对象的共同特征进行了建模。Circle 类 和Rectangle 类都包含分别用于计算圆和矩形的面积和周长的getArea( )方法和getPerimeter( )方法。因为可以计算所有几何对象的面积和周长,所以最好在GeometricObject 类中定义getArea( ) 和getPerimeter( )方法。但是,这些方法不能在GeometricObject 类中实现,因为它们的实现取决于几何对象的具体类型。这样的方法称为抽象方法(abstract method),在方法头中使用abstract 修饰符表示。在GeometricObject 类中定义了这些方法后,GeometricObject 就成为了一个抽象类。在类的头部使用abstract 修饰符表示该类为抽象类。在UML图形记号中,抽象类和抽象方法的名字用斜体表示,如图13-1所示。程序清单13-1 给出了新的GeometricObject 类的源代码
程序清单 13-1 GeometricObject.java
public abstract class GeometricObject {
private String color = "white";
private boolean filled;
private java.util.Date dateCreated;
/**
* Construct a default geometric object
*/
protected GeometricObject() {
dateCreated = new java.util.Date();
}
/**
* Construct a geometric object with color and filled value
*/
protected GeometricObject(String color, boolean filled) {
dateCreated = new java.util.Date();
this.color = color;
this.filled = filled;
}
/**
* Return color
*/
public String getColor() {
return color;
}
/**
* Set a new color
*/
public void setColor(String color) {
this.color = color;
}
/**
* Return filled .Since filled is boolean
* the getter method is named isFilled
*/
public boolean isFilled() {
return filled;
}
/**
* Set a new filled
*/
public void setFilled(boolean filled) {
this.filled = filled;
}
/**
* Get dataCreated
*/
public java.util.Date getDateCreated() {
return dateCreated;
}
@Override
public String toString() {
return "created on " + dateCreated + "\ncolor: " + color +
" and filled: " + filled;
}
/**
* Abstract method getArea
*/
public abstract double getArea();
/**
* Abstract method getPerimeter
*/
public abstract double getPerimeter();
}
- 抽象类和常规类很像,但是不能使用new 操作符创建它的实例。抽象方法只有定义而没有实现。它的实现由子类提供。一个包含抽象方法的类必须声明为抽象类。
- 抽象类的构造方法定义为protected ,因为它只被子类使用。创建一个具体子类的实例时,其父类的构造方法被调用以初始化父类中定义的数据域。
- 抽象类GeometricObject 为几何对象定义了共同特征(数据和方法),并且提供了合适的构造方法。因为不知道如何计算几何对象的面积和周长,所以,getArea( )和getPerimeter( )定义为抽象方法。这些方法在子类中实现。Circle类和Rectangle 类的实现除了继承本章中定义的GeometricObject 类之外,其他与程序清单11-2 和程序清单11-3 一样。可以分别从liveexample.personcmg.com/html/Circle.html 和 liveexample.pearsoncmg.com/html/Rectangle.html 得到两个程序的完整代码。
程序清单 13-2 Circle.java
public class Circle extends GeometricObject {
//Same as line 2-47 in Listing 11.2 ,so omitted
//为了程序完整,我还是把代码copy过来了
private double radius;
public Circle() {
}
public Circle(double radius) {
this.radius = radius;
}
public Circle(double radius, String color, boolean filled) {
this.radius = radius;
setColor(color);
setFilled(filled);
}
/**
* Return radius
*/
public double getRadius() {
return radius;
}
/**
* Set a new radius
*/
public void setRadius(double radius) {
this.radius = radius;
}
/**
* Return area
*/
public double getArea() {
return radius * radius * Math.PI;
}
/**
* Return diameter
*/
public double getDiameter() {
return 2 * radius;
}
/**
* Return perimeter
*/
public double getPerimeter() {
return 2 * radius * Math.PI;
}
/**
* Print the circle info
*/
public void printCircle() {
System.out.println("The circle is created " + getDateCreated()
+ " and the radius is " + radius);
}
}
程序清单 13-3 Rectangle.java
public class Rectangle extends GeometricObject {
private double width;
private double height;
public Rectangle() {
}
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public Rectangle(double width, double height, String color, boolean filled) {
this.width = width;
this.height = height;
setColor(color);
setFilled(filled);
}
/**
* Return width
*/
public double getWidth() {
return width;
}
/**
* Set a new width
*/
public void setWidth(double width) {
this.width = width;
}
/**
* Return height
*/
public double getHeight() {
return height;
}
/**
* Set a new height
*/
public void setHeight(double height) {
this.height = height;
}
/**
* Return area
*/
public double getArea() {
return width * height;
}
/**
* Return perimeter
*/
public double getPerimeter() {
return 2 * (width + height);
}
}
13.2.1 为何使用抽象方法
- 你可能会疑惑在 GeometricObject 类中定义方法getArea( ) 和 getPerimeter( ) 为抽象的而不是在每个子类中定义它们会有什么好处。从下面程序清单13-4 的例子中,就能看出在 GeometricObject 中定义它们的好处。程序创建了两个几何对象:一个圆和一个矩形,调用equalArea方法来检查它们的面积是否相同,然后调用displayGeometricObject 方法来显示它们。
程序清单 13-4 TestGeometricObject.java
public class TestGeometricObject {
/**
* Main method
*/
public static void main(String[] args) {
//Create two geometric objects
GeometricObject geoObject1 = new Circle(5);
GeometricObject geoObject2 = new Rectangle(5, 3);
System.out.println("The two objects have the same area? " + equalArea(geoObject1, geoObject2));
//Display circle
displayGeometricObject(geoObject1);
//Display rectangle
displayGeometricObject(geoObject2);
}
/**
* A method foe comparing the areas of two geometric objects
*/
public static boolean equalArea(GeometricObject object1, GeometricObject object2) {
return object1.getArea() == object2.getArea();
}
/**
* A method for displaying a geometric object
*/
public static void displayGeometricObject(GeometricObject object) {
System.out.println();
System.out.println("The area is " + object.getArea());
System.out.println("The perimeter is " + object.getPerimeter());
}
}
-
Circle 类和Rectangle 类中重写了定义在 GeometricObject 类中的 getArea( ) 和 getPerimeter( )方法。语句(第5 ~ 6行)
GeometricObject geoObject1 = new Circle(5); GeometricObject geoObject2 = new Rectangle(5.3);
-
创建了一个新的圆和一个新的矩形,并把它们赋值给变量getObject1 和 geoObject2。这两个变量都是GeometricObject 类型的。
-
当调用equalArea(geoObject1, geoObject2)时(第9行),由于geoObject1是一个圆,所以object1.getArea() 使用的是Circle 类定义的getArea( )方法,而getObject2 是一个矩形,所以object2.getArea( )使用的是Rectangle 类中定义的getArea( )方法。
-
类似地,当调用displayGeometricObject(geoObject1) (第12行)时,使用在Circle 类中定义的 getArea( ) 和 getPerimeter( )方法,而当调用displayGeometricObject(geoObject2)(第15行)时。使用的是在Rectangle类中定义的 getArea( ) 和 getPerimeter( )方法。JVM在运行时根据调用该方法的实际对象的类型来动态地决定调用哪一个方法。
-
注意。如果GeometricObject 里没有定义getArea( )方法,就不能在该程序中定义equalArea 方法来计算这两个几何对象的面积是否相同。所以,现在可以看出在GeometricObject 中定义抽象方法的好处。
13.2.2 抽象类的几点说明
下面的关于抽象类值得注意的几点:
-
抽象方法不能包含在非抽象类中。如果抽象父类的子类不能实现所有的抽象方法,那么子类也必须定义为抽象的。换句话说,在继承自抽象类的非抽象子类中,必须实现所有的抽象方法。还要注意到,抽象方法是非静态的。
-
抽象类不能使用new 操作符来初始化。但是,仍然可以定义它的构造方法,这个构造方法在它的子类的构造方法中调用。例如,GeometricObject类的构造方法在Circle类和Rectangle 类中调用。
-
包含抽象方法的类必须是抽象的。然而,可以定义一个不包含抽象方法的抽象类。这个抽象类用于作为定义新子类的基类。
-
子类可以重写父类的方法并将它定义为抽象的。这很少见,但是它在当父类的方法实现在子类中变得无效时是很有用的。在这种情况下,子类必须定义为抽象的。
-
即使子类的父类是具体的,这个子类也可以是抽象的。例如,Object 类是具体的,但是它的子类如GeometricObject 可以是抽象的。
-
不能使用new 操作符从一个抽象类创建一个实例,但是抽象类可以用作一种数据类型。因此,下面的语句创建一个元素是GeometricObject类型的数组是正确的:
GeometricObject[] objects = new GeometricObject[10];
-
然后可以创建一个GeometricObject的实例,并将它的引用赋值给数组,如下所示:
object[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 类中定义,该类是数值包装类、BigInteger 和 BigDecimal 类的父类,如图13-2 所示。
-
由于intValue( )、longValue( )、floatValue( ) 以及 doubleValue( )等方法不能在Number 类中给出实现,它们在Number 类中被定义为抽象方法。因此Number 类是一个抽象类。byteValue( ) 和 shortValue( ) 方法的实现从 intValue( )方法得到,如下所示:
public byte byteValue(){ return (byte)intValue(); } public short shortValue(){ return (short)intValue(); }
-
Number 定义为数值类的父类,这样可以定义方法来执行数值的共同操作。程序清单13-5 给出了一个程序,找到一个Number 对象列表中的最大数
程序清单 13-5 LargestNumber.java
/**
* 原书上是使用通配符导入 import java.math.*
* 为了体现导包更加具体,我没有使用上述语句,往后的import 语句基本也如是
* 2021-8-4留
*/
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
public class LargestNumber {
public static void main(String[] args) {
ArrayList<Number> list = new ArrayList<>();
//Add an integer
list.add(45);
//Add a double
list.add(3445.53);
//Add a BigInteger
list.add(new BigInteger("3432323234344343101"));
//Add a BigDecimal
list.add(new BigDecimal("2.0909090989091343433344343"));
System.out.println("The largest number is " +
getLargestNumber(list));
}
public static Number getLargestNumber(ArrayList<Number> list) {
if (list == null || list.size() == 0) {
return null;
}
Number number = list.get(0);
for (int i = 1; i < list.size(); i++) {
if (number.doubleValue() < list.get(i).doubleValue()) {
if (number.doubleValue() < list.get(i).doubleValue()) {
number = list.get(i);
}
}
}
return number;
}
}
- 程序创建一个Number 对象的ArrayList(第6行),向列表中添加一个Integer 对象、一个Double 对象、一个BigInteger 对象以及一个BigDeciaml 对象(第7 ~ 12行)。注意,通过拆箱操作,第7行中45自动转换为Integer 对象并添加到列表中,第8行中3445.53 自动转换为Double 对象并增加到列表中。
- 调用getLargestNumber 方法返回列表中的最大数值(第15行)。如果列表为null 或者列表大小为0,则getLargestNumber 方法返回null(第19 和 20 行)。为了找到列表中的最大数值,通过调用数值对象上面的doubleValue( )方法(第24行)。doubleValue( )方法定义在Number 类中,并在Number 类的具体子类中实现。如果一个数值是一个Integer 对象,Integer 的 doubleValue( ) 方法被调用。如果数值是一个BigDecimal 对象,BigDecimal 的doubleValue( )方法被调用。
- 如果duobleValue( )方法没有在Number 类中定义,将不能使用Number类从各种不同类型的数值中找到最大数值。
13.4 示例学习:Calendar 和 GregorianCalendar
- 要点提示:GregorianCalendar 是抽象类 Calendar 的一个具体子类。
- 一个java.util.Date 的实例表示一个以毫秒为精度的特定时刻。java.util.Calendar 是一个抽象的基类,可以提取出详细的日历信息,例如,年、月、日、小时、分钟和秒。Calendar 类的子类可以实现特定的日历系统,例如,公历(Gregorian calendar)、农历(lunar calendar)和犹太历(Jewish calendar)。目前,Java支持公历类java.util.GregorianCalendar ,如图13-3 所示。Calendar 类中的add方法是抽象的,因为它的实现依赖于某个具体的日历系统。
- 可以使用new GregorianCalendar ( )利用当前时间构造一个默认的GregorianCalendar 对象,可以使用new GregorianCalendar (year, month, date) 利用指定的year(年)、month(月)和date(日期)构造一个GregorianCalendar 对象。参数 month 是基于0 的,即0代表1月。
- 在Calendar 类中定义的get(int field) 方法从Calendar 类中提取日期和时间信息是很有用的。日期和时间域被定义为常量,如表13-1所示。
- 程序清单13-6 给出的例子显示了当前时间的日期和时间信息。
常量 | 说明 |
---|---|
YEAR | 日历的年份 |
MONTH | 日历的月份,0表示1月 |
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-6 TestCalendar.java
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
public class TestCalendar {
public static void main(String[] args) {
//Construct a Gregorian calendar for the current date and time
Calendar calendar = new GregorianCalendar();
System.out.println("Current time is " + new Date());
System.out.println("YEAR: " + calendar.get(Calendar.YEAR));
System.out.println("MONTH: " + calendar.get(Calendar.MONTH));
System.out.println("DATE: " + calendar.get(Calendar.DATE));
System.out.println("HOUR: " + calendar.get(Calendar.HOUR));
System.out.println("HOUR_OF_DAY: " + calendar.get(Calendar.HOUR_OF_DAY));
System.out.println("MINUTE: " + calendar.get(Calendar.MINUTE));
System.out.println("SECOND: " + calendar.get(Calendar.SECOND));
System.out.println("DAY_OF_WEEK: " + calendar.get(Calendar.DAY_OF_WEEK));
System.out.println("DAY_OF_MONTH: " + calendar.get(Calendar.DAY_OF_MONTH));
System.out.println("DAY_OF_YEAR: " + calendar.get(Calendar.DAY_OF_YEAR));
System.out.println("WEEK_OF_MONTH: " + calendar.get(Calendar.WEEK_OF_MONTH));
System.out.println("WEEK_OF_YEAR: " + calendar.get(Calendar.WEEK_OF_YEAR));
System.out.println("AM_PM: " + calendar.get(Calendar.AM_PM));
//Construct a calendar for December 25, 1997
Calendar calendar1 = new GregorianCalendar(1997, 11, 25);
String[] dayNameOfWeek = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
System.out.println("December 25, 1997 is a " + dayNameOfWeek[calendar1.get(Calendar.DAY_OF_WEEK) - 1]);
}
}
- Calendar 类中定义的set(int field, value)方法用来设置一个域。例如,可以使用calendar.set(Calendar.DAY_OF_MONTH, 1) 将calendar 设置为当月的第一天。
- add(field, value) 方法为某个特定域增加指定的量。例如,add(Calendar.DAY_OF_MONTH, 5) 给日历的当前时间加五天,而add(Calendar.DAY_OF_MONTH, -5)从日历的当前时间减去五天。
- 为了获得一个月中的天数,使用calendar,getActualMaximum(Calendar.DAY_OF_MONTH)方法。例如,如果是3月中的calendar,那么这个方法将返回31。
- 可以通过调用calendar.setTime(date) 为calendar 设置一个用Date 对象表示的时间,通过调用calendar.getTime( ) 获取时间。
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
关键字让对象所属的类实现这个接口。例如,程序清单13-7 中的Chicken 类和Fruit 类(第20和39行)实现了Edible 接口。类和接口之间的关系称为接口继承(interface inheritance)。因为接口继承和类继承本质上是相同的,所以我们将它们都简称为继承。
程序清单 13-7 TestEdible.java
//这个程序由于省略了数据字段、构造函数和方法,会导致报错,后期会跟进修正
public class TestEdible {
public static void main(String[] args) {
Object[] objects = {new Tiger(), new Chicken(), new Apple()};
for (int i = 0; i < objects.length; i++) {
if (objects[i] instanceof Edible) {
System.out.println(((Edible) objects[i]).howToEat());
}
if (objects[i] instanceof Animal) {
System.out.println(((Animal) objects[i]).sound());
}
}
}
}
abstract class Animal {
private double weight;
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
/**
* Return animal sound
*/
public abstract String sound();
}
class Chicken extends Animal implements Edible {
@Override
public String howToEat() {
return "Chicken: Fry it";
}
@Override
public String sound() {
return "Chicken: cock-a-doodle-doo";
}
}
class Tiger extends Animal {
@Override
public String sound() {
return "Tiger: RR00AARR";
}
}
abstract class Fruit implements Edible {
//Data fields,constructors , and methods omitted here
//这里省略了数据字段、构造函数和方法
}
class Apple extends Fruit {
@Override
public String howToEat() {
return "Apple: Make apple cider";
}
}
class Orange extends Fruit {
@Override
public String howToEat() {
return "Orange: Make orange juice";
}
}
-
这个例子使用了多个类和接口。它们的继承关系如图13-4 所示。
-
Animal 类定义了属性weight 及其获取方法和设置方法(第16 ~ 24 行),还定义了sound 方法(第27行)。sound 方法是一个抽象方法,将被具体的animal 类所实现的。
-
Chicken 类实现了Edilble 接口。因为它没有实现howToEat 方法,所以Fruit 必须定义为abstract (第49行)。Fruit 的具体子类必须实现hotToEat 方法。Apple 类和Orange 类实现了hotToEat 方法(第55和62行)。
-
main方法创建由Tiger、Chicken和Apple 类型的三个对象构成的数值(第3行),如果某元素是可食用的,则调用howToEat 方法(第6行),如果某元素是一种动物,则调用sound 方法(第9行)。
-
本质上,Eidble 接口定义了可食用对象的共同行为。所有可食用的对象都有howToEat方法。
-
注意:由于接口中所有的数据域都是public static final 而且所有的方法都是public abstract ,所以 Java 允许忽略这些修饰符。因此,下面的接口定义是等价的:
-
尽管public 修饰符对于定义在接口中的方法可以省略掉,但是在子类实现时方法必须定义为public 的。
-
注意:Java 8 引入了使用关键字default 的默认接口方法。一个默认接口方法为接口中的方法提供了一个默认实现。实现该接口的类可以简单地使用方法的默认实现,或者使用一个新的实现来重写该方法。利用该特征可以在一个具有默认实现的已有接口中添加一个新的方法,并且无须为实现了该接口的已有类重写编写代码。
-
Java 8 还允许接口中存在公有的静态方法。接口中的公有静态方法和类中的公有静态方法一样使用。下面是一个在接口中定义默认方法和静态方法的示例:
public interface A{ /** default method */ public default void doSomething(){ System.out.println("Do something"); } /** static method */ public static int getAValue(){ return 0; } }
13.6 Comparable 接口
-
要点提示:Comparable 接口定义了compareTo方法,用于比较对象。
-
假设要设计一个找出两个相同类型的对象中较大者的通用方法。这里的对象可以是两个学生、两个日期、两个圆、两个矩形或者两个正方形。为了实现这个方法,这两个对象必须是可比较的。因此,这两个对象都该有的共同行为就是comparable(可比较的)。为此, Java 提供了Comparable 接口。接口的定义如下所示:
//Interface for comparingt objects, defined in java.lang package java.lang; public interface Comparable<E>{ 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 类都如下定义:
-
因此,数字是可比较的,字符串是可比较的,日期也是如此。可以使用compareTo 方法来比较两个数字、两个字符串以及两个日期。例如,下面代码
System.out.println(new Integer(3).compareTo(new Integer(5))); System.out.println("ABC".compareTo("ABC")); java.util.Date date1 = new java.util.Date(2013, 1, 1); java.util.Date date2 = new java.util.Date(2012, 1, 1); System.out.println(date1.compareTo(date2));
-
显示
-1 0 1
-
第1行显示一个负数,因为3小于5。第2行显示0,因为ABC 等于ABC。第5行显示一个正数,因为date1大于date2。
-
将n 赋值为一个Integer 对象,s 为一个string 对象,d 为一个Date 对象。下面的所有表达式都为true。
-
由于所有Comparable 对象都有compareTo 方法,如果对象是Comparable 接口类型的实例的话,Java API 中的java.util.Arrays.sort(Object[])方法就可以使用compareTo 方法来对数组中的对象进行比较和排序。程序清单13-8 给出了一个对字符串数组和BigInteger 对象数组进行排序的示例:
程序清单 13-8 SortComparableObjects.java
import java.math.BigInteger;
public class SortComparableObjects {
public static void main(String[] args) {
//萨凡纳 波士顿 亚特兰大 坦帕市
String[] cities = {"Savannah", "Boston", "Atlanta", "Tampa"};
//按照字母顺序大小进行排序
java.util.Arrays.sort(cities);
for (String city : cities
) {
System.out.print(city + " ");
}
System.out.println();
//按照数字大小进行排序
BigInteger[] hugeNumbers = {new BigInteger("2323231092923992"), new BigInteger("432232323239292")
, new BigInteger("54623239292")};
java.util.Arrays.sort(hugeNumbers);
for (BigInteger number : hugeNumbers
) {
System.out.print(number + " ");
}
}
}
- 程序创建一个字符串数组(第5行),并且调用sort 方法来对字符串进行排序(第6行)。程序创建一个BigInteger 对象的数组(第11 ~ 13 行),并且调用sort 方法来对BigInteger 对象进行排序(第14 行)。
- 不能使用sort 方法来对一个Rectangle 对象数组进行排序,因为Rectangle 类没有实现接口Comparable。然而,可以定义一个新的Rectangle 类来实现Comparable 。这个新类的实例就是可比较的。将这个新类命名为ComparableRectangle,如程序清单13-9 所示。
程序清单 13-9 ComparableRectangle.java
public class ComparableRectangle extends Rectangle
implements Comparable<ComparableRectangle> {
/**
* Construct a ComparableRectangle with specified properties
*/
public ComparableRectangle(double width, double height) {
super(width, height);
}
//Implement the compareTo method defined in Comparable
@Override
public int compareTo(ComparableRectangle o) {
if (getArea() > o.getArea())
return 1;
else if (getArea() < o.getArea())
return -1;
else
return 0;
}
//Implement the toString method in GeometricObject
@Override
public String toString() {
return super.toString() + " Area: " + getArea();
}
}
- ComparableRectangle类继承自Rectangle 类并实现了Comparable , 如图13-5 所示。关键字implements 表示ComparableRectangle 类继承了Comparable 接口的所有常量,并实现该接口的方法。compareTo 方法比较两个矩形的买诺记。ComparableRectangle 类的一个实例也是Rectangle、GeometricObject、Object和Comparable的实例。
- 现在,可以使用sort方法来对ComparableRectangle对象数组进行排序了,如程序清单13-10所示。
- 接口提供通用程序设计的另一种形式。在这个例子中,如果不用接口,很难使用通用的sort方法来对对象排序,因为必须使用多重继承才能同时继承Comparable 和另一个类,例如Rectangle。
程序清单 13-10 SortRectangles.java
//有点小问题,输出和书本上给出了有些不一样
public class SortRectangles {
public static void main(String[] args) {
ComparableRectangle[] rectangles = {
new ComparableRectangle(3.4, 5.4),
new ComparableRectangle(13.24, 55.4),
new ComparableRectangle(7.4, 35.4),
new ComparableRectangle(1.4, 25.4)};
java.util.Arrays.sort(rectangles);
for (Rectangle rectangle : rectangles
) {
System.out.print(rectangle + " ");
System.out.println();
}
}
}
- Object 类包含equals 方法,它的目的就是为了让Objec 类的子类来重写它,以比较对象的内容是否相同。假设Object 类包含一个类似于Comparable 接口中所定义的compareTo方法,那么sort 方法就可以用来比较一组任意的对象。Object 类中是否应该包含一个compareTo方法尚有争论。由于在Object 类中没有定义compareTo方法,所以 Java 中定义了Comparable 接口,以便能够对两个Comparable 接口的实例对象进行比较。compareTo 应该与equals 保持一致。也就是说,对于两个对象o1 和 o2,应该确保当且仅当o1.equals(o2)为true 时o1.compareTo(o2) == 0 成立。因此,也应该在ComparableRectangle 类中重写equals 方法,使得两个矩形具有同样面积时返回true。
13.7 Cloneable 接口
-
要点提示:Cloneable 接口指定了一个对象可以被克隆。
-
经常希望创建一个对象的拷贝。为了实现这个目的,需要使用clone 方法并理解Cloneable 接口。
-
接口包括常量和抽象方法,但是Cloneable 接口是一个特殊情况。在java.lang 包中的Cloneable 接口的定义如下所示:
package java.lang; public interface Cloneable{ }
-
这个接口是空的。一个方法体为空的接口称为标记接口(marker interface)。一个标记接口既不包括常量也不包括方法。它用来表示一个类拥有某些希望具有的特征。实现Cloneable 接口的类标记为可克隆的,而且它的对象可以使用在Object 类中定义的clone( )方法克隆。
-
Java 库中的很多类(例如,Date、Calendar和 ArrayList)实现了Cloneable。这样,这些类的实例可以被克隆。例如,下面的代码
Calendar calendar = new GregorianCalendar(2013, 2, 1); Calendar calendar1 = calendar; Calendar calendar2 = (Calendar)calendar.clone(); System.out.println("calendar == calendar1 is " + (calendar == calendar1)); System.out.println("calendar == calendar2 is " + (calendar == calendar2)); System.out.println("calendar.equals(calendar2) is " + 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 是内容相同的不同对象。
-
下面的代码
ArrayList<Double> list1 = new ArrayList<>(); list1.add(1.5); list1.add(2.5); list1.add(3.5); ArrayList<Double> list2 = (ArrayList<Double>)list1.clone(); ArrayList<Double> list3 = list1; list2.add(4.5); list3.remove(1.5); System.out.println("list1 is " + list1); System.out.println("list2 is " + list2); System.out.println("list3 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 方法克隆一个数组。例如,下面的代码
int[] list1 = {1,2}; int[] list2 = list1.clone(); list1[0] = 7; list2[1] = 8; System.out.println("list1 is " + list1[0] + ", " + list1[1]); System.out.println("list2 is " + list2[0] + ", " + list2[1]);
-
显示
list1 is 7, 2 list2 is 1, 8
-
注意,数组调用clone( )方法的返回类型和该数组的类型是一样的。例如,list1.clone( )的返回类型是int[ ],因为list1 是int[ ]类型的。
-
为了定义一个实现Cloneable 接口的自定义类,这个类必须重写Object 类中的clone( )方法。程序清单13-11 定义一个实现Cloneable 和Comparable 的名为House 的类。
程序清单 13-11 House.java
public class House implements Cloneable, Comparable<House> {
private int id;
private double area;
private java.util.Date whenBuilt;
public House(int id, double area) {
this.id = id;
this.area = area;
whenBuilt = new java.util.Date();
}
public int getId() {
return id;
}
public double getArea() {
return area;
}
public java.util.Date getWhenBuilt() {
return whenBuilt;
}
@Override
/**
* Override the protected clone method defined in
* the Object class , and strengthen its accessibility
*/
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException ex) {
return null;
}
}
//Implement the compareTo method defined in Comparable
@Override
public int compareTo(House o) {
if (area > o.area) {
return 1;
} else if (area < o.area) {
return -1;
} else {
return 0;
}
}
}
-
House 类实现在Object 类中定义的clone 方法(第26 ~ 33 行)。在Object 类中定义的clone 方法头是:
protected native Object clone() throws CloneNotSupportedException;
-
关键字native 表明这个方法不是用Java 写的,但它是 JVM 针对本地平台实现的。关键字protected 限定方法只能在同一个包内或在其子类中访问。由于这个原因,House 类必须重写该方法并将它的可见性修饰符改为public ,这样,该方法就可以在任何一个包中使用。因为Object 类中针对自身平台实现的clone 方法完成了克隆对象的任务,所以,在House 类中的clone 方法只要简单调用super.clone( )即可。如果对象不是Cloneable 类型的,在Object 类中定义的clone 方法会抛出CloneNotSupportedException 异常。由于我们在方法中捕获了该异常(第30 ~ 32 行),就没有必要再clone( ) 方法头部声明异常了。
-
House 类实现了定义在Comparable 接口中的compareTo 方法(第34 ~ 43 行)。该方法比较两个房子的面积。
-
现在,可以创建一个House 类的对象,然后从这个对象创建一个完全一样的拷贝,如下所示:
House house1 = new House(1, 1750, 50); House house2 = (House)house1.clone();
-
house1 和 house2 是两个内容相同的不同对象。Object 类中的clone 方法将原始对象的每个数据域复制给目标对象。如果一个数据域是基本类型,复制的就是它的值。例如,area(double 类型)的值从house1 复制到house2。如果一个数据域是对象,复制的就是该域的引用。例如,域whenBuilt 是 Date 类,所以,它的引用被复制给house2, 如图13-6a所示。因此,尽管house1 == house2 为假,但是house1.whenBuilt == house2.whenBuilt 为真。这称为浅复制(shallow copy)而不是深复制(deep copy),这意味着如果数据域是对象类型,那么复制的是对象的引用,而不是它的内容。
-
如果希望为House 对象执行深复制,将clone( ) 方法中的第26 ~ 33 行替换为下面代码(完整代码参见 liveexample.pearsoncmg.com/text/House.txt):
public Object clone() throws CloneNotSupportedException{ //Perform a shallow copy House houseClone = (House)super.clone(); //Deep copy on whenBuilt houseClone.whenBuilt = (java.util.Date)(whenBuilt.clone()); return houseClone }
-
或者
public Object clone(){ try{ //Perform a shallow copy House houseClone = (House)super.clone(); //Deep copy on whenBuilt houseClone.whenBuilt = (java.util.Date)(whenBuilt.clone()); return houseClone } catch (CloneNotSupportedException ex){ return null; } }
-
现在如果使用下面代码复制一个House 对象:
House house1 = new House(1, 1750, 50); House house2 = (House)house1.clone();
-
House1.whenBuilt == house2.whenBuilt 将为false。house1 和house2 包含两个不同的Date 对象,如图13-6b所示。
-
clone 方法和Cloneable 接口引发了对一些问题的思考。
-
其一,为什么Object类中的clone 方法定义为 protected ,而不是public ?因为不是每个对象都可以被克隆的。如果子类的对象是可克隆的,Java的设计者故意强制子类重写该方法。
-
其二,为什么clone 方法不是定义在Cloneable 接口中呢? 因为 Java 提供了一个本地方法来执行一个浅复制以克隆一个对象。由于接口中的方法是抽象的,该本地方法不能再接口中实现,因此,Java 的设计者决定再Object类中定义和实现本地clone 方法。
-
其三,为什么Object 类不实现Cloneable 接口呢?答案和第一个问题一样。
-
其四,如果程序清单13-11 的第1行中House 类不是先Cloneable ,将会发生什么?house1.clone( ) 将返回null,因为第28行的super.clone( ) 将抛出一个CloneNotSupportedException。
-
其五,可以再House类中实现clone 方法,而不调用Object 类中的clone 方法,如下所示:
public Object clone(){ //Perform a shallow copy House houseClone = new House(id, area); //Deep copy on whenBuilt houseClone.whenBuilt = new Date(); hosueClone.getWhenBuilt().setTime(whenBuilt.getTime()); return houseClone; }
-
这种情况下,House 类没有必要实现Cloneable 接口,并且需要确保所有的数据域都被正确地复制。使用Object 类中的clone( ) 方法可以避免手工复制数据域的麻烦。Object 类中的clone 方法自动对所有的数据域进行浅复制。
13.8 接口与抽象类
- 要点提示: 一个类可以实现多个接口,但是只能继承一个父类。
- 接口的使用和抽象类的使用基本类似,但是,定义一个接口与定义一个抽象类有所不同。表13-2 总结了这些不同点。
变量 | 构造方法 | 方法 | |
---|---|---|---|
抽象类 | 无限制 | 子类通过构造方法链调用构造方法,抽象类不能用new 操作符实例化 | 无限制 |
接口 | 所有的变量必须是public static final | 没有构造方法。接口不能用new 操作符实例化 | 可以包含public 的抽象实例方法、public 的默认方法以及public 的静态方法 |
-
Java 只允许为类的继承做单一继承,但是允许使用接口做多重继承。例如,
public class NewClass extends BaseClass implements Interface1, ... , InterfaceN { ... }
-
利用关键字extends,接口可以继承其他接口。这样的接口称为子接口(subinterface)。例如,在下面代码中,New Interface 是 Interface1,... , InterfaceN 的子接口。
public interface NewInterface extends Interface1, ... , InterfaceN{ //constants and abstract methods }
-
一个实现NewInterface 的类必须实现在NewInterface, Interface1, ... , IntefaceN 中定义的抽象方法。接口可以继承其他接口但不能继承类。一个类可以继承它的父类同时实现多个接口。
-
所有的类共享一个根类Object,但是接口没有共同的根。与类相似,接口也可以定义一种类型。一个接口类型的变量可以引用任何实现该接口的类的实例。如果一个类实现了一个接口,那么这个接口就类似于该类的一个父类。可以将接口当作一种数据类型使用,将接口类型的变量转换为它的子类,反过来也可以。例如,假设c是图13-7 中Class2 的一个实例,那么c也是Object、Class1、Interface1、Interface1_1、Interface1_2、Interface2_1 和 Interface2_2的实例。
-
注意:类名是一个名词。接口名可以是形容词或名词。
-
设计指南:
- 抽象类和接口都是用来指定多个对象的共同特征的。那么如何确定在什么情况下应该使用接口,什么情况下应该使用类呢?一般来说,清晰描述父子关系的强的 "是 ... 的一种" 关系(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"; } } 类鸭延伸动物{ @覆盖 公共字符串howToEat(){ 返回“烤它”; } } ` ` ` +假设给定这个继承体系结构,多态使你可以在一个类型为动物的变量中保存鸡对象或鸭对象的引用,如下面代码所示:` ` `的Java 公共静态无效的主要(String[] args){ Animal Animal = new Chicken(); 吃(动物); 动物 = 新鸭(); 吃(动物); } public static void eat(Animal animal){ System.out.println(animal.howToEat()); } ` ` `
-
JVM 会基于调用方法时所用的确切对象来动态地决定调用哪个howToEat方法。
-
可以定义Animal 的一个子类。但是,这里有个限制条件。该子类必须是另一种动物(例如,Turkey)。另外一个问题产生了:如果一种动物(例如,Tiger)不可食用,那么它继承Animal 类就不合适了。
-
接口没有这种问题。接口比类拥有更多的灵活性,因为不用使所有东西都属于同一个类型的类。可以在接口中定义howToEat( )方法,然后把它当作其他类的共同父类型。例如,
public class DesignDemo { 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) { System.out.println(stuff.howToEat()); } } interface Edible { public String howToEat(); } class Chicken implements Edible { @Override public String howToEat() { return "Fry it"; } } class Duck implements Edible { @Override public String howToEat() { return "Roast it"; } } class Broccoli implements Edible { @Override public String howToEat() { return "Stir - try it"; } }
-
为了定义表示可食用对象的一个类,只须让该类实现Edible 接口即可。现在,这个类就成为Edible 类型的子类型。任何Edible 对象都可以被传递以调用howToEat 方法。
13.9 示例学习:Rational 类
- 要点提示:本节演示如何设计一个Rational 类,用于表示和处理有理数。
- 有理数可以采用分子和分母以形式 a/b 表达,这里的a 是分子而b 是分母。例如,1/3、3/4和10/4 都是有理数。
- 有理数的分母不能为0,但是分子可以为0。每个整数
i
等价于一个有理数 i / 1。有理数用于设计分数的准确计算,例如,1/3 = 0.33333...。这个数组不能用double 或float 数据类型精确地表示为浮点形式。为了获取准确的结果,必须使用有理数。 - Java 提供了表示整数和浮点数的数据类型,但是没有提供表示有理数的数据类型。本节给出如何设计一个表示有理数的类。
- 因为有理数共享了很多整数和浮点数的通用特性,而且Number 是数值包装类的根类,所以将Rational 类定义为Number 类的子类是合适的。因为有理数是可以比较的,所以Rational 类也应该实现Comparable 接口。图13-8 给出Rational 类以及它和Number 类及Comparable 接口的关系。
- 一个有理数包括一个分子和分母。许多有理数是等价的,例如,1/3 = 2/6 = 3/9 = 4/ 12。1/3的分子和分母都除了1之外没有公约数,所以,1/3 称为最简形式。
- 为了将一个有理数约减为它的最简形式,需要找到分子和分母绝对值的最大公约数(GCD),然后将分子和分母都除以这个值。可以使用程序清单5-9 中给出的计算两个整数n 和 d 的GCD 的方法。在Rational 对象中的分子和分母都可以约简它们的最简形式。
- 与往常一样,我们首先编写一个测试程序来创建两个Rational 对象,测试它的方法。程序清大13-12 是一个测试程序。
- main 方法创建了两个有理数: r1 和 r2(第5 和 6行),然后显示r1 + r2、 r1 - r2、 r1 * r2 和 r1 / r2的结果(第9 ~ 12 行)。为了计算r1 + r2,调用r1.add(r2)返回一个新的Rational 对象。同样,r1.subtract(r2)用于计算r1 - r2,r1.multiply(r2)用于计算r1 * r2,而r1.divide(r2) 用于计算r1 / r2。
程序清单 13-12 TestRationalClass.java
public class TestRationalClass {
/**
* Main method
*/
public static void main(String[] args) {
//Create and initialize two rational numbers r1 and r2
Rational r1 = new Rational(4, 2);
Rational r2 = new Rational(2, 3);
//Display results
System.out.println(r1 + " + " + r2 + " = " + r1.add(r2));
System.out.println(r1 + " - " + r2 + " = " + r1.subtract(r2));
System.out.println(r1 + " * " + r2 + " = " + r1.multiply(r2));
System.out.println(r1 + " / " + r2 + " = " + r1.divide(r2));
System.out.println(r1 + " is " + r2.doubleValue());
}
}
- doubleValue( )方法显示r2 的double 值(第13行)。doubleValue( ) 方法在java.lang.Number 中定义并且 在Rational 中被重写。
- 注意,当使用加号(+)将一个字符串和一个对象进行连接时,在这个对象上调用toString( )方法得到的字符串表示用于同这个字符串进行连接。因此,r1 + " + " + r2 + " = " + r1.add(r2) 等价于r1.toString( ) + " + " + r2.toString( ) + " = " + r1.add(r2) .toString( )。
- Rational 类在程序清单13-13 中实现。
程序清单 13-13 Rational.java
public class Rational extends Number implements Comparable<Rational> {
//Data fields for numerator and denominator
private long numerator = 0;
public long denominator = 1;
/**
* Construct a rational with default properties
*/
public Rational() {
this(0, 1);
}
/**
* Construct a rational with specified numerator and denominator
*/
public Rational(long numerator, long denominator) {
long gcd = gcd(numerator, denominator);
this.numerator = (denominator > 0 ? 1 : -1) * numerator / gcd;
this.denominator = Math.abs(denominator) / gcd;
}
/**
* Find GCD of two numbers
*/
public static long gcd(long n, long d) {
long n1 = Math.abs(n);
long n2 = Math.abs(d);
int gcd = 1;
for (int k = 1; k <= n1 && k <= n2; k++) {
if (n1 % k == 0 && n2 % k == 0) {
gcd = k;
}
}
return gcd;
}
/**
* Return numerator
*/
public long getNumerator() {
return numerator;
}
/**
* Return denominator
*/
public long getDenomiator() {
return denominator;
}
/**
* Add a rational number to this rational
*/
public Rational add(Rational secondRational) {
long n = numerator * secondRational.getDenomiator() +
denominator * secondRational.getNumerator();
long d = denominator * secondRational.getDenomiator();
return new Rational(n, d);
}
/**
* Subtract a rational number from this rational
*/
public Rational subtract(Rational secondRadtional) {
long n = numerator * secondRadtional.getDenomiator() - denominator * secondRadtional.getNumerator();
long d = denominator * secondRadtional.getDenomiator();
return new Rational(n, d);
}
/**
* Multiply a rational number by this rational
*/
public Rational multiply(Rational secondRational) {
long n = numerator * secondRational.getNumerator();
long d = denominator * secondRational.getDenomiator();
return new Rational(n, d);
}
/**
* Divide a rational number by this rational
*/
public Rational divide(Rational secondRational) {
long n = numerator * secondRational.getDenomiator();
long d = denominator * secondRational.numerator;
return new Rational(n, d);
}
@Override
public String toString() {
if (denominator == 1) {
return numerator + "";
} else {
return numerator + "/" + denominator;
}
}
//Override the equals method in the OBject class
@Override
public boolean equals(Object other) {
if ((this.subtract((Rational) (other))).getNumerator() == 0) {
return true;
} else {
return false;
}
}
//Implement the abstract intValue method in Number
@Override
public int intValue() {
return (int) doubleValue();
}
//Implement the abstract floatValue method in Number
@Override
public float floatValue() {
return (float) doubleValue();
}
//Implement the abstract floatValue method in Number
@Override
public double doubleValue() {
return numerator * 1.0 / denominator;
}
//Implement the abstract longValue method in Number
@Override
public long longValue() {
return (long) doubleValue();
}
//Implement the compareTo method in Comparable
public int compareTo(Rational o) {
if (this.subtract(o).getNumerator() > 0) {
return 1;
} else if (this.subtract(o).getNumerator() < 0) {
return -1;
} else {
return 0;
}
}
}
-
有理数封装在Rational 对象中。一个有理数内部表示为它的最简形式(第13行),分子决定有理数的符号(第14行)。分母总是正数(第15行)。
-
gcd( ) 方法(Rational 类中的第19 ~ 30 行)是私有的,不能被其他客户程序使用。gcd( ) 方法只能在Rational 类的内部使用。gcd( )方法也是静态的,因为它不依赖于任何一个特定的Rational 对象。
-
abs(x) 方法(Rational 类中的第20和21行)在Math类中定义,并返回x 的绝对值。
-
两个Rational 对象可以相互作用来完成加、减、乘、除操作。这些方法返回一个新的Rational 对象(第43 ~ 70行)。
-
Object 类中的toString 方法 和 equals 方法在Rational 类中被重写(第72 ~ 86 行)。toString( )方法以numerator / denominator (分子 / 分母)的形式返回一个Rational 对象的字符串表示,如果分母为1就将它简化为numerator。如果该有理数和另一个有理数相等,那么方法equals(Object other) 返回值为真。
-
Number 类中的抽象方法 intValue、longValue、floatValue 和 doubleValue 在Rational 类中被实现(第88 ~116行)。这些方法返回该有理数的int、 float 和double 值。
-
Comparable 接口中的compareTo(Object other)方法在Rational 类中被实现(第108 ~ 116 行),用于将该有理数与另一个有理数进行比较。
-
提示:在Rational 类中提供了属性numerator 和denominator 的获取方法,但是没有提供设置方法,因此,一旦创建Rational 对象,那么它的内容就不能改变。Rational 类是不可变的。String 类和基本类型值的包装类也都是不可变的。
-
提示:可以使用两个变量表示分子和分母。也可以使用两个整数构成的数值表示分子和分母(参见编程练习题13.14)。尽管有理数的内部表示改变,但是Rational 类中的公共方法的签名是不变的。这是一个演示类的数据域应该保持私有,以确保将类的实现和类的使用分隔开的很好的例子。
-
Rational 类有严重的局限性,很容易溢出。例如,下面的代码将显示不正确的结果,因为分母太大了。
public class Test{ public static void main(String[] args){ Rational r1 = new Rational (1, 123456789); Rational r2 = new Rational (1, 123456789); Rational r3 = new Rational (1, 123456789); System.out.println("r1 * r2 * r3 is " + r1.multipy(r2.multipy(r3))); } }
-
为了修正这个问题,可以使用BigInteger 表示分子和分母来实现Rational 类(参见编程练习题13.15)./
13.10 类的设计原则
- 要点提示:类的设计原则有卒于设计出合理的类
- 从上面例子以及签前面几章中的其他许多例子中,我们已经学习了如何设计类。本节对一些设计原则进行总结。
13.10.1 内聚性
- 类应该描述一个单一的实体,而所有的类操作应该在逻辑上相互契合来支持一个一致的目的。例如:可以设计一个类用于学生,但不应该将学生与教职工组合在同一个类中,因为学生和教职工是不同的实体。
- 如果一个实体承担太多的职责,就应该按各自的职责分成几个类。例如:String 类、StringBuffer 类和StringBuilder 类都用于处理字符串,但是它们的职责不同。String 类对处理不可变字符串,StringBuilder 类创建可变字符串,StringBuffer 与 StringBuilder 类似,只是StringBuffer 类还包含更新字符串的同步方法。
13.10.2 一致性
- 遵循标准 Java 程序设计风格和命名习惯。为类、数据域和方法选取传递信息的名字。通常的风格是将数据声明置于构造方法之前,并且将构造方法置于普通方法之前。
- 选择名字要保持一致。给类似的操作选择不同的名字并非好的做法。例如:length( ) 方法返回String、StringBuilder 和StringBuffer 的大小。如果在这些类中给这个方法用不同的名字就不一致了。
- 一般来说,应该具有一致性地提供一个公共无参构造方法,用于构建默认实例。如果一个类不支持无参的构造方法,要用文档写下原因。如果没有显式地定义构造方法,则会提供一个具有空方法体的公有默认无参构造方法。
- 如果不想让用户创建类的对象,可以在类中声明一个私有的构造方法,Math 类 和GuessDate 类就是如此。
13.10.3 封装性
- 一个类应该使用
private
修饰符隐藏其数据,以免用户直接访问它。这使得类更易于维护。 - 只在希望数据域可读的情况下,才提供获取方法;也只在希望数据域可更新的情况下,才提供设置方法。例如:Rational 类为numerator 和 denominator 提供了获取方法,但是没有提供设置方法,因为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中,Circle 中的变量numberOfObjects 被 Circle 类的所有对象共享。因此,它被声明为静态的。如果方法不依赖于某个具体的实例,那就应该将它声明为静态方法。例如:Circle 中的getNumberOfObjects( ) 方法没有绑定到任何具体实例,因此,它被声明为静态方法。
- 应该总是使用类名(而不是引用变量)引用静态变量和方法,以增强可读性并避免错误。
- 不要从构造方法中传入参数来初始化静态数据域。最好使用设置方法改变静态数据域。因此,下面图a中的类最好替换为图b。
- 实例和静态是 面向对象程序设计不可或缺的部分。数据域或方法要么是实例的,要么是静态的。不要错误地忽视了静态数据域或方法。常见的设计错误是将本应该声明为静态方法的方法声明为实例方法。例如,用于计算n的阶乘的factorial(int n) 方法应该定义为静态的,因为它不依赖于任何具体实例。
- 构造方法永远都是实力方法,因为它是用来创建具体实例的。一个静态变量或方法可以从实例方法中调用,但是不能从静态方法中调用实例变量或方法。
13.10.7 继承和聚合
- 继承和聚合之间的差异,就是is - a (是一种) 和 has -a (具有) 之间的关系。例如,苹果是一种水果,因此,可以使用继承来对Apple 类和Fruit 类之间的关系进行建模。人具有名字,因此,可以使用聚合来对Person 类 和Name 类 之间的关系建模
13.10.8 接口和抽象类
+接口和抽象类都可以用于为对象指定共同的行为。如何确定是接口还是类呢?例如,因为橘子是一种水果,它们的关系就应该采用类的继承关系来建模。对象拥有自己的属性。弱的 - 一个关系可以使用接口建模。例如,所有字符串的字符串类实现了可比较的接口。圆形或者椭圆形都是一个几何对象,因此Circle可以设计为GeometricObject的子类。圆有不同的半径,并且可以基于半径进行比较,因此Circle可以实现可比的接口接口。+接口比抽象类更加灵活,因为一个子类可以继承一个父类,但是却可以实现任意个数的接口。然而,接口不能包含数据域。Java 8 中,接口可以采用默认方法和方法,这对简化类的设计非常专业。我们将在第 20 章提出此类设计的实例。