Java高效编程之三【类和接口】
本部分包含的一些指导原则,可以帮助哦我们更好滴利用这些语言元素,以便让设计出来的类更加有用、健壮和灵活。
十二、使类和成员的访问能力最小化
三个关键词访问修饰符:private(私有的=类级别的)、未指定(包级私有的)、protected(受保护的=继承级别的+包级别的访问)、pulbic(共有的)
备注:其中未指定,使用的是默认的访问级别,包内部的任何类都可以访问这个成员。如果类或者接口是包级私有的,就应该做成包级私有的。包级私有的是这个包实现的一部分,而不是这个报API的一部分,包级私有的可以更改实现、修改或去除,不必担心伤害到客户,如果是共有的,你就要永远支持它,并且保持兼容性。
经验表明,尽可能地使每一个类或成员都不被外界访问。即,在保证软件功能正确的前提下,使用最低的访问级别。
公有类不应该包含公有域,除了后面的共有静态final域的特殊情形——通过公有域的静态final域来暴露类的常量。按照惯例,这样的域的名字由大写字母构成,单词之间用下划线隔开(见三十八)。很重要的一点是,这个域要么包含原语类型的值,要么包含指向非可变对象的引用(见十三)。
注意:非零长度的数组总是可变的,所以具有共有静态final数据域几乎总是错误的。如果一个类包含这样的一个域,客户能够修改数组中的内容。这是安全漏洞的一个常见根源:
//潜在的安全漏洞 public static final Type[] VALUES={……};
共有数组应该被替换成私有数组,以及一个共有的非可变列表:
private static final Type[] PRIVATE_VALUES={……}; public static final List VALUES= Collection.unmodifiableList(Arrays.asList(PRIVATE_VALUES))
十三、支持非可变性
非可变性类是一个简单的类,它的实例不能被修改。每个实例中包含的所有信息都必须在该实例被创建的时候就提供出来,并且在对象的整个生命周期保持不变。Java平台库包含许多非可变类,其中String、原语类型的包装类、BigInteger和BigDecimal。
非可变类要遵循的五条规则:
- 不要提供任何会修改对象的方法。
- 保证没有可被子类改写的方法。 ->通常将类设置成final,其他方法后面讨论。
- 使所有域都是final的。
- 是所有的域都成为私有的。
- 保证对于任何可变组件的互质访问。 ->在构造方法、访问方法、和readObject方法(见五十六)中请使用保护性拷贝(defensive copy)技术(见二十四)。
- 非可变对象本质上是安全的,他们不要求同步。 ->非可变对象可以被自由的共享,对于频繁用到的值,为它们提供公有的静态final常量。
如:public static final Complext ZERO=new Complex(0,0);
这种方法可以进一步扩展,一个非可变对象可以提供一些静态工厂,它们吧频繁用到的实例缓存起来,当请求一个预先存在的实例的时候,可以不再创建新的实例。BigInteger和Boolean都有这样的静态工厂。使用这样的静态工厂可以使得客户之间可以共享已有的实例,而不是创建新的实例,从而降低内存占用和垃圾回收的代价。
- 你不仅可以共享非可变对象,甚至可以共享它们的内部信息。
- 非可变对象为其他对象——无论是可变的还是非可变的——提供了大量的构建
- 非可变对象的唯一缺点是,对于每一个不同的值都需要一个单独的对象。
注:StringBuffer是String类的可变配套类,在特定的环境下,相对BigInteger而言,BigSet是String类的可变配套类。StringBuffer是可变对象,可以对字符串进行改写,主要是insert和append两个方法,用于多线程。而StringBuilder是JDK1.5之后才加入的,和StringBuffer没有本质区别,但是在单线程的情况下使用,速度更快。
- 除非有更好的理由让一个类成为可变类,否则英爱是非可变的。 ->有get方法,不一定就要有set方法。
- 如果一个类不能做成非可变类,那么你要尽可能的限制其行为。构造函数应该创建完全初始化的对象,所有的约束关系都应该在这个时候起来。
下面是延迟初始化技术的习惯用法:
//不可变对象的缓存,延迟初始化函数 private volatile Foo cacheFooVal=UNLIKE_FOO_VALUE public Foo foo(){ Foo result=cachedFooVal; if(result==UNLIKE_FOO_VALUE) result=cachedFooVal=fooVal(); return result; } //fool值的私有帮助函数 private Fool fooVal(){}
十四、组合优先于继承
与方法不同的是,继承打破了封装性。能用组合完成的就用组合,组合优先于继承。
用组合的方式可以避免有不合适的继承所带来的问题。使用组合不在扩展一个已有的类,而是在新类中增加一个私有域,它引用了这个类的一个实例。新类中的每个实例方法都可以调用被包含已有实例中对应的方法,并返回它的结果。这被称为转发(forwarding),新类中的方法被称为转发方法(forwarding method)。这样的类将会非常稳固,它不依赖已有类的实现细节。即使已有的类增加了新的方法,也不会影响新的类。
// 使用组合取代继承的包装类 public class InstrumentedSet implements Set { private final Set s; private int addCount = 0; public InstrumentedSet(Set s) { this.s = s; } public boolean add(Object o) { addCount++; return s.add(o); } public boolean addAll(Collection c) { addCount += c.size(); return s.addAll(c); } public int getAddCount() { return addCount; } // 转发方法 public void clear() { s.clear(); } public boolean contains(Object o) { return s.contains(o); } public boolean isEmpty() { return s.isEmpty(); } public int size() { return s.size(); } public Iterator iterator() { return s.iterator(); } public boolean remove(Object o) { return s.remove(o); } public boolean containsAll(Collection c) { return s.containsAll(c); } public boolean removeAll(Collection c) { return s.removeAll(c); } public boolean retainAll(Collection c) { return s.retainAll(c); } public Object[] toArray() { return s.toArray(); } public Object[] toArray(Object[] a) { return s.toArray(a); } public boolean equals(Object o) { return s.equals(o); } public int hashCode() { return s.hashCode(); } public String toString() { return s.toString(); } }
这里的包装类可以用来包装任何一个Set实现,并且可以与任何以前已有的构造函数一起工作。如
Set s1=new InstrumentedSet(new TreeSet(list)); Set s2=new InstrumentedSet(new HashSet(capacity, loadFactor));
因为一个Instrument实例都把另一个 Set实例包装了起来,所以我们称其为包装类。这也正是装饰模式,因为Instrument对一个集合进行了修饰,为它增加了计数器的特性。有时候,修改和转发这两项技术的结合被错误的引用为“委托(delegation)”,从技术角度讲,这不是委托,除非包装类把自己传递给一个被包装的对象。
十五、要么专门为继承而设计,给出文档说明,要么禁止继承
对于专门为继承而设计的类而言,需要满足:
- 该类的文档必须清晰的描述改写每一个方法所带来的影响。改写的方法是指非final的,公有的或受保护的。
- 一个类必须通过某种形式提供适当的钩子(hook),以便能进入它的内部工作流程中,这样的形式可以是精心选择的受保护(protected)方法。
- 构造函数一定不能调用可被改写的方法,无论是直接进行还是间接进行。 ->注:
注:如果违反了第三条规则,很有可能会导致程序失败。超类的构造函数在子类的构造函数之前运行,所以子类中改写版本的方法将会在子类的构造函数运行之前先被调用。如果改写版本的方法依赖于子类构造函数所执行的初始化工作,那么该方法就不会如期执行。
public class Super { // 违反了规则 -构造函数调用了重写的方法 public Super() { m(); } public void m() { } }
下面的子类改写了方法m,Super唯一的构造函数就错误的调用了这个方法m:
final class Sub extends Super { private final Date date; // 空的终结字段,由构造函数设置 Sub() { date = new Date(); } // 重写了 Super.m, 被Super的构造函数调用 public void m() { System.out.println(date); } public static void main(String[] args) { Sub s = new Sub(); s.m(); }
本来期望打印出两个日期,但是第一次打印出Null,因为方法被构造函数Super()调用的时候,造函数Sub还没有机会初始化data域。
这个的执行顺序是父类的构造函数->重写的方法(回到子类)->子类的构造函数。
- Cloneable的clone()和Serializable的readObject方法,在行为上非常相似于构造函数,所以一个类的限制规则也是适用的。无论是clone或者是readObject方法都不能调用一个可被改写的方法,不管是直接的方式还是间接地方式。
- 如果你决定在一个为了继承而设计的类中实线Serializable,并且该类有一个readResolve或者writeReplace方法,那么你必须使readReslove或者writeReplace方法称为受保护的方法,而不是私有的方法。
十六、 接口优于抽象类
接口和抽象类都是允许多个实现的类型。两者的区别是抽象类允许包含某些方法的实现,但是接口不允许。实现一个抽象类的类型,它必须成为抽象类的子类。因为Java只允许单继承,所以抽象类作为类型定义收到了极大的限制。
- 已有的类可以被更新,以实现新的接口。
- 接口是定义mixin(混合类型)的理想选择。
- 接口可以使我们构造出非层次结构的类型框架。
- 接口使得安全的增加一个类的功能成为可能。
- 虽然接口不允许包含方法的实现,但是,我们可以接口和抽象类的优点结合起来,对于你期望导出的每一个重要接口,都提供一个抽象的骨架(skeletal implements)实现类。
下面是一个静态工厂方法,它包含了一个静态的工厂方法,它包含一个完整的、功能全面的List实现:
//整形数组的List适配器 static List intArrayAsList(final int[] a){ if(a==null) throw new NullPointerException(); return new AbstractList(){ public Object get(int i){ return new Integer(a[i]); } public int size(){ return a.length; } public Object set(int i, Object o){ int oldVal=a[i]; a[i]=((Integer)o).intValue(); return new Integer(oldVal); } }; }
这个例子是一个适配器模式,它使得int数组可以被看做一个Integer实例列表。这个例子只提供了一个静态工厂,并且这个类是一个可被访问的匿名类,它被隐藏在静态工厂的内部。
下面是Map.Entry接口的骨架实现类:
public abstract class AbstractMapEntry implements Map.Entry { // 基本的 public abstract Object getKey(); public abstract Object getValue(); // 要改变maps的实体必须要重写的方法 public Object setValue(Object value) { throw new UnsupportedOperationException(); } // 实现Map.Entry.equals的通用约定 public boolean equals(Object o) { if (o == this) return true; if (! (o instanceof Map.Entry)) return false; Map.Entry arg = (Map.Entry)o; return eq(getKey(), arg.getKey()) && eq(getValue(), arg.getValue()); } private static boolean eq(Object o1, Object o2) { return (o1 == null ? o2 == null : o1.equals(o2)); } // 实现Map.Entry.hashCode的通用约定 public int hashCode() { return (getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode()); } }
- 抽象类的演化比接口的演化要容易的多。
十七、接口只是被用于定义类型
当一个类实现了一个接口的时候,这个接口被用做一个类型。通过这个类型可以引用这个类的实例。因此,一个类实现了某个接口,就表明客户可以对这个类的实例实施某些动作。为了其他的目的而定义的接口是不合适的。
常量接口是对接口的不良使用,下面是常量接口的例子:
// 常量接口模式- 请勿使用! public interface PhysicalConstants { // Avogadro's number (1/mol) static final double AVOGADROS_NUMBER = 6.02214199e23; // Boltzmann constant (J/K) static final double BOLTZMANN_CONSTANT = 1.3806503e-23; // Mass of the electron (kg) static final double ELECTRON_MASS = 9.10938188e-31; }
导出常量的几种可行方案:
- 常量与类或接口紧密的联系在一起,那么将常量添加到类或接口中。如Java平台中所有的数值包装类,比如Integer和Float,都导出了常量MAX_VALUE和MIN_VALUE。
- 如果这些常量最好被看做一个枚举类型的成员,那么应使用类型安全枚举类(typesafe enum class)来导出这些常量。
- 使用不可实例化的工具类(utility class)来导出常量。下面是PysicalConstants的工具类版本
// 常量工具类 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; }
总之,接口是被用来定义类型的,它们不应该被导出常量。
十八、优先考虑静态成员类
嵌套类(nested class)是指被定义在一个类的内部的类。嵌套类存在的目的是为外围的类提供服务。
嵌套类有四种:静态成员类(static member class)、非静态成员类(nostatic member class)、匿名类(anonymous class)和局部类(local class)。
除了第一种之外,其他三种都被成为内部类(inner class)。静态成员类是一种简单的嵌套类,最好把它看做一个普通类,只是碰巧被声明在类的内部而已。
非静态成员的另一个用法是定义一个Adapter,它允许外部类的一个实例被看做另一个不相关的实例。如,Map接口的实现往往使用非静态成员类来实现它们的集合视图(collection view),这些集合视图是有Map的keySet、entrySet和Value方法返回的。类似地,诸如Set和List这样的集合接口的实现往往也使用非静态成员类来实现它们的迭代器。
//非静态成员的典型用法 public class MySet extends AbstractSet{ ……//省去不相关的 public Iterator iterator(){ return MyIterator(); } priavate class MyIterator implements Iterator{ …… } }
如果你声明的成员类不要求访问外围实例,那么请记住把static修饰符放到成员类的声明中。
非静态成员类需要访问外围实例,如果省略了static修饰符,则每个实例都将包含一个额外的指向外围实例的引用,维护这份引用需要耗费时间和空间,但又没有相应的好处。
匿名类仅仅在使用的时候被声明和实例化,行为与静态成员类或非静态成员类非常相似,取决于它所处的环境:如果匿名类出现在一个非静态的环境中,则它有一个外围实例。
匿名类通常出现在表达式的中间,可能20行或者更短,太长影响到程序的可读性。
匿名类的一个通用方法是创建一个函数对象(function object),比如Comparator实例。例如,下面的方法调用对一组字符串按照其长度进行排序:
//匿名类的典型使用 Arrays.sort(args, new Comparator(){ public int compare(Object o1, Object o2){ return ((String)o1).length()-((String)o2).length(); } });
匿名类的另一个用法是创建一个过程对象(process object),比如Thread、Runable或者TimeTask实例。第三个常见用法是在一个静态工厂方法的内部(见十六intArrayAsList方法)。第四个常见的用法是在复杂的类型安全枚举类型(它要求为每个实例提供单独的子类)中,用于公有的静态final域的初始化器中(见二十一Operation类)。如果Operation类是Calculator的一个静态成员类,那么Operation类是双重嵌套类。
// 公有静态成员类的典型使用 public class Calculator { public static abstract class Operation { private final String name; Operation(String name) { this.name = name; } public String toString() { return this.name; } // 通过这一常量进行运算符表示 abstract double eval(double x, double y); // 双重嵌套匿名类 public static final Operation PLUS = new Operation("+") { double eval(double x, double y) { return x + y; } }; public static final Operation MINUS = new Operation("-") { double eval(double x, double y) { return x - y; } }; public static final Operation TIMES = new Operation("*") { double eval(double x, double y) { return x * y; } }; public static final Operation DIVIDE = new Operation("/") { double eval(double x, double y) { return x / y; } }; } // 返回指定的计算结果 public double calculate(double x, Operation op, double y) { return op.eval(x, y); } }