转载

《Effective Java》Second Edition 第4章 类和接口

第4章 类和接口

第13条:使类和成员的可访问性最小化

  1. 信息隐藏之所以非常重要有许多原因,其中大多数理由都源于这样一个事实:它可以有效地解除组成系统的各模块之间的耦合关系,使得这些模块可以独立地开发、测试、优化、使用、理解和修改。
  2. 对于顶层的(非嵌套的)类和接口,只有两种可能的访问级别:包级私有的(package-private)和公有的(public)。如果类或者接口能够被做成包级私有的,它就应该被做成包级私有。通过把类或者接口做成包级私有,它实际上成了这个包的实现的一部分,而不是该包导出的API的一部分,在以后的发行版本中,可以对它进行修改、替换,或者删除,而无需担心会影响到现有的客户端程序。
  3. 如果一个包级私有的顶层类(或者接口)只是在某一个类的内部被用到,就应该考虑使它成为唯一使用它的那个类的私有嵌套类。然而,降低不必要公有类的可访问性,比降低包级私有的顶层类的更重要得多:因为公有类是包的API的一部分,而包级私有的顶层类则已经是这个包的实现的一部分。
  4. 包级私有的(package-private)——声明该成员的包内部的任何类都可以访问这个成员。从技术上讲,它被称为“缺省(default)访问级别”,如果没有为成员指定访问修饰符,就采用这个访问级别。
  5. 其实,只有当同一个包内的另一个类真正需要访问一个成员的时候,你才应该删除private修饰符,使该成员变成包级私有的。如果你发现自己经常要做这样的事情,就应该重新检查你的系统设计,看看是否另一种分解方案所得到的类,与其他类之间的耦合度会更小。然而,如果这个类实现了Serializable接口,这些域就有可能被“泄漏(leak)”到导出的API中。
  6. 受保护的成员是类的导出的API的一部分,必须永远得到支持。导出的类的受保护成员也代表了该类对于某个实现细节的公开承诺。受保护的成员应该尽量少用。
  7. 如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别。这样可以确保任何可使用超类的实例的地方也都可以使用子类的实例。
  8. 为了测试而将一个公有类的私有成员变成包级私有的,这还可以接受,但是要将访问级别提高到超过它,这就无法接受了。换句话说,不能为了测试,而将类、接口或者成员变成包的导出的API的一部分。幸运的是,也没有必要这么做,因为可以让测试作为被测试的包的一部分来运行,从而能够访问它的包级私有的元素。
  9. 实例域决不能是公有的。如果域是非final的,或者是一个指向可变对象的final引用,那么一旦使这个域成为公有的,就放弃了对存储在这个域中的值进行限制的能力;这意味着,你也放弃了强制这个域不可变的能力。因此,包含公有可变域的类并不是线程安全的。即使域是final的,并且引用不可变的对象,当把这个域变成公有的时候,也就放弃了“切换到一种新的内部数据表示法”的灵活性。
  10. 假设常量构成了类提供的整个抽象中的一部分,可以通过公有的静态final域来暴露这些常量。按惯例,这种域的名称由大写字母组成,单词之间用下划线隔开。很重要的一点是,这些域要么包含基本类型的值,要么包含指向不可变对象的引用。
  11. 注意,长度非零的数组总是可变的,所以,类具有公有的静态final数组域,或者返回这种域的访问方法,这几乎总是错误的。
  12. 要注意,许多IDE会产生返回指向私有数组域的引用的访问方法,这样就会产生这个问题。修正这个问题有两种方法。可以使公有数组变成私有的,并增加一个公有的不可变列表:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = 
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

第14条:在公有类中使用访问方法而非公有域

  1. Java平台类库中有几个类违反了“公有类不应该直接暴露数据域”的告诫。显著的例子包括java.awt包中的Point和Dimension类。它们是不值得效仿的例子,相反,这些类应该被当作反面警告示例。正如第55条中所讲述的,决定暴露Dimension类的内部数据造成了严重的性能问题,而且这个问题至今依然存在。
  2. 总之,公有类永远都不应该暴露可变的域。虽然还是有问题,但是让公有类暴露不可变的域其危害比较小。但是,有时候会需要用包级私有的或者私有的嵌套类来暴露域,无论这个类是可变的还是不可变的。

