4. 类和接口
15. 使类和成员的可访问性最小化
把API与实现清晰地隔离开,组件间通过API进行通信,不需要知道其他模块的内部工作情况,这称为:实现信息隐藏或封装
解耦系统中的各个组件
尽可能地使每个类或者成员不被外界访问
成员(域、方法、嵌套类、嵌套接口)的四种可能访问级别:
- 私有(private)
- 包级私有(package-private)
- 受保护(protected) 被称为“缺省” default
- 公有(public)
如果一个缺省的顶级类或接口只在某一类内部用到,就应该考虑使它成为哪个类的私有嵌套类
子类覆盖父类方法,访问级别不能低于父类
公有类的实例域绝对不能是公有的,如果是final或者可变对象的final引用,公有就等于放弃了对存储在这个域中值的限制,除非是是为了暴露静态final常量
注意!
常见问题:长度非零的数组总是可变的,用静态final数组域存储也是错误的,解决:
- 使用Collections.unmodifiableList() 返回不可变列表
- 用clone() 返回私有数组的拷贝
16. 要在公有类而非公有域中使用访问方法
如果类可以在它所在的包之外访问,就提供方法而非暴露数据域,以保留将来改变类内部表示法的灵活性
如果是缺省或私有的嵌套类,直接暴露数据域就没有本质的错误
Java类库反例:java.awt的Point类和Dimension类
17. 使可变性最小化
Java类库不可变类:String、基本类型包装类、BigInteger、BigDecimal
不可变类要遵守规则:
- 不提供任何会修改对象状态的方法
- 保证类不会被拓展 (final class)
- 申明所有的域都是final
- 申明所有的域都是私有的
- 确保对于任何可变组件的互斥访问
如果该不可变类具有指向可变对象的域,要确保该类的客户端无法获得指向这些对象的引用,并且不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用
方法结果返回新的实例而不是修改这个实例:这称为函数方法(与之对应的是:过程或命令式方法)
这些 函数方法 方法命名使用介词而非动词,强调不会改变对象值
不可变对象本质上是线程安全的,不要求同步,不可变对象可以自由共享,鼓励尽可能重复用现有的实例:为频繁用到的值,提供公有静态final常量
可以提供静态工厂:把频繁请求的实例缓存起来,用静态工厂提供代替公有构造器可以让以后有添加缓存的灵活性,而不影响客户端
不需要为不可变类提供clone方法或拷贝构造器,Java类库反例:String有拷贝构造器(应尽量少用)
不可变类可以共享内部信息,例如:BigInterger negate方法
不可变类为其他对象提供大量构件:Map Set
不可变类无偿提供失败的原子性,不存在临时不一致的可能性
缺点:每个不同的值都需要一个单独对象,创建大量对象会影响性能,解决:
- 用基本类型代替其中一些创建大量对象的多步骤操作
- 提供可变配套类:例如 String 对应的 StringBuilder
注意:BigInteger、BigDecimal 最初对于不可变类必须为final没有广泛理解,导致如果编写的一个类安全性依赖于不可信客户端的BigInteger或者BigDecimal参数的不可变性,必须检查是否为真实的而非不可信任子类实例,进行保护性拷贝
注意如果让自己的不可实现类实现Serializable接口,要提供显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectOutputStream.readUnshared方法
Java类库反例:Date、Point 本来应该是不可变类
构造器应该创建完全初始化的对象,并建立起所有的约束关系
18. 复合优先于继承 (此条不适用于接口继承)
包的内部使用继承非常安全,但是普通的具体类进行跨包的继承非常危险
与方法调用不同的是,继承打破了封装性:子类依赖于其父类中特定功能的实现细节,父类的实现可能随着版本的不同而变化,子类会遭到破坏
父类方法:自用性是实现细节,不是承诺
非重写父类方法也有风险:父类后续的发行版可能与你的子类提供签名相同返回值不同或者是签名和返回值都相同的方法
复合(composition):不依赖父类的实现细节,不拓展现有的类,而实在新的类中增加一个私有域,它引用现有类的实例
实现包括两部分:转发类(包含了所有转发方法),以及类本身
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
@Override
public int size() {
return s.size();
}
@Override
public boolean isEmpty() {
return s.isEmpty();
}
@Override
public boolean contains(Object o) {
return s.contains(o);
}
@Override
public Iterator<E> iterator() {
return s.iterator();
}
@Override
public Object[] toArray() {
return s.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean add(E e) {
return s.add(e);
}
@Override
public boolean remove(Object o) {
return s.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
@Override
public void clear() {
s.clear();
}
}
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedHashSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public static void main(String[] args) {
InstrumentedHashSet<String> strings = new InstrumentedHashSet<>(new TreeSet<>());
strings.addAll(Arrays.asList("1", "2", "3"));
System.out.println(strings.addCount); // 3
}
}
InstrumentedHashSet实例把另一个Set实例包装起来了,所以 InstrumentedHashSet类被成为包装类,这就是:Decorator(修饰者)模式
注意!包装类不适合用于回调框架:回调框架中,对象把自生的引用传递给其他对象用于后续回调,包装类不知道自己是一个包装类,传递了指向自生(this)的引用,回调时避开了外面的包装对象(SELF问题)
只有当子类真正是父类的子类型才适合用继承(is-a关系)
Java类库反例:Stack继承Vector Properties继承Hashtable
暴露内部实现细节,客户端可以直接访问内部细节,直接修改父类,从而破坏子类约束
19. 要么设计继承并提供文档说明,要么禁止继承
对于专门为了继承而设计的类,必须精确地描述覆盖每个方法所带来的影响
关于程序文档的一句格言:好的API文档应该描述一个给定的方法做了什么工作,而不是描述他是如何做到的
为了继承的类必须精心挑选受保护(protected)的方法的形式,提供适当的钩子(hook),以便进入其内部工作
允许继承的类的构造器不能直接或间接地调用可能被覆盖的方法,因为父类构造器在子类构造器之前运行,子类中覆盖的方法会在子类构造器之前运行
而通过构造器调用私有方法、final方法、静态是安全的,因为它们都是不可被覆盖的方法
在为了继承而设计的类中实现Cloneable和Serializable接口,要注意clone和readObject方法,类似于构造器,不能直接或间接调用可覆盖方法
所以对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化
- final修饰类
- 私有或包级私有构造器,并增加公有静态工厂
20. 接口优于抽象类
Java提供两种机制可以用来定义允许多个实现的类型:接口和抽象类
Java 8 为继承引入了缺省方法
一般来说,无法更新现有的类来拓展新的抽象类,如过希望两个类拓展一个抽象类,就必须把抽象类放到类型层次的高度,使其成为两个类的祖先
接口的定义 mixin(混合类型)的理想选择,接口允许构造非层次结构的类型框架
接口使得安全地增强类的功能成为可能,如果是抽象类,只能通过继承来增加功能
接口可以使用缺省方法,但是不允许给Object方法(equals hashCode) 提供缺省方法,而且接口中不允许包含实例域或者非公有的静态成员(私有的静态方法除外)
可以通过接口提供一个抽象的骨架实现类,把接口和抽象类的有点结合,接口负责定义类型,还可以提供缺省方法,骨架类实现非基本类型接口方法,这就是模板方法模式
实现这个接口的类,可以把对于接口方法的调用转发到内部私有类的实例上,内部私有类拓展了骨架实现类,别称为:模拟多重继承
对于骨架实现类,好的文档非常必要
21. 为后代设计接口
Java 8 增加了缺省方法,目的是允许给现有接口添加方法,Java 8 在核心类库增加了许多新的缺省方法,为了便于使用Lambda
应尽量避免这种用法,缺省的方法实现可能会破坏现有的接口实现
22. 接口只用于定义类型
常量接口是反例,如果实现类以后不需要这些常量了,依然需要实现这个接口,以确保二进制兼容,非final类实现了常量接口,它的所有名字的命名空间会被这些常量“污染”
解决方案:
- 添加常量在与之紧密相关的类或接口上
- 这些常量最好被看作枚举类成员就用枚举
- 不可实例化的工具类(private 构造器)
Tips:Java 7 开始数字的字面量中可以使用下划线区分
可以使用静态导入:import static 全限定类名.值
23. 类层次优于标签类
类层次例子:
public abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * (radius * radius);
}
}
class Rectangle extends Figure {
final double length;
final double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
double area() {
return length * width;
}
}
多种风格的实例使用标签类的问题:过于冗长、容易出错、效率低下
使用类层次:为标签类中的每个方法都定义一个包含抽象方法的抽象类,所有的方法都用到了某些数据域就放到这个抽象类;为每种原始标签类都定义根类的具体子类
24. 静态成员类优于非静态成员类
嵌套类(4种):在另一个类内部的类,目的是为了外围类提供服务
- 静态成员类:看作普通类,可以访问外围类所有成员,它是外围类的一个静态成员;常见用法:作为公有的辅助类。私有静态成员:常见用法:外围类所代表对象的组件,比如Map的Entry对象,它不需要访问Map,使用非静态成员类很浪费
- 非静态成员类:它的每个实例都隐含地与外围类的一个外围实例相关联;常见用法:定义Adapter,它允许外部类的实例被看作是另一个不相关类的实例,例如:Map的keySet、entrySet、values以及Set和List的迭代器
- 匿名内部类:出现在非静态环境中才有外围实例,即使是在静态环境中,也不能拥有任何静态成员,拥有的常数变量是final基本类型或者被初始化成常量表达式的字符串域;还有许多限制:无法实例化、不能instanceof、无法实现多个接口或拓展类,除了从父类继承,无法调用任何成员;常见用法:静态工厂方法的内部
- 局部类:出现在可以声明局部变量的地方,有名字可重复利用、在非静态环境下定义才有外围实例、不能包含静态成员
除了静态成员类,其他3个都称为内部类
嵌套类中只有静态成员类可以在它的外围类之外独立存在
如果声明的成员类不要求访问外围实例,那就把它定义成静态成员类,而不是非静态成员类,这样可避免每个实例都包含一个额外指向外围对象的引用,这可能造成内存泄漏
25. 限制源文件为单个顶级类
一个源文件只定义多个顶级类,可能导致给一个类提供多个定义,具体使用哪个定义取决于源文件被传递给编译器的顺序
一个源文件放多个顶级类,考虑使用静态成员类
5. 泛型
26. 请不要使用原生态类型
每一种泛型都定义一个原生态类型(raw type):不带任何实际类型参数的泛型,主要为了兼容之前代码(移植兼容性)
List
List 逃避了泛型检查,而List