一 、 消除对象的引用
下面是一个简单的栈实现的例子:
public class Stack {
private Object[] elements;
private int size = 0;
private static fianl int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if(size == 0){
throw new EmptyStackException();
return elements[--size];
}
}
public void ensureCapacity(){
if (elements.length == size)
elements = Arrays.copyOf(element, 2* size + 1);
}
}
不严格的讲,上面这段程序有一个“内存泄漏”,如果一个栈先是增长,然后在收缩,那么,从栈中弹出来的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。因为,栈内部维护着对这些对象的过期引用。
解决办法:
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
Object result = element[--size];
elements[size] = null; // 清空过期引用
}
清空过期引用的另一个好处: 如果它们以后又被错误的解除引用,程序就会立即抛出NullPointerException异常,而不是悄悄的错误运行下去。
注意: 只要是类自己管理内存,程序员就应该警惕内存泄露问题。
内存泄露的另一个常见的来源是缓存 : 有几种可能的解决方案。只要在缓存之外存在对某个项的键的引用,该项就有意义,那么可以用WeakHashMap代表缓存,当缓存中的缓存项过期之后,它们就会自动被删除。
内存泄露的第三个常见的来源是:监听器和其他回调。
二 、 复合优于继承
在包的内部使用继承是非常安全的,在那里,子类和超类的实现都处在同一个程序员的控制下。而对于专门为了继承而设计、并且具有很好的文档说明的类来说,使用继承也是非常安全的。对于普通的具体类进行跨越包边界的继承,则是非常危险的。
示例: 假设我们需要查询HushSet,看它被创建以来曾添加了多少个元素。
public class InstrumentedHashSet<E> extends HushSet<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;
}
}
这个类看起来十分合理,但是它不能正常的工作:现在我们创建一个示例并,添加三个元素
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crack", "Tom"));
这时候我们期望getAddCount方法返回 3 ,可是实际上返回的是6。
这是因为InstrumentedHashSet中的addAll方法首先给addCount方法加三,然后利用super.addAll来调用InstrumentedHashSet覆盖的add方法,每个元素调用一次。总共增加了6。
三 、 列表优先于数组
下面这段代码片段是合法的: 会运行是出错
Object[] objectArray = new Long[1];
objectArray[0] = "addString"; //在Long类型中 加入 String , 运行是出错
下面这段代码则是不合法的: 会编译时出错
List<Object> ol = new ArrayList<Long>; // 编译报错
ol.add("addString");
我们当然想在编译时发现错误。
四、 用enum枚举类型代替int常量
示例:int枚举模式
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TRMPLE = 1;
public static final int ORANGE_BLOOD = 2;
这种被称为int枚举模式。它在类型安全性和使用方便性方面没有任何不足。例如:你将apple传入到想要orange的方法中编译器也不会出现警告
采用int枚举模式的程序是十分脆弱的。因为int枚举是编译时常量,被编译到使用它们的客户端中。如果与枚举常量关联的int发生变化,客户端就必须重新编译。
解决办法: Java 1.5 以后提出了一种解决方案:
public enum Apple {FUJI, PIPPIN, GRANNY_SMITH}
public enum Orange {NAVEL, TEMPLE, BLOOD}
Java枚举类型背后的想法非常简单: 它们就是通过公有的静态final域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的final。因为客户端不能创建枚举类型实例,也不能对其进行扩展。
枚举提供了编译时的类型安全。如果一个参数声明为Apple,就可以保证传入的参数一定是有效的三个Apple值之一。
五、 用实例域代替序数
所有的枚举都有一个ordinal方法,它返回每个枚举常量在类型中的数字位置。
public enum Ensemble {
SOLO, DUET, TRIO, QUATET, QUINTET;
public int numberOfMusicians() {return ordinal() + 1;}
}
这个枚举不错,但是维护起来就像一场噩梦。如果变量进行重新排序, numberOfMusicians方法就会遭到破坏。
解决: 永远不用根据枚举的序数导出与它关联的值,而是将它保存在一个实例域中:
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUATET(4), QUINTET(5);
public final int numberOfMusicians;
Ensemle(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() {return numberOfMusicians;}
}
六、 检查参数的有效性
绝大多数的方法和构造器对于传入它们的参数值都会有某些限制。如: 传入对象不能为null,索引值必须为非负数。
01 . 对于公有的方法,要用 Javadoc的@throw标签在文档中说明违反参数值限制会抛出什么异常。
下面是一个典型的例子:
/**
* 方法和方法解释
*
* @param m the mudulus,which must be positive
* @return this mod m
* @throw AritheticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if(m.signum() <= 0){
throm new ArithmeticException("Modulus <= 0: " + m);
// doSomething()....
}
}
02 . 非公有方法通常使用断言(assertion)来检查它们的参数,具体做法如下所示:
private static void sort(long a[], int offset, int length) {
assert a != null;
assert offset >=0 && offset <= a.length;
// doSomething().......
}
断言不同于一般的有效性检查,断言如果失败,将抛出AssertionError。也不同于一般的有效性检查,如果它们没起作用,本质上也不会有成本开销,除非通过将-ea(或者 -enableassertions)标记(flag)传递给Java解释器,来启用它们。