第13条: 使类和成员的可访问性最小化
良好的模块设计能隐藏其内部数据和其他实现细节,模块之间只通过它们的API进行通信。java语言提供了许多机制来协助隐藏信息。访问控制机制决定了类、接口和成员的可访问性,实体的可访问性是由该实体申明所在的位置,以及该实体申明中所出现的访问修饰符共同决定。正确使用修饰符对于实现信息隐藏非常关键。
1.尽可能使每个类或成员不被外界访问。对于成员有四种可能的访问级别。
a.私有的--只有在申明该成员的顶层类内部才可以访问这个成员。
b.包级私有--申明该成员的包内部的任何类都可以访问这个成员。
c.受保护的--申明该成员的包内部的任何类都可以访问这个成员。并且该类的子类可以访问这个成员。
d.公有的--在任何地方都可以访问该成员。
2.实例域绝不能是公有的。一旦使这个域称为公有的,就放弃了对存储在这个域中的值进行限制的能力。
还要注意,长度非零的数组总是可变的。修正这个问题有两个方法,使数组变成私有的,并增加一个公有的不可变列表,或添加一个方法,它返回私有数组的一个备份。
public class ArrayTest { //potential security hole //public static final Object[] VALUES = {"one", "two", "three"}; private static final Object[] PRIVATE_VALUES = {"one", "two", "three"}; public static final List<Object> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES)); public static final Object[] values(){ return PRIVATE_VALUES.clone(); } }
总而言之,应该始终尽可能地降低可访问性。
第14条: 在公有类中使用访问方法而非公有域
如果类可以在它所在的包的外部进行访问,就提供访问方法。如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误。如果域是不可变的,暴露域的做法危害就比较小些。总之,公有类永远都不应该暴露可变的域。
第15条: 使可变性最小
不可变类只是其实例不能被修改的类,每个实例中所包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。
为了使类成为不可变,需遵循五条规则:
1.不要提供任何会修改对象状态的方法。
2.保证类不会被扩展。
3.使所有的域都是final的。
4.使所有的域都是私有的。
5.确保对于任何可变组件的互斥访问。如果类有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用,并且永远不要用客户端提供的对象来初始化这样的域,也不要从任何访问方法中返回该对象引用。
一个复数的例子:
public class Complex { private final double re; private final double im; public static final Complex ZERO = new Complex(0, 0); public static final Complex ONE = new Complex(1, 0); public static final Complex I = new Complex(0, 1); public Complex(double re, double im){ this.re = re; this.im = im; } public double realPart(){ return re; } public double imaginaryPart(){ return im; } public Complex add(Complex c){ return new Complex(re + c.re, im + c.im); } public Complex subtract(Complex c){ return new Complex(re - c.re, im - c.im); } public Complex mutiply(Complex c){ return new Complex(re * c.re - im * c.im, re * c.im + im * c.re); } public Complex divide(Complex c){ double tmp = c.re * c.re + c.im * c.im; return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp); } @Override public boolean equals(Object obj) { if(obj == this){ return true; } if(!(obj instanceof Complex)){ return false; } Complex c = (Complex) obj; return Double.compare(re, c.re) == 0 && Double.compare(im, c.im) == 0; } @Override public int hashCode() { int result = 17 + hashDouble(re); result = 31 * result + hashDouble(im); return result; } private int hashDouble(double val){ long longBits = Double.doubleToLongBits(val); return (int) (longBits ^ (longBits >>> 32)); } @Override public String toString() { return "(" + re + " + " + im + "i)"; } }
这个类表示一个复数,注意这些算数运算是如何创建并返回新的Complex实例的。大多数重要的不可变类都使用了这种模式。
不可变类的特点:
1.不可变对象比较简单。
2.不可变对象本质上是线程安全的,它们不要求同步。
3.不仅可以共享不可变对象,甚至也可以共享它们的内部信息。
4.不可变对象为其他对象提供了大量构建。
5.不可变对象的缺点是对于每个不同的值,都需要一个单独的对象。
如果类不能被做成是不可变的,仍然应该尽可能地限制它的可变性。降低对象可以存在的状态数,可以更容易地分析该对象的行为,同时降低出错的可能性。
第16条: 复合优先于继承
继承是实现代码重用的有力手段,但并非是最佳工具。使用不当会导致软件变得很脆弱。与方法调用不同,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现可能随着版本的不同而不同,此时子类可能会遭到破坏,即使子类的代码完全没有变化。因而,子类必须跟着超类的变化而演变,除非超类是专门为继承而设计的,并且有很好的文档说明。
一个hashSet计数的例子:
//Broken - Inappropriate use of inheritance! public class InstrumentedHashSet<E> extends HashSet<E>{ private int addCount = 0; public InstrumentedHashSet(){ } public InstrumentedHashSet(int initCap, float loadFactor){ super(initCap, loadFactor); } @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 int getAddCount(){ return addCount; } public static void main(String[] args){ InstrumentedHashSet<String> s = new InstrumentedHashSet<>(); s.addAll(Arrays.asList("snap", "crackle", "pop")); System.out.println(s.getAddCount()); // 6 wrong } }
这个类看似很合理,但不能正常工作。我们期望返回为3,但返回了6.原因是在hashSet内部,addAll方法是基于它的add方法实现的。
有一种办法可以避免碰到这种问题。不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计称为“复合”。
public class ForwardingSet<E> implements Set<E>{ private final Set<E> s; public ForwardingSet(Set<E> s){ this.s = s; } @Override public int size() { // TODO Auto-generated method stub return s.size(); } @Override public boolean isEmpty() { // TODO Auto-generated method stub return s.isEmpty(); } @Override public boolean contains(Object o) { // TODO Auto-generated method stub return s.contains(o); } @Override public Iterator<E> iterator() { // TODO Auto-generated method stub return s.iterator(); } @Override public Object[] toArray() { // TODO Auto-generated method stub return s.toArray(); } @Override public <T> T[] toArray(T[] a) { // TODO Auto-generated method stub return s.toArray(a); } @Override public boolean add(E e) { // TODO Auto-generated method stub return s.add(e); } @Override public boolean remove(Object o) { // TODO Auto-generated method stub return s.remove(o); } @Override public boolean containsAll(Collection<?> c) { // TODO Auto-generated method stub return s.containsAll(c); } @Override public boolean addAll(Collection<? extends E> c) { // TODO Auto-generated method stub return s.addAll(c); } @Override public boolean retainAll(Collection<?> c) { // TODO Auto-generated method stub return s.retainAll(c); } @Override public boolean removeAll(Collection<?> c) { // TODO Auto-generated method stub return s.removeAll(c); } @Override public void clear() { // TODO Auto-generated method stub s.clear(); } }
public class InstrumentedSet<E> extends ForwardingSet<E>{ public InstrumentedSet(Set<E> s) { super(s); } private int addCount = 0; @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 int getAddCount(){ return addCount; } public static void main(String[] args){ int capacity = 16; InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<String>(capacity)); s.addAll(Arrays.asList("snap", "crackle", "pop")); System.out.println(s.getAddCount()); // 3 right } }
因为每个instrumentedSet实例都把另一个Set实例包装起来了,所以instrumentedSet类被称为包装类。这也正是Decorator模式。
只有当子类真正是超类的子类型的时候,才适合用继承,换句话说,只有两者确实存在“is-a”关系的时候,才应该使用扩展。简而言之,继承功能非常强大,但也存在诸多问题,因为它违背了封装原则。所以可以用复合和转发机制来代替继承。
第17条: 要么为继承而设计,并提供文档,要么就禁止继承
对于专门为继承而设计的类而言,该类的文档必须精确的描述覆盖每个方法所带来的影响。唯一的测试方法就是编写子类。为了允许继承,构造器绝不能调用可被覆盖的方法,无论是直接调用还是间接调用。如果决定为继承而设计的的类实现Cloneable和serializable接口,就应该意识到,clone和readObject方法非常类似构造器,所以都不可以调用可覆盖的方法。对于那些并非为了安全地进行子类化而设计的类,要禁止子类化,一种办法是把该类申明为final的,另一种就是把构造器设为私有或包级私有的。
第18条: 接口优先于抽象类
接口和抽象类的区别:
接口不能被实例化不能有构造器,抽象类也不能被实例化但可以有构造器;
接口中不能有实现方法(JDK8在接口中可以有实现方法,称“默认方法”),抽象类中可以有实现方法,也可以没有;
接口方法的默认修饰符就是public,不可以用其他修饰符,抽象类可以有public、protected、default。
对于“接口优于抽象类”,原因就是Java只支持单继承,但可以实现多个接口。对导出的重要接口都提供一个抽象的骨架实现类,可以把接口和抽象的优点结合起来。接口的作用仍是定义类型,骨架实现类接管所有接口实现相关工作。
一个Map.Entry接口的例子:
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V>{ public abstract K getKey(); public abstract V getValue(); @Override public V setValue(V value) { throw new UnsupportedOperationException(); } @Override public boolean equals(Object obj) { if(obj == this){ return true; } if(!(obj instanceof Map.Entry)){ return false; } Map.Entry<?, ?> arg = (Map.Entry) obj; return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue()); } private static boolean equals(Object o1, Object o2){ return o1 == null ? o2 == null : o1.equals(o2); } @Override public int hashCode() { return hashCode(getKey()) ^ hashCode(getValue()); } private static int hashCode(Object obj){ return obj == null ? 0 : obj.hashCode(); } }
简而言之,接口通常是定义允许多个实现的类型的最佳途径。
第19条: 接口只用于定义类型
当类实现接口时,接口就可以充当引用这个类的实例的类型。类实现了某个接口,就表明客户端可以对这个类的实例实施某些动作,为了任何其他目的而定义接口是不恰当的。
public interface PhysicalConstants { static final double AVOGADROS_NUMBER = 6.02214199e23; static final double BOLTZMANN_CONSTANT = 1.3806503e-23; }
这种常量接口模式是对接口的不良使用。应该使用枚举类或不可实例化的工具类来导出这些常量。总之,接口应该只用来定义类型,不应该被用来导出常量。
第20条: 类层次优先于标签类
标签类是指在类中定义了一个变量,使用该变量的值来控制该做什么动作。
public class TagFigure { enum Shape {RECTANGLE, CIRCLE}; final Shape shape; double length; double width; double radius; TagFigure(double radius){ shape = Shape.CIRCLE; this.radius = radius; } TagFigure(double length, double width){ shape = Shape.RECTANGLE; this.length = length; this.width = width; } double area(){ switch(shape){ case RECTANGLE: return length * width; case CIRCLE: return Math.PI * (radius * radius); default: throw new AssertionError(); } } }
该类就是个标签类。这种类有很多缺点:过于冗长,容易出错,并且效率低下。
用类层次来实现:
public abstract class Figure { abstract double area(); } class Circle extends Figure { final double radius; Circle(double radius){ this.radius = radius; } @Override double area() { // TODO Auto-generated method stub 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; } @Override double area() { // TODO Auto-generated method stub return length * width; } }
这段代码简单清楚,它反映了类型之间本质上的层次关系。有助于增强灵活性,并进行更好的编译时类型检查。简而言之,当想要编写一个包含显示标签域的类时,应该考虑一下,这个标签是否可以被取消,这个类是否可以用类层次来代替。
第21条: 用函数对象表示策略
什么是函数对象?实际上这是在JDK8之前没有Java不支持lamda表达式,方法参数不能传递一个方法只能通过传递对象的方式“曲线救国”,例如Arrays.sort(T[] a, Comparator<? super T> c)方法,第一个参数传递数组,根据传入第二个自定义的比较类中的比较方法进行排序。如果能传入函数指针、Lambda表达式等,那就自然不用传递一个类。
public class Host { private static class StrLenCmp implements Comparator<String>, Serializable { @Override public int compare(String o1, String o2) { return o1.length() - o2.length(); } } public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp(); }
上面例子中,宿主类(host class)中的 STRING_LENGTH_COMPARATOR 就是一个函数对象。表示一种策略。
第22条: 优先考虑静态成员类
嵌套类是指定义在另一个类内部的类。嵌套类有四种:静态成员类,非静态成员类,匿名类和局部类。除了第一种外,其他三种称为“内部类”。
静态成员类相比较于非静态成员类就是多了一个static关键字修饰类,另外一个更重要的区别在于非静态成员类的每个实例都包含一个额外的指向外围对象的引用,保存这份引用要耗费时间和空间。
举个例子,在JDK7中,HashMap内部使用Entry类表示每个键-值对,这个类是static静态的,如果将static去掉仍然可以工作,但每个entry中将会包含一个指向该Map的引用,这样就浪费了空间和时间。
那么什么时候使用静态什么时候使用非静态呢?
书中给出了比较明确的原则:如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中。也就是说如果成员类和外围实例类有交互,那这个类就应该是非静态的,如果没有交互而是作为外围类的一个组件存在在应使用静态的。
最后一个是局部类,只要是在任何“可以声明局部变量的地方”,都可以声明局部类,用得最少,如果要使用那也必须非常简短。