第4章 类和接口
使类和成员的可访问性最小化
模块之间只通过它们的API进行通信, 一个模块不需要知道其他模块的工作情况,这称之为信息隐藏(information hiding)或封装(encapsualtion),是软件设计的基本原则之一。
灵活使用成员(域、方法、嵌套类和嵌套接口)的四种访问级别。
- 私有的(private)
- 包访问的(package-private)
- 受保护的(protected)
- 公有的(public)
其中私有和包访问则是类的实现中的一部分即不会影响它的导出的API。
受保护的成员的应该尽量少用。
包含公有可变域的类并不是线程安全的。这一点主要说明的是我们应该多使用不可变的域,即多使用final来达到我们的目的。PS:final并不是万能的解决方案,即当final指向一个可变对象的引用,同样也会带来问题。
长度非零的数组总是可变的,所以,类具有共有的静态final数组域,或者返回这种域的访问方法,这几乎总是错误的。如下:
public static final Thing[] values = {...};
有如下的两种修正方法:
- 使公有数组变成私有的,并增加一个公有的不可变列表:
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableLIst(Arrays.asList(PRIVATE_VALUES));
- 使数组变成私有的,并添加一个公有方法,返回私有数组的一个备份:
private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values(){
return PRIVATE_VALUES.clone();
}
总结:应该始终尽可能地降低可访问性。在仔细地设计了一个最小的公有API之后,应该防止把任何散乱的类、接口和成员变成API的一部分。除了公有静态final域的特殊情形之外,公有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都是不可变的。
在公有类中使用访问方法而非公有域
如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性。
当域不可变的时候,在读取域时,可强加约束条件。
公有类永远都不应该暴露可变的域。虽然还是有问题,但是让公有类暴露不可变的域其危害比较小。但是,有时候会需要用包级私有的或者私有的嵌套类来暴露域,无论这个类是不变的还是不可变的。
是可变性最小化
Java平台类库中包含的不可变类,有String,基本类型的包装类,BigInteger和BigDecimal。不可变的类比可变类更加易于设计、实现和使用,不容易出错且更加安全。
为了使类成为不可变,要遵循下面五条原则:
- 不要提高任何会修改对象状态的方法。
- 保证类不会扩展
- 使所有的于都是final的。
- 使所有的域成为私有的。
- 确保对于任何可变组件的互斥访问。
采用函数的做法,即在方法中返回的是一个新的实例,而不是修改这个实例。
不仅可以共享不可变对象,甚至也可以共享它们的内部信息。如BigInteger类。
不可变对象为其他对象提供了大量的构建。
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。
为了确保不可变性,类绝对不允许自身被子类话,除了“使类成为final的”这种方法之外,还有另外一种更加灵活的办法可以做到这一点:让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂(static factory)来代替公有的构造器。
复合优先于继承
继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同有所变化,如果真的发生了变化,子类可能遭到破坏,即使它的代码完全没有改变。
继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷。
要么为继承而设计,并提供文档说明,要么就禁止继承
为了设计一个类的文档,以便它能够被安全地子类化 ,你必须描述清楚那些有可能未定义的实现细节。
为了允许继承,类还必须遵守其他一些约束。构造器不能调用可被覆盖的方法。
你可以机械地消除类中可覆盖方法的自用特性,而不改变它的行为。将每个可覆盖方法的代码体移到一个私有的"辅助方法(helper method)"中,并且让每个可覆盖的方法调用它的私有辅助方法。然后,用“直接调用可覆盖方法的私有辅助方法”来代替“可覆盖方法的每个自用调用”。
接口优于抽象类
Java只允许单集成,所以抽象类作为类型定义受到了极大的限制。而相对地,接口则有以下好处:
- 现有的类可以很容易被更新,以实现新的接口。(抽象类则需要修改类层次,单继承也会给我们带来不小的困扰。)
- 接口是定义mixin(混合类型)的理想选择。
- 接口允许我们构造非层次结构的类型框架。
Sometime, 我们需要对接口来提供一个抽象的骨架实现类(skeletal implementation), 把接口和抽象类的优点结合起来。骨架实现通常会以 AbstractInterface的形式出现,如 Collections Framework中的AbstractCollection、AbstractSet、AbstractList和AbstractMap。
抽象类的演变比接口的演变要容易得多。在抽象类中增加新的方法,则该抽象类的所有现有实现都将提供这个新的方法。对于接口,这样做是行不通的。
总结: 接口通常是定义允许多个实现的类型的最佳途径。一个例外就是,当演变的容易性比灵活性和功能更为重要的时候,这种情况下,应该使用抽象类,但前提是必须理解并且可以接受这些局限性。如果导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。最后,应该尽可能谨慎地设计所有的公有接口,并通过编写多个实现来对它们进行全面的测试。
接口只用于定义类型
接口应该只被用来定义类型,不应该被用来导出常量。常量接口模式是对接口的不良使用。应该使用不可实例化的工具类(utility class)来导出这些常量。
如果大量利用工具类导出的常量,可以通过利用静态导入(static import)机制,避免用类名来修饰常量名,(静态导入机制是在Java1.5中才引入的)。
类层次优于标签类
标签类是指带有两种设置更多风格的实例的类,并包含实例风格的标签(tag)域。
它有着有多缺点,其中充斥着样板代码,包括枚举声明、标签域及条件语句。一句话:标签类过于冗长,容易出错,并且效率低下。
解决方法:使用子类型化(subtyping)为每种原始标签类定义根类的具体子类。
类层次的另一种好处:可以反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查。
用函数对象表示策略
常用的比较器函数就代表一种为元素排序的策略。
总结:函数指针的主要用途就是实现策略(Strategy)模式。为了在Java实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用过的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。
优先考虑静态成员类
嵌套类(nested class)是指被定义在另一个类的内部的类。其有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)、和局部类(local class),后三种都被成为内部类(inner class)。
静态成员类可以访问外围类的所有成员,包括哪些声明为私有的成员。静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则。如果被声明为私有的,它则只能在外围类的内部才可以被访问。
常见的地方:Map的集合视图(collection view)keySet、entrySet和Values;Set和List集合接口中的迭代器(iterator)。
非静态成员类的每个实例都隐含着与外部类的一个外围实例(enclosing instance)相关联。这种关联关系需要消耗非静态成员类实例的空间,并且增加了构造的时间开销。
当且仅当匿名类出现在非静态的环境中,它才有外围实例。当出现在静态的环境中,则不会用于任何静态成员。常用用法是动态地创建行数对象(function object);创建过程对象(process object)如,Runnable, Thread 或 TimerTask实例;在静态工厂方法的内部。
局部类有名字,可以被重复地使用。与匿名类一样,只有在非静态环境中定义的时候,才有外围实例,它们也不能包含静态成员。
** 总结:** 四种嵌套类各有用途。若果一个嵌套类需要在单个方法之外仍然是可见的,或者太长了,不适合在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的;否则,就做成静态的。假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类;否则就做成局部类。