第15条:使可变性最小化

  1. Java平台类库中包含许多不可变的类,其中有String、基本类型的包装类、BigInteger和BigDecimal。
  2. 保证类不会被扩展。这样可以防止粗心或者恶意的子类假装对象的状态已经改变,而破坏该类的不可变行为。为了防止子类化,一般做法是使这个类成为final的,但是后面我们还会讨论到其他的做法。
  3. 确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法(accessor)中返回该对象的引用。在构造器、访问方法和readObject方法中请使用保护性拷贝(defensive copy)技术。
  4. 注意这些算术运算是如何创建并返回新的Complex实例,而不是修改这个实例。大多数重要的不可变类都使用了这种模式。它被称为函数的(functional)做法,因为这些方法返回了一个函数的结果,这些函数对操作数进行运算但并不修改它。与之相对应的更常见的是过程的(procedural)或者命令式的(imperative)做法,使用这些方式时,将一个过程作用在它们的操作数上,会导致它的状态发生改变。
  5. “不可变对象可以被自由地共享”导致的结果是,永远也不需要进行保护性拷贝。实际上,你根本无需做任何拷贝,因为这些拷贝始终等于原始的对象。因此,你不需要,也不应该为不可变的类提供clone方法或者拷贝构造器(copy constructor)。这一点在Java平台的早期并不好理解,所以String类仍然具有拷贝构造器,但是应该尽量少用它。
  6. 如果能够精确地预测出客户端将要在不可变的类上执行哪些复杂的多阶段操作,这种包级私有的可变配套类的方法就可以工作得很好。如果无法预测,最好的办法是提供一个公有的可变配套类。在Java平台类库中,这种方法的主要例子是String类,它的可变配套类是StringBuilder(和基本上已经废弃的StringBuffer)。可以这样认为,在特定的环境下,相对于BigInteger而言,BitSet同样扮演了可变配套类的角色。
  7. 例如,假设你希望提供一种“基于极坐标创建复数”的方式。如果使用构造器来实现这样的功能,可能会使得这个类很零乱,因为这样的构造器与已用的构造器Complex(double, double)具有相同的签名。通过静态工厂,这很容易做到。只需添加第二个静态工厂,并且工厂的名字清楚地表明了它的功能即可:
public static Complex valueOfPolar(double r, double theta) {
    return new Complex(r * Math.cos(theta), r * Math.sin(theta));
}

当BigInteger和BigDecimal刚被编写出来的时候,对于“不可变的类必须为final的”还没有得到广泛地理解,所以它们的所有方法都有可能会被覆盖。遗憾的是,为了保持向后兼容,这个问题一直无法得以修正。如果你编写一个类,它的安全性依赖于(来自不可信客户端的)BigInteger或者BigDecimal参数的不可变性,就必须进行检查,以确定这个参数是否为“真正的”的BigInteger或者BigDecimal,而不是不可信任子类的实例。如果是后者的话,就必须在假设它可能是可变的前提下对它进行保护性拷贝:

public static BigInteger safeInstance(BigInteger val) {
    if (val.getClass() != BigInteger.class)
        return new BigInteger(val.toByteArray());
    return val;
}

总之,坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由要让类称为可变的类,否则就应该是不可变的。你应该总是使一些小的值对象,比如PhoneNumber和Complex,成为不可变的(在Java平台类库中,有几个类如java.util.Date和java.awt.Point,它们本应该是不可变的,但实际上却不是)。你也应该认真考虑把一些较大的值对象做成不可变的,例如String和BigInteger。只有当你确认有必要实现令人满意的性能时,才应该为不可变的类提供公有的可变配套类。

可以通过TimerTask类来说明这些原则。它是可变的,但是它的状态空间被有意地设计得非常小。你可以创建一个实例,对它进行调度使它执行起来,也可以随意地取消它。一旦一个定时器任务(timer task)已经完成,或者已经被取消,就不可能再对它重新调度。

第16条:复合优先于继承

  1. 在包的内部使用继承是非常安全的,在那里,子类和超类的实现都处在同一个程序员的控制之下。对于专门为了继承而设计、并且具有很好的文档说明的类来说,使用继承也是非常安全的。然而,对普通的具体类(concrete class)进行跨越包边界的继承,则是非常危险的。
  2. 我们只要去掉被覆盖的addAll方法,就可以“修正”这个子类。虽然这样得到的类可以正常工作,但是,它的功能正确性则需要依赖于这样的事实:HashSet的addAll方法是在它的add方法上实现的。这种“自用性(self-use)”是实现细节,不是承诺,不能保证在Java平台的所有实现中都保持不变,不能保证随着发行版本的不同而不发生变化。因此,这样得到的InstrumentedHashSet类将是非常脆弱的。
  3. 上面这两个问题都来源于覆盖(overriding)动作。如果在扩展一个类的时候,仅仅是增加新的方法,而不是覆盖现有的方法,你可能会认为这是安全的。虽然这种扩展方式比较安全一些,但是也并非完全没有风险。如果超类在后续的发行版本中获得了一个新的方法,并且不幸的是,你给子类提供了一个签名相同但返回类型不同的方法,那么这样的子类将无法通过编译。如果给子类提供的方法带有与新的超类方法完全相同的签名和返回类型,实际上就覆盖了超类中的方法,因此又回到上述的两个问题上去了。
  4. 不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称作“复合(composition)”,因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。这被称为转发(forwarding),新类中的方法被称为转发方法(forwarding method)。这样得到的类将会非常稳固,它不依赖于现有类的实现细节。即使现有的类添加了新的方法,也不会影响新的类。(因为新类中的实例方法没有转发这个新的方法的方法。)
// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {
    private int addCount = 0;
    public InstrumentedSet(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 int getAddCount() {
        return addCount;
    }
}

// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }

    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<E> iterator() { return s.iterator(); }
    public boolean add(E e) { return s.add(e); }
    public boolean remove(Object o) { return s.remove(o); }
    public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c) { return s.addAll(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 <T> T[] toArray(T[] a) { return s.toArray(a); }
    @Override public boolean equals(Object o) { return s.equals(o); }
    @Override public int hashCode() { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

Set接口的存在使得InstrumentedSet类的设计成为可能,因为Set接口保存了HashSet类的功能特性。从本质上讲,这个类把一个Set转变成了另一个Set,同时增加了计数的功能。前面提到的基于继承的方法只适用于单个具体的类,并且对于超类中所支持的每个构造器都要求有一个单独的构造器,与此不同的是,这里的包装类(wrapper class)可以被用来包装任何Set实现,并且可以结合任何先前存在的构造器一起工作。例如:

Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp));
Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));

因为每一个InstrumentedSet实例都把另一个Set实例包装起来了,所以InstrumentedSet类被称作包装类(wrapper class)。这也正是Decorator模式,因为InstrumentedSet类对一个Set惊醒了修饰,为它增加了计数特性。有时候,复合和转发的结合也被错误地称为“委托(delegation)”。从技术的角度而言,这不是委托,除非包装对象把自身传递给被包装的对象。

换句话说,对于两个类A和B,只有当两者之间确实存在“is-a”关系的时候,类B才应该扩展类A。如果答案是否定的,通常情况下,B应该包含A的一个私有实例,并且暴露一个较小的、较简单的API:A本质上不是B的一部分,只是它的实现细节而已。

在Java平台类库中,有许多明显违反这条原则的地方。例如,栈(stack)并不是向量(vector),所以Stack不应该扩展Vector。同样地,属性列表也不是散列表,所以Properties不应该扩展Hashtable。在这两种情况下,复合模式才是恰当的。

对于你正试图扩展的类,它的API中有没有缺陷呢?如果有,你是否愿意把那些缺陷传播到类的API中?继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷。

为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。

第17条:要么为继承而设计,并提供文档说明,要么就禁止继承

  1. 对于为了继承而设计的类,唯一的测试方法就是编写子类。如果遗漏了关键的受保护成员,尝试编写子类就会使遗漏所带来的痛苦变得更加明显。相反,如果编写了多个子类,并且无一使用受保护的成员,或许就应该把它做成私有的。经验表明,3个子类通常就足以测试一个可扩展的类。
  2. 构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。如果违反了这条规则,很有可能导致程序失败。超类的构造器在子类的构造器之前运行,所以,子类中覆盖版本的方法将会在子类的构造器运行之前就先被调用。
  3. 如果你决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,因为clone和readObject方法在行为上非常类似于构造器,所以类似的限制规则也是适用的:无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接方式。对于readObject方法,覆盖版本的方法将在子类的状态被反序列化(deserialized)之前先被运行;而对于clone方法,覆盖版本的方法则是在子类的clone方法有机会修正被克隆对象的状态之前先被运行。无论哪种情形,都不可避免地将导致程序失败。
  4. 这条建议可能会引来争议,因为许多程序员已经习惯于对普通的具体类进行子类化,以便增加新的功能设施,比如仪表功能(instrumentation,如计数显示等)、通知机制或者同步功能,或者为了限制原有类中的功能。如果类实现了某个能够反映其本质的接口,比如Set、List或者Map,就不应该为了禁止子类化而感到后悔。第16条中介绍的包装类(wrapper class)模式提供了另一种更好的办法,让继承机制实现更多的功能。
  5. 如果你认为必须允许从这样的类继承,一种合理的办法是确保这个类永远不会调用它的任何可覆盖的方法,并在文档中说明这一点。

