铁文整理
6.1 接口
在Java程序设计语言中,接口不是类,而是对类的一组需术描述,这些类要遵从接口描述的统一格式进行定义。
我们经常听到服务提供商这样说:“如果类遵从某个特定接口,那么就履行这项服务”。下面给出一个具体的示例,Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下列前提:对象所属的类必须实现了Comparable接口。
下面是Comparable接口的代码:
public interface Comparable {
int compareTo(Object other);
}
这就是说,任何实现Comparable接口的类都需要包含compareTo方法,并且这个方法的参数必须是一个Object对象,返回一个整型数值。
注释:在Java SE 5.0中,Comparable接口已经改进为泛型类型。
public interface Comparable<T> {
int compareTo(T other); // parameter has type T
}
例如,在实现Comparable<Employee>接口的类中,必须提供下列方法
int compareTo(Empployee other);
也可以使用没有类型参数的“原始”Comparable类型,但必须手工地将compareTo方法的参数转换成所希望的类型。
接口中的所有方法自动地属于public。因此,在Java中声明方法时,不必提供关键字public。
当然,接口中还有一个没有明确说明的附加要求:在调用x.compareTo(y)的时候,这个compareTo方法必须确实比较两个对象的内容,并返回比较的结果。当x小于y时,返回一个负数,当x等于y时,返回0,否则返回一个正数。
上面这个接口只有一个方法,而有些接口可能包含多个方法。稍后可以看到,在接口中还可以定义常量。然而,更为重要的是要知道接口不能提供哪些功能。接口绝不能含有实例域,也不能在接口中实现方法。提供实例域和方法实现的任务应该由实现接口的那个类来完成。因此,可以将接口看成是没有实例域的抽象类。但是这两个概念还是有一定区别的,稍后将给出详细的解释。
现在,假设希望使用Arrays类的sort方法对Employee对象数组进行排序,Employee类就必须实现Comparable接口。
为了让类实现一个接口,通常需要下面两个步骤:
-
将类声明为实现给定的接口。
-
对接口中的所有方法进行定义。
要将类声明为实现某个接口,需要使用关键字implements:
class Employee implements Comparable
当然,这里的Employee类需要提供compareTo方法。假设希望根据雇员的薪水进行比较。如果第一个雇员的薪水低于第二个雇昂的薪水就返回-1;如果相等就返回0;否则返回1。
public int compareTo(Object otherObject) {
Employee other = (Employee) otherObject;
if (salary < other.salary)
return -1;
if (salary > other.salary)
return 1;
return 0;
}
警告:在接口声明中,没有将compareTo方法声明为public,这是因为在接口中的所有方法都自动地是public。不过,在实现接口时,必须把方法声明为public,否则,编译器将认为这个方法的访问属性是包可见的,即类的默认访问属性,之后编译器就会给出试图提供更弱的访问权限的警告信息。
在Java SE 5.0中,可以做得更好一些。可以将上面的实现替换为对Comparable<Employee>接口的实现。
class Employee implements Comparable<Employee> {
public int compareTo(Employee othert) {
if (salary < other.salary)
return -1;
if (salary > other.salary)
return 1;
return 0;
}
}
请注意,将参数Object进行类型转换总是让人感觉不太顺眼,但现在已经不见了。
提示:Comparable接口中的compareTo方法将返回一个整型数值。如果两个对象不相等,则返回一个正值或者一个负值。在对两个整数域进行比较时,这点非常有用。例如,假设每个雇员都有一个唯一整数id,并希望根据ID对雇员进行重新排序,那么就可以返回id-other.id。如果第一个ID小于另一个ID,则返回一个负值;如果两个ID相等,则返回0;否则,返回一个正值。但有一点需要注意:整数的范围不能过大,以避免造成减法运算的溢出。如果能够确信ID为非负整数,或者它们的绝对值不会超过(Integer.MAX_VALUE-1)/2就不会出现问题。
当然,这里的相减技巧不适用于淨点值,因为在salary和other.salary很接近但又不相等的时候,它们的差经过四舍五入后有可能变成0。
现在,我们已经看到,要让一个类使用排序服务必须让它实现compareTo方法。这是理所当然的,因为要向sort方法提供对象的比较方式。但是为什么不能在Employee类直接提供一个compareTo方法,而必须实现Comparable接口呢?
主要原因在于Java程序设计语言是一种强类型语言。在调用方法的时候编译器将会检査这个方法是否存在,在sort方法中可能存在下面这样的语句:
if (a[i].compareTo(a[j])> 0) {
// rearrange a[i] and a[j]
}
为此,编译器必须确认a[i]一定有compareTo方法。如果a是一个Comparable对象的数组,就可以确保拥有compareTo方法,因为每个实现Comparable接口的类都必须提供这个方法的定义。
注释:有人认为,将Arrays类中的sort方法定义为接收一个Comparable[]数组就可以在使用元素类型没有实现Comparable接口的数组作为参数调用sort方法时,由编译器给出错误报告。但事实并非如此。在这祌情况下,sort方法可以接收一个Object[]数组,并对其进行笨拙的类型转换:
// from the stardard library--not recormended
if (((Comparable) a[i]).compareTo(a[j]) > 0) {
// rearrange a[i] and a[j]
}
如果a[i]不属于实现了Comparable接口的类,那么虚拟机就会抛出一个异常。
例6-1给出了实现雇员数组排序的全部代码。
例6-1 EmployeeSortTest.java
import java.util.*;
/**
* This program demonstrates the use of the Comparable interface.
*
* @version 1.30 2004-02-27
* @author Cay Horstmann
*/
public class EmployeeSortTest {
public static void main(String[] args) {
Employee[] staff = new Employee[3];
staff[0] = new Employee("Harry Hacker", 35000);
staff[1] = new Employee("Carl Cracker", 75000);
staff[2] = new Employee("Tony Tester", 38000);
Arrays.sort(staff);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
}
}
class Employee implements Comparable<Employee> {
public Employee(String n, double s) {
name = n;
salary = s;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
/**
* Compares employees by salary
*
* @param other
* another Employee object
* @return a negative value if this employee has a lower salary than
* otherObject, 0 if the salaries are the same, a positive value
* otherwise
*/
public int compareTo(Employee other) {
if (salary < other.salary)
return -1;
if (salary > other.salary)
return 1;
return 0;
}
private String name;
private double salary;
}
API:java.util.Arrays 1.2
-
static void sort(Object[] a):使用mergesort算法对数组aa中的元素进行排序,要求数组中的元素必须属于实现了Comparable接口的类,并且元素之间必须是可比较的。
注释:语言标准规定:对于任意的x和y,实现必须能够保证sgn(x.compareTo(y)) = -sgn(y.compareTo(x))。也就是说,如果y.compareTo(x)抛出一个异常,那么x.compareTo(y)也应该抛出一个异常。这里的sgn是一个数值的符号。
与equals方法一样,在继承过程中有可能会出现问题。
这是因为Manager扩展了Employee,而Employee实现的是Comparable<Employee>,而不是Comparable<Manager>。如果Manager覆盖了compareTo,就必须要有经理与雇员进行比较的思想准备,绝不能仅仅将雇员转换成经理。
class Manager extends Employee {
public int compareTo(Emplayee other) {
Manager otherManager = (Manager) other; // NO
}
}
这不符合反对称的规则。如果x是一个Employee对象,y是一个Manager对象,调用x.compareTo(y)不会抛出异常,它只是将x和y都作为雇员进行比较。但是反过来,y.compareTo(x)将会抛出一个ClassCastException。
这种情况与第5章中讨论的equals方法一样,修改的方式也一样。有两种不同的情况。
如果子类之间的比较含义不一样,那就属于不同类对象的非法比较。每个compareTo方法都应该在开始时进行下列检测:
if (getClass() != other.getC1ass()) throw new ClassCastException();
如果存在这样一种通用算法,它能够对两个不同的子类对象进行比较,则应该超类中提供一个compareTo方法,并将这个方法声明为final。
例如,假设不管薪水的多少都想让经理大于雇员,像Excutive和Secretary这样的子类又该怎么办呢?如果一定要按照职务排列的话,那就应该在Employee类中提供一个rank方法。每个子类覆盖rank,并实现一个比较rank值的compareTo方法。
6.1.1 接口的特性
接口不是类,尤其不能使用new运算符实例化一个接口:
x = new Comparable(...); // ERROR
然而,尽管不能构造接口的对象,却能声明接口的变量:
Comparable x; // 0K
接口变量必须引用实现了接口的类对象:
x = new Employee(...); // OK provided Employee implements Comparable
接下来,如同使用instanceof检查一个对象是否属于某个特定类一样,也可以使用instanceof检査一个对象是否实现了某个特定的接口:
if (anObject instanceof Comparable) { ... }
与可以建立类的继承关系一样,接口也可以被扩展。这里允许存在多条从具有较高通用性的接口到较高专用性的接口的链。例如,假设有一个称为Moveable的接口:
public interface Moveable {
void move(double x, double y);
}
然后,可以以它为基础扩展一个叫做Powered的接口:
public interface Powered extends Moveable {
double milesPerCallon();
}
虽然在接口中不能包含实例域或静态方法,但却可以包含常量。例如:
public interface Powered extends Moveable {
double milesPerCallon();
double SPEED_LIMIT = 95; // a public static final constant
}
与接口中的方法都自动地被设置为public一样,接口中的域将被自动设为public static final。
注释:可以将接口方法标记为pubIic,将域标记为public static final。有些程序员出于习惯或提高清晰度的考虑,愿意这样做。但Java语言规范却建议不要书写这些多余的关键字,本书也采纳了这个建议。
有些接口只定义了常量,而没有定义方法。例如,在标准库中有一个SwingConstants就是这样一个接口,其中只包含NORTH,SOUTH和HORlZONTAL等常量。任何实现SwingConstants接口的类都自动地继承了这些常量,并可以在方法中直接地引用NORTH,而不必采用SwingConstants.NORTH这样的繁琐书写形式。然而,这样应用接口似乎有点偏离了接口概念的初衷,最好不要这样使用它。
尽管每个类只能够拥有一个超类,但却可以实现多个接口。这就为定义类的行为提供了极大的灵活性。例如,Java程序设计语言有一个非常重要的内置接口,称为Cloneable(将在下一节中给予详细的讨论)。如果某个类实现了这个Cloneable接口,Object类中的clone方法就可以创建类对象的一个拷贝。如果希望自己设计的类拥有克隆和比较的能力只要实现这两个接口就可以了。
class Employee implements Cloneable, Comparable
使用逗号将实现的各个接口分隔开。
6.1.2 接口与抽象类
如果阅读了第5章中有关抽象类的内容,那就可能会产生这样一个疑问:为什么Java程序设计语言还要不辞辛苦地引入接口概念?为什么不将Comparable直接设计成如下所示的抽象类。
abstract class Comparable // why not?
{
public abstract int compareTo(Object other);
}
然后,Employee类再直接扩展这个抽象类,并提供compareTo方法的实现:
class Employee implements Comparable // why not?
{
public int compareTo(Object other) { ... }
}
非常遗憾,使用抽象类表示通用属性存在这样一个问题:每个类只能扩展于一个类。假设Employee类已经扩展于一个类,例如Person,它就不能再像下面这样扩展第二个类了:class Employee extends Person, Comparable // ERROR
但每个类可以像下面这样实现多个接口:
class Employee extends Person implements Comparable // OK
有些程序设计语言允许一个类有多个超类,例如C++。我们将此特性称为多继承。而Java的设计者选择了不支持多继承,其主要原因是多继承会让语言本身变得非常复杂(如同C++),效率也会降低(如同Eiffel)。
为了避免这类问题的出现,Java语言利用接口机制来实现多继承的大部分功能。
C++注释:C++具有多继承,随之带来了一些诸如虚基类控制规则和横向指针类型转换等复杂特性。很少有C++程序员使用多继承,甚至有些人说:就不应该使用多继承。也有些程序员建议只对“混合”风格的继承使用多继承。在“混合”风格中,一个主要的基类描述父对象,其他的基类(因此你为混合)扮演辅助的角色。这种风格类似于Java类中从一个基类派生,然后实现若干个辅助接口。然而,在C++中,“混合”类可以忝加默认的行为,而Java的接口则不行。