26 KiB
回忆一下在继承与多态这一章的例子,圆和矩形对象都有一个getArea()
方法和getPerimeter()
方法这两个方法为什么不抽象到父类?其实仔细思考一下,这两个函数应该被抽象到父类,因为所有二维平面的封闭图形(圆,四边形)都有面积和周长。只不过圆、四边形面积和周长的计算方法是不同的。那么有没有什么方式可以只定义函数签名,而让子类去覆盖这个函数,根据动态绑定的方式,可以更好的实现多态的应用。有,这就是抽象类和抽象方法。
1. 抽象类和抽象方法
我们还是以上一章的那个圆和四边形作为例子,只不过需要把getArea()
方法和getPerimeter()
方法放到父类中。
注意上图中斜体的代表是抽象方法,斜体的类是抽象类。具体看看代码如何实现的。
首先是GeometricObject
类的定义:
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();
}
- 注意这个类现在有个新的修饰词叫做 abstract,这个修饰代表这个类是抽象类。抽象类含义是不能实例化的类型。想一想,如果把抽象类作为引用变量的类型,当成能力声明进行多态的使用,抽象类有必要进行实例化吗?它只是能力声明。
getArea()
和getPerimeter()
两个函数前面也有abstract修饰,说明这两个函数是抽象函数,不需要有函数体(具体的实现)。
再看看圆和四边形的代码:
public class Circle extends GeometricObject {
private double radius;
public Circle() {
}
public Circle(double radius) {
this.radius = radius;
}
/** Return radius */
public double getRadius() {
return radius;
}
/** Set a new radius */
public void setRadius(double radius) {
this.radius = radius;
}
/** Return diameter */
public double getDiameter() {
return 2 * radius;
}
@Override /** Return area */
public double getArea() {
return radius * radius * Math.PI;
}
@Override /** 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);
}
}
四边形:
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;
}
/** 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;
}
@Override /** Return area */
public double getArea() {
return width * height;
}
@Override /** Return perimeter */
public double getPerimeter() {
return 2 * (width + height);
}
}
上述两个类都实现(impliment)了(覆盖了)getArea()
和 getPerimeter()
两个函数(这两个函数都有函数体),且这两个类都没有使用 abstract 进行修饰,那么这两个类是可以被实例化的。
看看主函数如何使用多态调用的:
public class TestGeometricObject {
/** Main method */
public static void main(String[] args) {
// Declare and initialize 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());
}
}
equalArea
函数和displayGeometricObject
函数传递的参数都是GeometricObject
类型。注意GeometricObject
类型是抽象类,因为类型隐式转换的原则,GeometricObject
类型的变量可以引用其自生类型(不可能,因为它是抽象类,不能实例化)和其子类型的对象。记住,这里的引用变量类型永远是能力声明。
传递进来的 Circle
l类型和Rectangle
类型的对象,可以在应用变量上面调用getArea()
和getPerimeter()
函数了,这就是抽象类和抽象方法的最核心的作用,其实就是抽象的能力描述。
抽象类的内涵是:
- 实例化我没有任何实际的意义;
- 我只是描述我后代必须具备的能力;
- 后代的某些能力我知道是什么(函数原型),但是我不知道如何实现(算法),因为他们变化太多了;这些能力我就只描述成抽象的方法(只有函数申明,没有函数体)。
- 有些能力我知道如何实现,这些能力就是非抽象的方法。
抽象类不能实例化,但是作为参数,用作多态实现通用编程非常有用!
1.1. 抽象类和抽象方法的特性
1.1.1. 包含抽象方法的类一定是抽象类
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 abstract. In other words, in a nonabstract subclass extended from an abstract class, all the abstract methods must be implemented, even if they are not used in the subclass.
非抽象类一定不能包含抽象方法;如果一个子类没有实现(implement)其超类的所有抽象方法,那么这个类也必须定义成抽象类;换句话,非抽象类一定不包括抽象方法。
这里需要注意:一个子类,拥有其超类(父类、父类的父类,父类的父类的父类...)的所有能力,当然也继承了其所有抽象方法(如果有);因此,其抽象方法可能不直接来自父类(可能还有父类的父类...);要使其成为非抽象类,需要实现其超类的所有抽象方法。
1.1.2. 抽象类不能被实例化
An abstract class cannot be instantiated using the new operator, but you can still define its constructors, which are invoked in the constructors of its subclasses. For instance, the constructors of GeometricObject are invoked in the Circle class and the Rectangle class.
抽象类不能被实例化(不能使用 new 操作符);但是你仍然可以定义其构造函数,其构造函数在子类被实例化的时候被调用;如同
GeometricObject
的构造函数,这需要用到上一章的构造函数链来解释。
1.1.3. 抽象类不一定需要有抽象方法
A class that contains abstract methods must be abstract. However, it is possible to define an abstract class that contains no abstract methods. In this case, you cannot create instances of the class using the new operator. This class is used as a base class for defining a new subclass.
包含抽象方法的类一定是抽象类;但是抽象类不一定包括抽象方法。
1.1.4. 抽象类的父类不一定是抽象类
A subclass can be abstract even if its superclass is concrete. For example, the Object class is concrete, but its subclasses, such as GeometricObject, may be abstract.
抽象类的超类不一定是抽象类。例如,所有类的超类都是Object,而Object是可以实例化的;而
GeometricObject
是直接继承至Object的。
1.1.5. 超类的非抽象方法可以被子类重载成抽象方法
A subclass can override a method from its superclass to define it abstract. This is rare, but useful when the implementation of the method in the superclass becomes invalid in the subclass. In this case, the subclass must be defined abstract.
超类的非抽象方法可以被子类重载成抽象方法,这种方式比较罕见。
1.1.6. 抽象类不能实例化
You cannot create an instance from an abstract class using the new operator, but an abstract class can be used as a data type. Therefore, the following statement, which creates an array whose elements are of GeometricObject type, is correct.
抽象类不能实例化,但是可以作为变量和函数参数的类型;因为引用变量的类型是能力描述。
例如,GeometricObject是抽象类,但是可以作为变量或者是参数类型,作为数组类型也是可以的。
GeometricObject[] geo = new GeometricObject[10];
1.2. Number抽象类
记得前面的封装类型吗?作为数值量的封装类型,其类型转换不能通过类似C的强制类型转换进行,而要通过其实体函数进行。
从根本来说,所有数值类型的封装类型都是 Number 的字类型;而Number是一个抽象类,如上图,定义了类型转换的抽象方法;其子类都实现了这些类型转换的抽象方法。
2. 接口interface
2.1. 为什么需要接口
classDiagram
脯乳动物 <|-- 人
脯乳动物 <|-- 宠物
人 <|-- 男人
人 <|-- 女人
宠物 <|-- 宠物猫
宠物 <|-- 宠物狗
宠物 <|-- 宠物鱼
class 人{
+speak()
}
如上图所示,这些类的继承关系:
- “人”可以作为男人和女人的超类,在“人”中定义一个能力
speak()
,那么其子类型(男人和女人)都具备speak()
这个能力。 - 在宠物这个分支中,现任都不具备说话的能力。
生物一直在进化,有一天一部分猫会说话了,因此分成了两个子类:会说话的宠物猫,不会说话的宠物猫。
classDiagram
脯乳动物 <|-- 人
脯乳动物 <|-- 宠物
人 <|-- 男人
人 <|-- 女人
宠物 <|-- 宠物猫
宠物 <|-- 宠物狗
宠物 <|-- 宠物鱼
宠物猫 <|-- 会说话的宠物猫
宠物猫 <|-- 不会说话的宠物猫
class 人{
+speak()
}
class 会说话的宠物猫{
+speak()
}
如上图所示,显然说话:speak()
这个能力在“人”和“会说话的宠物猫”中都存在;那么如何抽象说话的这种能力?。
再来看看,有一天一部分宠物狗也会说话了,成了以下情况:
classDiagram
脯乳动物 <|-- 人
脯乳动物 <|-- 宠物
人 <|-- 男人
人 <|-- 女人
宠物 <|-- 宠物猫
宠物 <|-- 宠物狗
宠物 <|-- 宠物鱼
宠物猫 <|-- 会说话的宠物猫
宠物猫 <|-- 不会说话的宠物猫
宠物狗 <|-- 会说话的宠物狗
宠物狗 <|-- 不会说话的宠物狗
class 人{
+speak()
}
class 会说话的宠物猫{
+speak()
}
class 会说话的宠物狗{
+speak()
}
这样情况更复杂了,显然说话这种能力是普遍存在的能力,但是好像又不是某个类属所专属的,那如何在跨类属的类中来抽象这种能力?
这个时候就需要用到接口了:
An interface is a class like construct that contains only constants and abstract methods. In many ways, an interface is similar to an abstract class, but the intent of an interface is to specify common behavior for objects. For example, you can specify that the objects are comparable, edible, cloneable using appropriate interfaces.
接口(interface)是一种类型(class),只包含了常量和抽象方法。这样看来,接口和抽象类很相似,但是接口是为对象定义了一种更通用的能力(跨越类属的能力)。你可以为对象(类)定义如下接口:可比较(comparable),可食用(edible),可复制(cloneable)。
在硬件当中,我们也会使用到接口技术,例如计算机和手机都有USB接口,虽然他们看起来不太一样。
手机和计算机实现USB接口的方式不同(可能使用不同的硬件实现),但是只要满足USB接口的标准,这些接口就可以互操作。很显然,接口是用作跨越类属来描述更为普遍但是无法用继承来满足一些能力。
- 如果说话是一种普遍而一般的能力,但确是跨越类属的;你无法把说话的这种能力统一的放在某个超类,让其子类继承;放在脯乳动物中?显然不合适,不是所有的脯乳动物都具备这种特性;
- USB接口也一样,这种能力不属于任何的类属,但是却普遍存在。
2.2. Java中的接口
2.2.1. 定义接口
To distinguish an interface from a class, Java uses the following syntax to define an interface:
在Java中,接口和类的定义不一样,使用interface关键字定义接口,而不是class。
public interface InterfaceName {
constant declarations; // 常量定义
abstract method signatures; // 抽象方法定义
}
// 下面是一个例子
public interface Edible {
/** Describe how to eat */
public abstract String howToEat();
}
An interface is treated like a special class in Java. Each interface is compiled into a separate bytecode file, just like a regular class. Like an abstract class, you cannot create an instance from an interface using the new operator, but in most cases you can use an interface more or less the same way you use an abstract class. For example, you can use an interface as a data type for a variable, as the result of casting, and so on.
接口如同一个特殊的类(class),每个接口被编译成一个对立的字节代码(bytecode)...
接口不能被实例化(使用new关键字),但是可以作为引用变量来引用实现了该接口的类的对象;这种情况下,接口的使用如同抽象类。
注意:一个接口是可以继承和扩展的;但是接口并不像类一样有一个最顶层的超类(Object);因为接口的设计目的就是跨越类属描述更普遍、更通用的能力,因此如同Object这样的顶层超类在接口中是无用的(或者说接口不能有顶层超类的概念,否则就不能跨越类属了)。
2.2.2. 一个例子
下面这个例子十分清楚的展示了抽象类和接口的不同使用场景:
- 定义一个接口Edible(可食用的),其中包含一个抽象函数,返回如何烹饪的字符串描述;显然作为可食用这种能力(特性),在大自然中并不是某个类属所拥有的,而是跨越类属的一般特性;某些植物可以食用,某些动物可以食用;但不是所有的动物或者植物都可以食用。
- 作为动物(这里应该是脯乳动物),可以发声是脯乳动物的的共性,但是发出什么样的声音确是每个种类都不一样。因此定义了一个抽象相类Animal,有个抽象方法:sound。
上图中,虚线箭头表示接口实现,实线表示类继承(扩展)关系。上图中可以看到,水果(Fruit)都是可以食用的,因此水果这个抽象类(斜体表示)实现了可食用的接口;Chicken和Tiger都可以发声,因此由Animal这个抽象类扩展而来,且实现了发生的这个抽象方法;动物中只有Chicken可以食用,因此Chicken还实现了可食用的接口。
在代码中,先看看抽象类Animal:
abstract class Animal {
/** Return animal sound */
public abstract String sound();
}
然后是可食用的接口Edible:
public interface Edible {
/** Describe how to eat */
String howToEat();
}
水果抽象类Fruit:
abstract class Fruit implements Edible {
// Data fields, constructors, and methods omitted here
}
因为Edible接口中的方法都是抽象方法,Fruit存在没有实现的抽象方法,因此Fruit必须是抽象类。
Orange和Apple:
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";
}
}
这两个类由Fruit扩展而来,因此需要实现Edible的接口。
Tiger因为不能食用,由Animal扩展而来,实现Animal的抽象方法sound:
class Tiger extends Animal {
@Override
public String sound() {
return "Tiger: RROOAARR";
}
}
Chicken很特殊,既可以发声,又能食用,因此由Anamal扩展而来,且实现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";
}
}
最后看看主函数是如何调用的:
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) {
Edible e = (Edible) objects[i];
System.out.println(e.howToEat());
}
if (objects[i] instanceof Animal) {
Animal a = (Animal) objects[i];
System.out.println(a.sound());
}
}
}
}
- 数组是一个Object的数组,因为Object是所有类的超类,因此可以存放任何的引用类型;
- 数组中有三个对象,分别是:Tiger、Chicken和Apple;
- 对数组进行遍历,如果对象是 Edible,就把对象转换成Edible类型的引用,然后调用
howToEat()
方法; - 对数组进行遍历,如果对象是Animal,就把对象转换成Animal类型的引用,然后调用
sound()
方法;
最后的输出结果是:
Tiger: RROOAARR
Chicken: Fry it
Chicken: cock-a-doodle-doo
Apple: Make apple cider
注意println函数中的调用 ((Edible) objects[i]).howToEat(),开括号的原则是从左到右,从内层到外层,这个和C的原则是一样的。
- 第一层是:(Edible) objects[i] 把对象强制转换成Edible类型;
- (Edible) objects[i]) 表示的是转换后类型是 Editble的对象;
- 最后是**((Edible) objects[i])**.howToEat(),调用对象的howToEat()方法;
如果不这样做,也可以写成:
Edible edible = (Edible) objects[i];
edible.howToEat();
注意,接口和普通的类型类似,也可以使用 instanceof 操作符判断某个对象是否实现了某个接口。
上述的例子其实表述了接口抽象和类型抽象的全部思想,只不过我们可能还不能熟练的鉴别和使用接口,接下来我们会看到使用接口的例子。
2.2.3. 接口中可以省略public修饰符
All data fields are public final static and all methods are public abstract in an interface. For this reason, these modifiers can be omitted, as shown below:
接口中所有的成员声明(常量和抽象方法)必须是公开的(废话,不公开如何使用接口?),因此public关键字可以省略。
3. 接口的使用
3.1. Comparable接口(理解)
首先来回忆一下在数组那一章学习到的排序函数:
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 + " ");
}
}
java.util.Arrays.sort()
是一个静态函数,可以对数组中的元素进行排序;稍加思考我们会发现,数组内可以存放任何类型的数据(对象),那是不是任意类型都是可以排序?显然不是,排序的基础是可以比较大小,那么好像可以比较大小是对象中普遍存在的特性(跨越类属),但又不是某个类属专有的。这样以来,接口最适合,这个接口就是Comparable接口。
考虑平面中的圆形和方形可以排序吗?如果我们假定是面积比较,就可以排序,下面来看看具体的实现:
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 "Width: " + getWidth() + " Height: " + getHeight() + " Area: " + getArea();
}
}
我们扩展这一章的 Rectangle 成为 ComparableRectangle,可以看到,在接口实现中实现了 Comparable这个接口。注意,这个接口是一个泛型接口,需要在后面的尖括号中输入可以比较大小的类型,当然这里就是 ComparableRectangle类型。
ComparableRectangle 接口只有一个抽象函数,就是 compareTo()函数,其含义是把自己对象和传入对象进行比较,如果自己对象较大,返回1,相等返回0,较小返回-1。在这个函数中我们可以发现是用面积进行比较。好了,目前 ComparableRectangle 这个类的对象是可以比较大小的了,我们来测试一下:
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();
}
}
}
简单吗?如果实现了 Comparable这个接口,就可以使用 java.util.Arrays.sort 函数对其数组进行排序,这就是使用接口实现通用编程的好处。其实在 java.util.Arrays.sort 函数内部,是把数组中的对象转换成 Comparable 接口的引用,然后调用接口的compareTo()实现比较,最终实现排序的。
挑战:如果圆形和方形面积可以参与比较,是不是数组中可以存在圆形和方形的对象,而且也可以排序?答案是肯定的。可以尝试对圆形和方形的混合数组进行排序。提示:最好不要在 圆形和方形的类上实现 Comparable 接口,而应该在其父类实现 Comparable 接口。想一想为什么?
4. 接口和抽象类
In an interface, the data must be constants; an abstract class can have all types of data. Each method in an interface has only a signature without implementation; an abstract class can have concrete methods.
接口的数据必须是常量;抽象类的数据可以是任何形式。接口中只包含抽象方法(没有函数体,只有函数签名);抽象类可以包含任何方法(实现了的方法和抽象方法)。
All classes share a single root, the Object class, but there is no single root for interfaces. Like a class, an interface also defines a type. A variable of an interface type can reference any instance of the class that implements the interface. If a class extends an interface, this interface plays the same role as a superclass. You can use an interface as a data type and cast a variable of an interface type to its subclass, and vice versa.
所有的类(class)共享一个根(Object);但接口没有共享的根。和类一样,接口也被定义成类型。一个接口类型的变量可以引用实现该接口的任何对象。如果一个类实现了某个接口,这个接口的角色类似于超类。接口类型如同类一样可以强制转换转换成子接口,也可以转换成父接口(和类继承一样)。
Suppose that c is an instance of Class2. c is also an instance of Object, Class1, Interface1, Interface1_1, Interface1_2, Interface2_1, and Interface2_2.
- 实线箭头表示类的扩展;虚线街头表示接口的扩展方向;
- 所有类都有一个共同的超类 Object,但是接口没有共同的超接口。
本章重点
除特别标注,其他都是重点,需要掌握。