第18条:接口优于抽象类

  1. 这两种机制之间最明显的区别在于,抽象类允许包含某些方法的实现,但是接口则不允许。一个更为重要的区别在于,为了实现由抽象类定义的类型,类必须成为抽象类的一个子类。任何一个类,只要它定义了所有必要的方法,并且遵守通用约定,它就被允许实现一个接口,而不管这个类是处于类层次(class hierarchy)的哪个位置。
  2. 虽然接口不允许包含方法的实现,但是,使用接口来定义类型并不妨碍你为程序员提供实现上的帮助。通过对你导出的每个重要接口都提供一个抽象的骨架实现(skeletal implementation)类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。
  3. 按照惯例,骨架实现被称为AbstractInterface,这里的Interface是指所实现的接口的名字。例如,Collections Framework为每个重要的集合接口都提供一个骨架实现,包括AbstractCollection、AbstractSet、AbstractList和AbstractMap。将它们称作SkeletalCollection、SkeletalSet、SkeletalList和SkeletalMap也是有道理的,但是现在Abstract的用法已经根深蒂固。
  4. 编写骨架实现类相对比较简单,只是有点单调乏味。首先,必须认真研究接口,并确定哪些方法是最为基本的(primitive),其他的方法则可以根据它们来实现。这些基本方法将成为骨架实现类中的抽象方法。然后,必须为接口中所有其他的方法提供具体的实现。例如,Map.Entry接口的骨架实现类:
// Skeletal Implementation
public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
    // Primitive operations
    public abstract K getKey();
    public abstract V getValue();

    // Entries in modifiable maps must override this method
    public V setValue(V value) {
        throw new UnsupportedOperationException();
    }

    // Implements the general contract of Map.Entry.equals
    @Override public boolean equals(Object o) {
        if (o == this)
            return true;
        if (! (o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> arg = (Map.Entry) o;
        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);
    }

    // Implements the general contract of Map.Entry.hashCode
    @Override public int hashCode() {
        return hashCode(getKey()) ^ hashCode(getValue());
    }
    private static int hashCode(Object obj) {
        return obj == null ? 0 : obj.hashCode();
    }
}

骨架实现上有个小小的不同,就是简单实现(simple implementation),AbstractMap.SimpleEntry就是个例子。简单实现就像个骨架实现,这是因为它实现了接口,并且是为了继承而设计的,但是区别在于它不是抽象的:它是最简单的可能的有效实现。你可以原封不动地使用,也可以看情况将它子类化。

使用抽象类来定义允许多个实现的类型,与使用接口相比有一个明显的优势:抽象类的演变比接口的演变要容易得多。如果在后续的发行版本中,你希望在抽象类中增加新的方法,始终可以增加具体方法,它包含合理的默认实现。然后,该抽象类的所有现有实现都将提供这个新的方法。对于接口,这样做是行不通的。

一般来说,要想在公有接口中增加方法,而不破坏实现这个接口的所有现有的类,这是不可能的。之前实现该接口的类将会漏掉新增加的方法,并且无法再通过编译。在为接口增加新方法的同时,也为骨架实现类增加同样的新方法,这样可以在一定程度上减小由此带来的破坏,但是,这样做并没有真正解决问题。所有不从骨架实现类继承的接口实现仍然会遭到破坏。

简而言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性和功能更为重要的时候。在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。最后,应该尽可能谨慎地设计所有的公有接口,并通过编写多个实现来对它们进行全面的测试。

第19条:接口只用于定义类型

  1. 当类实现接口时,接口就充当可以引用这个类的实例的类型(type)。因此,类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口是不恰当的。
  2. 常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。实现常量接口,会导致把这样的实现细节泄漏到该类的导出API中。类实现常量接口,这对于这个类的用户来讲并没有什么价值。实际上,这样做反而会让他们更加糊涂。更糟糕的是,它代表了一种承诺:如果在将来的发行版本中,这个类被修改了,它不再需要使用这些常量了,它依然必须实现这个接口,以确保二进制兼容性。如果非final类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所“污染”。
  3. 在Java平台类库中有几个常量接口,例如java.io.ObjectStreamConstants。这些接口应该被认为是反面的典型,不值得效仿。
  4. 如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中。例如,在Java平台类库中所有的数值包装类,如Integer和Double,都导出了MIN_VALUE和MAX_VALUE常量。如果这些常量最好被看作枚举类型的成员,就应该用枚举类型(enum type)来导出这些常量。否则,应该使用不可实例化的工具类(utility class)来导出这些常量。下面的例子是前面的PhysicalConstants例子的工具类翻版:
