13.1.1为何需要抽象方法
前面章节中我们定义过这三个类:GeometricObject,Circle和Rectangle,其中Circle和Rectangle都有这两个方法: getArea()和getPerimeter()。
考虑到求面积和求周长应该是几何物体的共性,因此将这两个方法定义在GeometricObject是合适的。但是问题在于, GeometricObject只是一个抽象概念,而求面积和求周长依赖于具体的几何形状,所以这两个方法在GeometricObject中是无法具体实现的。
13.1.2什么是抽象方法
作为概念设计,求面积和求周长的方法在GeometricObject中应该存在;矛盾的是这两个方法又无法具体实现。作为解决方案,Java提供了抽象方法机制,允许对某些方法只定义不实现。
只有定义,没有具体实现的方法叫做抽象方法(abstract methods),这种特殊的方法需要用到 abstract 修饰词,例如:
public abstract double getArea();
public abstract double getPerimeter();
13.2利用抽象方法重新定义GeometricObject类
给GeometricObject添加求面积和求周长两个抽象方法后, GeometricObject类的本身也必须被定义为抽象类。UML图中,斜体表示抽象类或抽象方法,#表示保护成员,+表示public成员。
LISTING 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 get method is named isFilled */
public boolean isFilled() {
return filled;
}
/** Set a new filled */
public void setFilled(boolean filled) {
this.filled = filled;
}
/** Get dateCreated */
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();
}
LISTING 13.2 Circle.java
public class Circle extends GeometricObject {
// Same as lines 3-48 in Listing 11.2, so omitted
}
LISTING 13.3 Rectangle.java
public class Rectangle extends GeometricObject {
// Same as lines 3-51 in Listing 11.3, so omitted
}
LISTING 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 for 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());
}
}
13.3.1抽象类中的抽象方法
1、抽象方法只能在抽象类中被定义。
2、如果父类定义了抽象方法,子类有两种选择:
如果子类不是抽象类,那么子类必须实现父类定义的全部抽象方法,哪怕这个方法对它没什么用处;
如果子类也是抽象类,那么子类可以选择性实现父类定义的抽象方法,甚至一个都不理会也行。
13.3.2抽象类不能用于创建对象
抽象类是不能被 new 出来的,不过你依然得定义它的构造方法,因为子类需要用到父类的构造方法。
例如 GeometricObject 的构造方法就会在 Circle 被new出来的时候被调用,而GeometricObject本身不能被new出来。
13.3.3没有抽象方法的抽象类
如果一个类含有抽象方法,这个类就必须定义为抽象类;不过反之并不成立,也就是说,一个抽象类甚至可以没有定义任何抽象方法,所有的方法都是普通的方法。
没有抽象方法的抽象类也是不能new出来的,这种类可以作为基类被子类所继承。
13.3.4抽象类的超类可以是具体类
当超类是具体类的时候,子类可以是抽象类。例如 Object 类就是具体类,不过它的子类 GeometricObject 就是抽象类。
13.3.5具体方法可以被抽象方法所覆盖
子类可以覆盖超类中定义的具体方法,并将其改为抽象。这种情况很少见,但是至少在一个地方会用到:子类无法实现父类定义的方法。这种情况下子类只能定义为抽象类,然后用抽象方法覆盖此方法,等待子类的子类去实现。
13.3.6将抽象类用作数据类型
抽象类是不能new的,但是不意味着抽象类没有用,至少它可以用作数据类型。例如可以建立抽象类 GeometricObject 的数组(注意这个new是创建数组用):
GeometricObject[] geo = new GeometricObject[10];
13.2.2 Interesting Points about Abstract Classes
The following points about abstract classes are worth noting:
■ An abstract method cannot be contained in a nonabstract class. If a subclass of an abstract superclass does not implement all the abstract methods, the subclass must be defined as abstract. In other words, in a nonabstract subclass extended from an abstract class, all the abstract methods must be implemented. Also note that abstract methods are nonstatic.
■ An abstract class cannot be instantiated using thenew operator, but you can still define its constructors, which are invoked in the constructors of its subclasses. For instance, the constructors ofGeometricObject are invoked in theCircle class and theRectangle class.
■ A class that contains abstract methods must be abstract. However, it is possible to define an abstract class that doesn’t contain any abstract methods. In this case, you cannot create instances of the class using thenew operator. This class is used as a base class for defining subclasses.
■ A subclass can override a method from its superclass to define it as abstract. This is very unusual, but it is useful when the implementation of the method in the superclass becomes invalid in the subclass. In this case, the subclass must be defined as abstract.
■ A subclass can be abstract even if its superclass is concrete. For example, theObject class is concrete, but its subclasses, such asGeometricObject, may be abstract.
■ You cannot create an instance from an abstract class using thenew operator, but an abstract class can be used as a data type. Therefore, the following statement, which creates an array whose elements are of the GeometricObjecttype, is correct. GeometricObject[] objects =new GeometricObject[10]; You can then create an instance of GeometricObjectand assign its reference to the array like this: objects[0] = new Circle();
13.3 Number抽象类
Number抽象类定义了与数值有关的一组抽象方法,这个类是所有数值包装类以及BigInteger, BigDecimal的超类,其继承关系如图。注意类似intValue() 的方法,在Number中是无法实现的,所以只能是抽象的。
一个例子:Number虽然是抽象的,但可以当作数据类型使用。
LISTING 13.5 LargestNumbers.java
import java.util.ArrayList;
import java.math.*;
public class LargestNumbers {
public static void main(String[] args) {
ArrayList<Number> list = new ArrayList<>();
list.add(45); // Add an integer
list.add(3445.53); // Add a double
// 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())
number = list.get(i);
return number;
}
}
The largest number is 3432323234344343101
13.4抽象类Calendar及其子类 GregorianCalendar
13.5 Date类,Calendar类与GregorianCalendar类
java.util.Date能表示一个时间,精度毫秒,但是不方便使用(例如不能直接获取年份)。
java.util.Calendar是一个表示具体时间信息的抽象类,定义了从Date对象读出年份、月份、日期等具体信息的方法(但未实现)。
java.util.GregorianCalendar是Calendar的子类,具体实现了Calendar定义的功能,所以要操作日期的话,你需要把这个类new出来。
13.6 Calendar定义的一些get方法
Calendar类定义的get(int field)方法用于从日期抽取具体信息,field具体取值可以是:
import java.util.*;
public class TestCalendar {
public static void main(String[] args) {
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("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));
}
}
注意new那行代码,calendar的类型可以用Calendar,当然使用GregorianCalendar也行。
13.7接口
接口是一种特殊的抽象类,这种类只含有常量和抽象方法。很多时候,接口和一个抽象类差不多,但是接口的主要作用在于指定对象的特征。
例如,借助接口,你可以指定对象是可比较的,可吃的,可克隆的等等。抽象类没有这种功能。
13.8定义一个接口
接口的语法如下:
public interface InterfaceName {
constant declarations;
method signatures;
}
例子:
public interface Edible {
/** Describe how to eat */
public abstract String howToEat();
}
13.9.1接口是一种特殊类
1、Java将接口当作一种特殊类,所以和类一样,每一个接口同样会被单独编译成字节码文件。
2、和抽象类一样,接口不能直接new出来,不过可以用作数据类型。
3、接口有两种用途:被其它类实现(implements)或被其它接口继承(extends)
13.9.2接口的成员可以不写修饰符
由于接口中定义的属性都只能是 public final static ,接口中定义的方法都只能是 public abstract ,所以在接口中这些修饰符可以省略不写:
接口中的常量可以使用如下语法访问:InterfaceName.CONSTANT_NAME (例如 T1.K)
13.9.3一个接口的例子
先设计一个Edible 的接口和以下几个类:Animal,Chicken,Tiger,Fruit,Apple,Orange,它们之间的关系如下:
13.9.4 Edible接口和Animal、Fruit抽象类定义
interface Edible {
/** Describe how to eat */
public abstract String howToEat();
}
abstract class Animal {
/** Return animal sound */
public abstract String sound();
}
abstract class Fruit implements Edible {
// Data fields, constructors, and methods omitted here
}
Chicken从Animal继承,并实现了Edible接口
class Chicken extends Animal implements Edible {
@Override
public String howToEat() {
return "Chicken: Fry it";
}
@Override
public String sound() {
return "Chicken: cock-a-doodle-doo";
}
}
Tiger从Animal继承,没有实现Edible接口
class Tiger extends Animal {
@Override
public String sound() {
return "Tiger: RROOAARR";
}
}
Apple、Orange从Fruit继承,因为Fruit规定必须实现Edible接口,所以Apple、Orange*实现了Edible接口
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";
}
}
测试程序
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());
}
}
}
13.9.5再来一个例子:Comparable 接口
// This interface is defined in
// java.lang package
package java.lang;
public interface Comparable<E> {
public int compareTo(E o);
}
约定compareTo如果返回正值,表示对象本身大于传入的对象o,0表示二者相等,负值表示对象本身小于传入的对象o。
数值类、字符串类和日期类显然是可以比较大小的,所以它们全都实现了Comparable 接口
13.9.6数值类、字符串类、日期类比较大小的例子
System.out.println(new Integer(3).compareTo(new Integer(5)));
System.out.println("ABC".compareTo("ABE"));
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
-2
1
import java.math.*;
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 + " ");
}
}
如果要让自定义对象也能使用sort,只要实现Comparable接口即可。
LISTING 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);
}
@Override // Implement the compareTo method defined in Comparable
public int compareTo(ComparableRectangle o) {
if (getArea() > o.getArea())
return 1;
else if (getArea() < o.getArea())
return -1;
else
return 0;
}
@Override // Implement the toString method in GeometricObject
public String toString() {
return super.toString() + " Area: " + getArea();
}
}
LISTING 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();
}
}
}
Width: 3.4 Height: 5.4 Area: 18.36
Width: 1.4 Height: 25.4 Area: 35.559999999999995
Width: 7.4 Height: 35.4 Area: 261.96
Width: 13.24 Height: 55.4 Area: 733.496
13.10.1 Cloneable接口
标识类接口(Marker Interface)是一种空接口,没有定义任何需要实现的方法。典型的是Cloneable接口,其定义如下:
package java.lang;
public interface Cloneable {
//接口体为空,没有定义任何成员
}
任何一个实现了Cloneable接口的类,相当于宣称自身具有可克隆的功能,可以直接调用clone()方法克隆出一个与自身完全相同的新对象。clone()方法是在Object类定义的,与Cloneable接口没有关系。
13.10.2 Date和Calendar类
许多Java类实现了Cloneable接口,例如Date和Calendar,下面是它们克隆自身的例子:
Calendar calendar = newGregorianCalendar(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
In the preceding code, line 2 copies the reference ofcalendar tocalendar1, socalendar andcalendar1 point to the sameCalendar object. Line 3 creates a new object that is the clone ofcalendar and assigns the new object’s reference tocalendar2.calendar2 and calendarare different objects with the same contents.
The following code
1 ArrayList<Double> list1 = newArrayList<>();
2 list1.add(1.5);
3 list1.add(2.5);
4 list1.add(3.5);
5 ArrayList<Double> list2 = (ArrayList<Double>)list1.clone();
6 ArrayList<Double> 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("list3 is "+ list3);
displays
list1 is [2.5, 3.5]
list2 is [1.5, 2.5, 3.5, 4.5]
list3 is [2.5, 3.5]
In the preceding code, line 5 creates a new object that is the clone oflist1 and assigns the new object’s reference tolist2.list2 andlist1 are different objects with the same contents.
Line 6 copies the reference of list1to list3, solist1 andlist3 point to the same ArrayListobject. Line 7 adds 4.5into list2. Line 8 removes1.5 fromlist3. Since list1and list3point to the same ArrayList, line 9 and 11 display the same content.
You can clone an array using the clonemethod. For example, the following code
13.10.3克隆数组的例子
1 int[] list1 = {1,2};
2 int[] list2 = list1.clone();
3 list1[0] =7;
4 list2[1] =8;
5 System.out.println("list1 is "+ list1[0] +", " + list1[1]);
6 System.out.println("list2 is "+ list2[0] +", " + list2[1]);
displays
list1 is 7,2
list2 is 1,8
13.10.4例子
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() throws CloneNotSupportedException {
return super.clone();
}
/* 此处覆盖了clone()方法,提升了访问权限,并调用了父类的clone()方法。原定义如下:
protected native Object clone() throws CloneNotSupportedException; */
@Override
//Implement the compareTo method defined in Comparable
public int compareTo(House o) {
if (area > o.area)
return 1;
else if (area < o.area)
return -1;
else
return 0;
}
}
The Houseclass implements the clonemethod (lines 26–28) defined in the Object class.
The header is:
protected native Object clone()throws CloneNotSupportedException;
The keyword nativeindicates that this method is not written in Java but is implemented in the JVM for the native platform. The keywordprotected restricts the method to be accessed in the same package or in a subclass. For this reason, the House class must overrid the method and change the visibility modifier topublic so that the method can be used in any package. Since theclone method implemented for the native platform in the Objectclass performs the task of cloning objects, theclone method in theHouse class simply invokessuper.clone(). Theclone method defined in theObject class may throw CloneNotSupportedException.
The Houseclass implements the compareTomethod (lines 31–38) defined in the Comparableinterface. The method compares the areas of two houses. You can now create an object of theHouse class and create an identical copy from it, as follows:
House house1 = newHouse(1,1750.50);
House house2 = (House)house1.clone();
house1 andhouse2 are two different objects with identical contents. Theclone method in theObject class copies each field from the original object to the target object. If the field is of a primitive type, its value is copied. For example, the value of area(doubletype) is copied fromhouse1 tohouse2. If the field is of an object, the reference of the field is copied. For example, the fieldwhenBuilt is of theDate class, so its reference is copied intohouse2, as shown in Figure 13.6. Therefore,house1.whenBuilt == house2.whenBuilt is true, althoughhouse1 == house2 is false. This is referred to as ashallow copy rather than a deep copy, meaning that if the field is of an object type, the object’s reference is copied rather than its contents.
13.11.1浅拷贝(Shallow Copy)和深拷贝(Deep Copy)
House house1 = new House(1, 1750.50);
House house2 = (House)house1.clone();
解释一下
clone方法会将所有的数据域从原对象复制到新对象。如果数据域是基本数据类型,直接复制原始值;如果是引用数据类型,直接复制原引用。这种做法叫做浅拷贝。
上例中,double型的area的值直接从house1复制到house2,Date型的whenBuilt的引用被复制到house2,于是:
house1.whenBuilt == house2.whenBuilt为true
house1 == house2为false
To perform a deep copy for a Houseobject, replace the clone()method in lines 26–28 with the following code:
13.11.2 House的深拷贝版本
@Override
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;
}
Or
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;
}
}
Now if you clone a Houseobject in the following code:
House house1 = newHouse(1,1750.50);
House house2 = (House)house1.clone();
house1.whenBuilt ==house2.whenBuilt will befalse.house1 andhouse2 contain
two different Dateobjects, as shown in Figure 13.6b.
13.12.1接口和抽象类的对比
|
成员变量 |
构造方法 |
成员方法 |
抽象类 |
无限制 |
构造方法由子类的构造方法链自动调用。抽象类不能使用new创建实例。 |
无限制 |
接口 |
必须为public static final |
无构造方法。接口不能使用new创建实例。 |
必须为 public abstract |
Java只允许单一继承,所以抽象类只能从一个超类继承;但是Java允许一个类同时实现多个接口,例如:
public class NewClass extends BaseClass Implements Interface1, . . ., InterfaceN {
. . .
}
一个接口可以从多个接口继承,例如:
public interface NewInterface extends Interface1, . . . , InterfaceN {
// constants and abstract methods
}
13.12.2例子
如下图,假设c是Class2的实例,那么c同时也是Object, Class1, Interface1, Interface1_1, Interface1_2, Interface2_1, 和Interface2_2的实例。
In general, interfaces are preferred over abstract classes because an interface can define a common supertype for unrelated classes. Interfaces are more flexible than classes. Consider theAnimal class. Suppose thehowToEat method is defined in theAnimal class, as follows:
public static void eat(Edible stuff) {
stuff.howToEat();
}
interface Edible {
public String howToEat();
}
class Chickenimplements Edible {
@Override
public String howToEat() {
return "Fry it";
}
}
class Duckimplements Edible {
@Override
public String howToEat() {
return "Roast it";
}
}
class Broccoliimplements Edible {
@Override
public String howToEat() {
return "Stir-fry it";
}
}
To define a class that represents edible objects, simply let the class implement theEdible interface. The class is now a subtype of theEdible type, and anyEdible object can be passed to invoke thehowToEa method.
abstract class Animal {
public abstract String howToEat();
}
Two subclasses of Animal are defined as follows:
class Chickenextends Animal {
@Override
public String howToEat() {
return "Fry it";
}
}
class Duckextends Animal {
@Override
public String howToEat() {
return "Roast it";
}
}
Given this inheritance hierarchy, polymorphism enables you to hold a reference to aChicken object or aDuck object in a variable of typeAnimal, as in the following code:
public static void main(String[] args) {
Animal animal = newChicken();
eat(animal);
animal = newDuck();
eat(animal);
}
public static void eat(Animal animal) {
animal.howToEat();
}
The JVM dynamically decides which howToEatmethod to invoke based on the actual object that invokes the method.
You can define a subclass of Animal. However, there is a restriction: The subclass must be for another animal (e.g., Turkey).
Interfaces don’t have this restriction. Interfaces give you more flexibility than classes, because you don’t have to make everything fit into one type of class. You may define the howToEat()method in an interface and let it serve as a common supertype for other classes. For example,
public static void main(String[] args) {
Edible stuff = newChicken();
eat(stuff);
stuff = newDuck();
eat(stuff);
stuff =new Broccoli();
eat(stuff);
}
13.13抽象类还是接口?
一般而言,一种强的is-a联系能够明确表达出父子关系。例如staff是一个person,所以这种联系适合用类继承来表达;
一种弱的is-a联系,或者是is-kind-of联系,仅仅表明对象拥有某种属性,这种情况下用接口更加合适。例如,所有的字符串都是可比较的,所以String类实现了Comparable接口。再例如Edible,作为接口是合适的;但是用作抽象类的话,为了表明鸭子是可吃的,你只能写出下面这个难以理解的代码了:
class Duck extends Edible { … } //显然Edible不适合作为超类
接口的另一个好处就是能够绕开Java的单一继承限制,间接实现多继承——因为一个类可以同时继承多个接口。
13.14 Case Study: The Rational Class
This section shows how to design the Rationalclass for representing and processing rational numbers.
A rational number has a numerator and a denominator in the forma/b, wherea is the numerator andb the denominator. For example,1/3,3/4, and10/4 are rational numbers. A rational number cannot have a denominator of0, but a numerator of0 is fine. Every integer iis equivalent to a rational number i/1. Rational numbers are used in exact computations involving fractions—for example,1/3 = 0.33333. . . . This number cannot be precisely represented in floating-point format using either the data type double orfloat. To obtain the exact result, we must use rational numbers.
A rational number consists of a numerator and a denominator. There are many equivalent rational numbers—for example,1/3 = 2/6 = 3/9 = 4/12. The numerator and the denominator of1/3 have no common divisor except1, so1/3 is said to be inlowest terms.
To reduce a rational number to its lowest terms, you need to find the greatest common divisor (GCD) of the absolute values of its numerator and denominator, then divide both the numerator and denominator by this value. You can use the method for computing the GCD of two integersn andd, as suggested in Listing 5.9, GreatestCommonDivisor.java. The numerator and denominator in aRational object are reduced to their lowest terms. As usual, let us first write a test program to create two Rational objects and test its methods.
LISTING 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(r2 + " is " + r2.doubleValue());
}
}
LISTING 13.13 Rational.java
public class Rational extends Number implements Comparable<Rational> {
// Data fields for numerator and denominator
private long numerator = 0;
private 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 */
private 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 getDenominator() {
return denominator;
}
/** Add a rational number to this rational */
public Rational add(Rational secondRational) {
long n = numerator * secondRational.getDenominator() + denominator * secondRational.getNumerator();
long d = denominator * secondRational.getDenominator();
return new Rational(n, d);
}
/** Subtract a rational number from this rational */
public Rational subtract(Rational secondRational) {
long n = numerator * secondRational.getDenominator() - denominator * secondRational.getNumerator();
long d = denominator * secondRational.getDenominator();
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.getDenominator();
return new Rational(n, d);
}
/** Divide a rational number by this rational */
public Rational divide(Rational secondRational) {
long n = numerator * secondRational.getDenominator();
long d = denominator * secondRational.numerator;
return new Rational(n, d);
}
@Override
public String toString() {
if (denominator == 1)
return numerator + "";
else
return numerator + "/" + denominator;
}
@Override // Override the equals method in the Object class
public boolean equals(Object other) {
if ((this.subtract((Rational)(other))).getNumerator() == 0)
return true;
else
return false;
}
@Override // Implement the abstract intValue method in Number
public int intValue() {
return (int)doubleValue();
}
@Override // Implement the abstract floatValue method in Number
public float floatValue() {
return (float)doubleValue();
}
@Override // Implement the doubleValue method in Number
public double doubleValue() {
return numerator * 1.0 / denominator;
}
@Override // Implement the abstract longValue method in Number
public long longValue() {
return (long)doubleValue();
}
@Override // 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;
}
}
The Rational class has serious limitations and can easily overflow. For example, the following code will display an incorrect result, because the denominator is too large.
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.multiply(r2.multiply(r3)));
}
}
r1 * r2 * r3 is -1/2204193661661244627
13.15 Class Design Guidelines
Class design guidelines are helpful for designing sound classes.
13.15.1 Cohesion
A class should describe a single entity, and all the class operations should logically fit together to support a coherent purpose. You can use a class for students, for example, but you should not combine students and staff in the same class, because students and staff are different entities.
A single entity with many responsibilities can be broken into several classes to separate the responsibilities. The classesString,StringBuilder, andStringBuffer all deal with strings, for example, but have different responsibilities.
The Stringclass deals with immutable strings, theStringBuilder class is for creating mutable strings, and theStringBuffer class is similar toStringBuilder except thatStringBuffer contains synchronized methods for updating strings.
13.15.2 Consistency
Follow standard Java programming style and naming conventions.
Choose informative names for classes, data fields, and methods. A popular style is to place the data declaration before the constructor and place constructors before methods.
Make the names consistent. It is not a good practice to choose different names for similar operations. For example, thelength() method returns the size of aString, a StringBuilder, and aStringBuffer. It would be inconsistent if different names were used for this method in these classes.
In general, you should consistently provide a public no-arg constructor for constructing a default instance.
If a class does not support a no-arg constructor, document the reason. If no constructors are defined explicitly, a public default no-arg constructor with an empty body is assumed. If you want to prevent users from creating an object for a class, you can declare a private constructor in the class, as is the case for the Mathclass.
13.15.3 Encapsulation
A class should use the privatemodifier to hide its data from direct access by clients. This makes the class easy to maintain.
Provide a getter method only if you want the data field to be readable, and provide a setter method only if you want the data field to be updateable. For example, the Rational class provides a getter method fornumerator anddenominator, but no setter method, because a Rationalobject is immutable.
13.15.4 Clarity
Cohesion, consistency, and encapsulation are good guidelines for achieving design clarity.
Additionally, a class should have a clear contract that is easy to explain and easy to understand. Users can incorporate classes in many different combinations, orders, and environments.
Therefore, you should design a class that imposes no restrictions on how or when the user can use it, design the properties in a way that lets the user set them in any order and with any combination of values, and design methods that function independently of their order of occurrence. For example, the Loan class contains the propertiesloanAmount,numberOfYears, and annualInterestRate. The values of these properties can be set in any order.
Methods should be defined intuitively without causing confusion. For example, the substring(int beginIndex, int endIndex)method in the Stringclass is somewhat confusing. The method returns a substring frombeginIndex toendIndex – 1, rather than to endIndex. It would be more intuitive to return a substring from beginIndexto endIndex.
You should not declare a data field that can be derived from other data fields. For example, the followingPerson class has two data fields:birthDate andage. Sinceage can be derived frombirthDate,age should not be declared as a data field.
public class Person {
private java.util.Date birthDate;
private int age;
...
}
13.15.5 Completeness
Classes are designed for use by many different customers. In order to be useful in a wide range of applications, a class should provide a variety of ways for customization through properties and methods. For example, theString class contains more than 40 methods that are useful for a variety of applications.
13.15.6 Instance vs. Static
A variable or method that is dependent on a specific instance of the class must be an instance variable or method. A variable that is shared by all the instances of a class should be declared static. For example, the variablenumberOfObjects inCircleWithPrivateDataFields in Listing 9.8 is shared by all the objects of theCircleWithPrivateDataFields class and therefore is declared static. A method that is not dependent on a specific instance should be defined as a static method. For instance, thegetNumberOfObjects() method inCircleWithPrivateDataFields is not tied to any specific instance and therefore is defined as a static method.
Always reference static variables and methods from a class name (rather than a reference variable) to improve readability and avoid errors. Do not pass a parameter from a constructor to initialize a static data field. It is better to use a setter method to change the static data field. Thus, the following class in (a) is better replaced by (b).
Instance and static are integral parts of object-oriented programming. A data field or method is either instance or static. Do not mistakenly overlook static data fields or methods.
It is a common design error to define an instance method that should have been static. For example, thefactorial(int n) method for computing the factorial ofn should be defined static, because it is independent of any specific instance. A constructor is always instance, because it is used to create a specific instance. A static variable or method can be invoked from an instance method, but an instance variable or method cannot be invoked from a static method.
13.15.7 Inheritance vs. Aggregation
The difference between inheritance and aggregation is the difference between an is-a and a has-a relationship. For example, an apple is a fruit; thus, you would use inheritance to model the relationship between the classesApple andFruit. A person has a name; thus, you would use aggregation to model the relationship between the classesPerson andName.
13.15.8 Interfaces vs. Abstract Classes
Both interfaces and abstract classes can be used to specify common behavior for objects. How do you decide whether to use an interface or a class? In general, a strong is-a relationship that clearly describes a parent–child relationship should be modeled using classes. For example, since an orange is a fruit, their relationship should be modeled using class inheritance.
A weak is-a relationship, also known as an is-kind-of relationship, indicates that an object possesses a certain property. A weak is-a relationship can be modeled using interfaces. For example, all strings are comparable, so theString class implements theComparable interface.
A circle or a rectangle is a geometric object, so Circle can be designed as a subclass ofGeometricObject. Circles are different and comparable based on their radii, soCircle can implement theComparable interface.
Interfaces are more flexible than abstract classes, because a subclass can extend only one superclass but can implement any number of interfaces. However, interfaces cannot contain concrete methods. The virtues of interfaces and abstract classes can be combined by creating an interface with an abstract class that implements it. Then you can use the interface or the abstract class, whichever is convenient. We will give examples of this type of design in Chapter 20, Lists, Stacks, Queues, and Priority Queues.
CHAPTER 13 SUMMARY
1. Abstract classesare like regular classes with data and methods, but you cannot creat instances of abstract classes using thenew operator.
2. Anabstract method cannot be contained in a nonabstract class. If a subclass of a abstract superclass does not implement all the inherited abstract methods of the superclass the subclass must be defined as abstract.
3. A class that contains abstract methods must be abstract. However, it is possible to defin an abstract class that doesn’t contain any abstract methods.
4. A subclass can be abstract even if its superclass is concrete.
5. Aninterface is a class-like construct that contains only constants and abstract methods. In many ways, an interface is similar to an abstract class, but an abstract class can contain constants and abstract methods as well as variables and concrete methods.
6. An interface is treated like a special class in Java. Each interface is compiled into a separate bytecode file, just like a regular class.
7. Thejava.lang.Comparable interface defines thecompareTo method. Many classes in the Java library implementComparable.
8. Thejava.lang.Cloneable interface is amarker interface. An object of the class that implements theCloneable interface is cloneable.
9. A class can extend only one superclass but can implement one or more interfaces.
10. An interface can extend one or more interfaces.