目录
十八、接口优先于抽象类
十九、接口只用于定义类型
二十、类层次优于标签类
二十一、用函数对象表示策略
二十二、优先考虑静态成员类
十八、接口优先于抽象类
Java是不支持多重继承但是可以实现多个接口的,而这也恰恰成为了接口优于抽象类的一个重要因素。他们的主要差异如下:
1.现有的类可以很容易被更新,以实现新的接口。如果现存的类并不具备某些功能,如比较和序列化,那么我们可以直接修改该类的定义分别实现Comparable和Serializable接口。倘若Comparable和Serializable不是接口而是抽象类,那么同时继承两个抽象类是Java语法规则所不允许的,如果当前类已经继承自某个超类了,那么他将无法再扩展任何新的超类。
2.接口是定义mixin(混合类型)的理想选择。Comparable是一个典型的mixin接口,他允许类表明他的实例可以与其他的可相互比较的对象进行排序。这样的接口之所以被称为mixin,是因为他允许任选的功能可被混合到类型的主要功能中。抽象类不能被用于定义mixin,同样也是因为他们不能被更新到现有的类中:类不可能有一个以上的超类,类层次结构中也没有适当的地方来插入mixin。
3.接口允许我们构造非层次结构的类型框架。由于我们可以为任何已有类添加新的接口,而无需考虑他当前所在框架中的类层次关系,这样便给功能的扩展带来了极大的灵活性,也减少了对已有类层次的冲击。如:
public interface Singer { //歌唱家
AudioClip sing(Song s);
}
public interface SongWriter { //作曲家
Song compose(boolean hit);
}
在现实生活中,有些歌唱家本身也是作曲家。因为我们这里是通过接口来定义这两个角色的,所有同时实现他们是完全可能的。甚至可以再提供一个接口扩展自这两个接口,并提供新的方法,如:
public interface SingerWriter extends Singer, SongWriter {
AudioClip strum();
void actSensitive();
}
通过对导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。骨架实现被称为AbstractInterface,例如,Collections Framework为每个重要的集合接口都提供了一个骨架实现,包括AbstractCollection、AbstractSet、AbstractList和AbstractMap。如果设计得当,骨架实现可以使程序员很容易的提供他们自己的接口实现。这种组合还可以让我们在设计自己的类时,根据实际情况选择是直接实现接口,还是扩展该抽象类。和接口相比,骨架实现类还存在一个非常明显的优势,既如果今后为该骨架实现类提供新的方法,并提供了默认的实现,那么他的所有子类均不会受到影响,而接口则不同,由于接口不能提供任何方法实现,因此他所有的实现类必须进行修改,为接口中新增的方法提供自己的实现,否则将无法通过编译。而且一般而言,要想在公有接口中增加方法,而不破坏实现这个接口的所有现有的类,这是不可能的。
简而言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性更为重要的时候。在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。
十九、接口只用于定义类型
当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的定义接口是不恰当的。
有一种接口被称为常量接口,它不满足上面的条件,这种接口没有包含任何方法,它只包含静态的final域,每个域都导出一个常量。常量接口模式是对接口的不良使用,会给实现了这个接口的类带来不好的影响,应该杜绝使用。
如果要导出常量,应该把这些常量添加到与其紧密相关的类或接口中。如果这些常量最好被看作枚举类型的成员,就应该用枚举类型来导出这些常量。否则应该使用不可实例化的工具类:
public class PhysicalConstants {
private PhysicalConstants() {}
public static final double AVOGADROS_NUMBER = 6.02214199e23;
public static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
public static final double ELECTRON_MASS = 9.10938188e-31;
}
二十、类层次优于标签类
这是标签类的示例代码:
class Figure {
enum Shape { RECT,CIRCLE };
final Shape s; //标签域字段,标识当前Figure对象的实际类型RECT或CIRCLE。
double length; //length和width均为RECT形状的专有域字段
double width;
double radius; //radius是CIRCLE的专有域字段
Figure(double radius) { //专为生成CIRCLE对象的构造函数
s = Shape.CIRCLE;
this.radius = radius;
}
Figure(double length,double width) { //专为生成RECT对象的构造函数
s = Shape.RECT;
this.length = length;
this.width = width;
}
double area() {
switch (s) { //存在大量的case判断来确定实际的对象类型。
case RECT:
return length * width
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError();
}
}
}
像Figure这样的类通常被我们定义为标签类,他实际包含多个不同类的逻辑,其中每个类都有自己专有的域字段和类型标识,然而他们又都同属于一个标签类,因此被混乱的定义在一起。在执行真正的功能逻辑时,如area(),他们又不得不通过case语句再重新进行划分。现在我们总结一下标签类将会给我们的程序带来哪些负面影响。1.不同类型实例要求的域字段被定义在同一个类中,不仅显得混乱,而且在构造新对象实例时,也会加大内存的开销。
2.初始化不统一,从上面的代码中已经可以看出,在专为创建CIRCLE对象的构造函数中,并没有提供length和width的初始化功能,而是借助了JVM的缺省初始化。这样会给程序今后的运行带来潜在的失败风险。
3.由于没有在构造函数中初始化所有的域字段,因此不能将所有的域字段定义为final的,这样该类将有可能成为可变类。
4.大量的swtich--case语句,在今后添加新类型的时候,不得不修改area方法,这样便会引发因误修改而造成错误的风险。
如何解决这样的问题呢?已经有了明确的答案:利用Java语句提供的继承功能。见下面的代码:
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius) {
this.radius = radius;
}
double area() {
return Math.PI * (radius * radius);
}
}
class Rectangle extends Figure {
final double length;
final double width;
Rectangle(double length,double width) {
this.length = length;
this.width = width;
}
double area() {
return length * width;
}
}
现在我们为每种标签类型都定义了不同的子类,可以明显看出,这种基于类层次的设计规避了标签类的所有问题,同时也大大提供了程序的可读性和可扩展性,如:
class Square extends Rectangle {
Square(double side) {
super(side, side);
}
}
简而言之,标签类很少有适用的场景。当你想要编写一个包含显式标签域的类时,应该考虑一下,这个标签是否可以被取消,这个类是否可以用类层次来代替。当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去。
二十一、用函数对象表示策略
Java没有提供函数指针,但是可以用对象引用实现统一的功能。调用对象上的方法通常是执行该对象(that Object)上的某项操作。然而,我们也可能定义这样一种对象,它的方法执行其他对象(other Objects)(这些对象被显式传递给这些方法)上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象(Function Object),如JDK中Comparator,我们可以将该对象看做是实现两个对象之间进行比较的“具体策略对象”,如:
class StringLengthComparator {
public int compare(String s1,String s2) {
return s1.length() - s2.length();
}
}
这种对象自身并不包含任何域字段,其所有实例在功能上都是等价的,因此可以看作为无状态的对象。这样为了提供系统的性能,避免不必要的对象创建开销,我们可以将该类定义为Singleton对象:
class StringLengthComparator {
private StringLengthComparator() {} //禁止外部实例化该类
public static final StringLengthComparator INSTANCE = new StringLengthComparator();
public int compare(String s1,String s2) {
return s1.length() - s2.length();
}
}
StringLengthComparator类的定义极大的限制了参数的类型,这样客户端也无法再传递任何其他的比较策略。为了修正这一问题,我们需要让该类成为Comparator<T>接口的实现类,由于Comparator<T>是泛型类,因此我们可以随时替换策略对象的参数类型,如:
public interface Comparator<T> {
public int compare(T t1,T t2);
}
class StringLengthComparator implements Comparator<String> {
public int compare(String s1,String s2) {
return s1.length() - s2.length();
}
}
而具体的策略类往往使用匿名类声明,下面的语句根据长度对一个字符串数组进行排序:
Array.sort(stringArray, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
});
要注意,以这种方式使用匿名类时,将会在每次执行调用的时候创建一个新的实例。如果它被重复执行,考虑将函数存储到一个私有的静态final域里,并重它。下面的例子使用静态成员类,而不是匿名类,以便允许具体的策略实现第二个接口Serializable:
class Host {
private static class StrlenCmp implements Comparator<String>, Serializable {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
...
}
简而言之,函数指针的主要用途就是实现策略模式。为了在Java中实现这种模式,要声明一个接口来表示策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,可以考虑使用匿名类来声明和实例化这个具体的策略类。当一个具体策略是设计用来重复使用的时候,他的类通常就要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。
二十二、优先考虑静态成员类
在Java中嵌套类主要分为四种类型,下面给出这四种类型的应用场景。
1.静态成员类:静态成员类可以看做外部类的公有辅助类,仅当与它的外部类一起使用时才有意义。例如,考虑一个枚举,它描述了计算器支持的各种操作。Operation枚举应该是Calculator类的公有静态成员类,然后,Calculator类的客户端就可以用诸如Calculator.Operation.PLUSCalculator.Operation.MINUS这样的名称来引用这些操作。
2.非静态成员类:每个非静态成员类的实例中都隐含一个外部类的对象实例,在非静态成员类的实例方法内部,可以调用外围实例的方法。如果嵌套类的实例可以在它的外围类的实例之外独立存在,这个嵌套类就必须是静态成员类。由于静态成员类中并不包含外部类实例的对象引用,因此在创建时减少了内存开销。非静态成员类一种常见的用法是定义一个Adapter,它允许外部类的实例被看做是另一个不相关的类的实例。如Map接口的实现往往使用非静态成员类来实现它们的集合视图,这些集合视图是由Map的keySet、entrySet和Values方法返回的。同样的,诸如Set和List这种集合接口的实现往往也是使用非静态成员类来实现他们的迭代器:
public class MySet<E> extends AbstractSet<E> {
...
public Iterator<E> iterator() {
return new MyIterator();
}
private class MyIterator<E> {
...
}
}
3.匿名类:匿名类没有自己的类名称,也不是外围类的一个成员。匿名类可以出现在代码中任何允许存在表达式的地方。然而匿名类的适用性受到诸多限制,如不能执行instanceof测试,或者任何需要类名称的其他事情。我们也无法让匿名类实现多个接口,当然也不能直接访问其任何成员。最后需要说的是,建议匿名类的代码尽量短小,否则会影响程序的可读性。匿名类的一种常见用法是动态地创建函数对象(见21条)。另一种常见用法是创建过程对象,比如Runnable、Thread或者TimerTask实例。第三种常见的实例是在静态工厂方法的内部。4.局部类:是四种嵌套类中最少使用的类,在任何“可以声明局部变量”的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。
简而言之,如果一个嵌套类需要再单个方法之外仍然是可见的,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向外围实例的引用,就要把成员类做成非静态的,否则做成静态的。假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类,否则就做成局部类。