// Constant utility class
public class PhysicalConstants {
    private PhysicalConstants() { } // Prevents instantiation
    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;
}

第20条:类层次优于标签类

  1. 这种标签类(tagged class)有着许多缺点。它们中充斥着样板代码,包括枚举声明、标签域以及条件语句。由于多个实现乱七八糟地挤在了单个类中,破坏了可读性。内存占用也增加了,因此实例承担着属于其他风格的不相关的域。域不能做成是final的,除非构造器初始化了不相关的域,产生更多的样板代码。
  2. 当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去。

第21条:用函数对象表示策略

  1. 然而,我们也可能定义这样一种对象,它的方法执行其他对象(other objects)(这些对象被显式传递给这些方法)上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象(function object)。
  2. 作为典型的具体策略类,StringLengthComparator类是无状态的(stateless):它没有域,所以,这个类的所有实例在功能上都是相互等价的。因此,它作为一个Singleton是非常合适的,可以节省不必要的对象创建开销:
class StringLengthComparator {
    private StringLengthComparator() { }
    public static final StringLengthComparator INSTANCE = new StringLengthComparator();
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

为了把StringLengthComparator实例传递给方法,需要适当的参数类型。使用StringLengthComparator并不好,因为客户端将无法传递任何其他的比较策略。相反,我们需要定义一个Comparator接口,并修改StringLengthComparator来实现这个接口。换句话说,我们在设计具体的策略类时,还需要定义一个策略接口(strategy interface)。

因为策略接口被用做具体策略实例的类型,所以我们并不需要为了导出具体策略,而把具体策略类做成公有的。相反,“宿主类(host class)”还可以导出公有的静态域(或者静态工厂方法),其类型为策略接口,具体的策略类可以是宿主类的私有嵌套类。下面的例子使用静态成员类,而不是匿名类,以便允许具体的策略类实现第二个接口Seriablizable:

// Exporting a concrete strategy
class Host {
    private static class StrLenCmp implements Comparator<String>, Serializable {
        public int compare(String s1, String s2) {
            return s1.length() - s2.length();
        }
    }
    // Returned comparator is serializable
    public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();

    ... // Bulk of class omitted

String类利用这种模式,通过它的CASE_INTENSITIVE_ORDER域,导出一个不区分大小写的字符串比较器。

简而言之,函数指针的主要途径就是实现策略(Strategy)模式。为了在Java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。

第22条:优先考虑静态成员类

  1. 嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和局部类(local class)。除了第一种之外,其他三种都被称为内部类(inner class)。
  2. 静态成员类的一种常见用法是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。例如,考虑一个枚举,它描述了计算器支持的各种操作。Operation枚举应该是Calculator类的公有静态成员类,然后,Calculator类的客户端就可以用诸如Calculator.Operation.PLUS和Calculator.Operation.MINUS这样的名称来引用这些操作。
  3. 非静态成员类的每个实例都隐含着与外围类的一个外围实例(enclosing instance)相关联。
  4. 如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。
  5. 非静态成员类的一种常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例。例如,Map接口的实现往往使用非静态成员类来实现它们的集合视图(collection view),这些集合视图是由Map的keySet、entrySet和Values方法返回的。同样地,诸如Set和List这种集合接口的实现往往也使用非静态成员类来实现它们的迭代器(iterator):
// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
    ... // Bulk of the class omitted
    public Iterator<E> iterator() {
        return new MyIterator();
    }
    private class MyIterator implements Iterator<E> {
        ...
    }
}

如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类。

私有静态成员类的一种常见用法是用来代表外围类所代表的对象的组件。例如,考虑一个Map实例,它把键(key)和值(value)关联起来。许多Map实现的内部都有一个Entry对象,对应于Map中的每个键-值对。虽然每个entry都与一个Map关联,但是entry上的方法(getKey、getValue和setValue)并不需要访问该Map。因此,使用非静态成员来表示entry是很浪费的:私有的静态成员类是最佳的选择。如果不小心漏掉了entry声明中的static修饰符,该Map仍然可以工作,但是每个entry中将会包含一个指向该Map的引用,这样就浪费了空间和时间。

正如你所想象的,匿名类没有名字。它不是外围类的一个成员。

匿名类的另一种常见用法是创建过程对象(process object),比如Runnable、Thread或者TimerTask实例。

原文地址:http://www.jsdblog.com/article/63 

正文到此结束
Loading...