NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();
builder模式模拟了具名的可选参数,就像Ada和Python中的一样。
// Builder Pattern
public class NutritionFacts {
private final int ServingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required Parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) { calories = val; return this; }
public Builder fat(int val) { fat = val; return this; }
public Builder carbohydrate(int val) { carbohydrate = val; return this; }
public Builder sodium(int val) { sodium = val; return this; }
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
//Singleton with public final field
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
私有构造器仅被调用一次,用来实例化公有的静态final域Elvis.INSTANCE。由于缺少公有的或者受保护的构造器,所以保证了Elvis的全局唯一性:一旦Elvis类被实例化,只会存在一个Elvis实例,不多也不少。
//Singleton with static factory
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() { ... }
}
公有域方法的主要好处在于,组成类的成员的声明很清楚地表明了这个类是一个Singleton:公有的静态域是final的,所以该域将总是包含相同的对象引用。公有域方法在性能上不再有任何优势:现代的JVM实现几乎都能够将静态工厂方法的调用内联化。
为了使利用这其中一种方法实现的Singleton类变成是可序列化的(Serializable),仅仅在声明中加上“implements Serializable”是不够的。为了维护并保证Singleton,必须声明所有实例域都是瞬时(transient)的,并提供一个readResolve方法。
从Java 1.5发行版本起,实现Singleton还有第三种方法。只需编写一个包含单个元素的枚举类型:
//Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
// Noninstantiable utility class
public class UtilityClass {
//Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
}
... //Remainder omitted
}
class Person {
private final Date birthDate;
// Other fields, methods, and constructor omitted
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0 &&
birthDate.compareTo(BOOM_END) < 0;
}
}
改进后的Person类只在初始化的时候创建Calendar、TimeZone和Date实例一次,而不是在每次调用isBabyBoomer的时候都创建这些实例。
把boomStart和boomEnd从局部变量改为final静态域,这些日期显然是被作为常量对待,从而使得代码更易于理解。
如果改进后的Person类被初始化了,它的isBabyBoomer方法却永远不会被调用,那就没有必要初始化BOOM_START和BOOM_END域。通过延迟初始化(lazily initializing),即把对这些域的初始化延迟到isBabyBoomer方法第一次被调用的时候进行,则有可能消除这些不必要的初始化工作,但是不建议这样做。正如延迟初始化中常见的情况一样,这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平。
考虑适配器(adpater)的情形,有时也叫做视图(view)。适配器是指这样一个对象:它把功能委托给一个后备对象(backing object),从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。
例如,Map接口的keySet方法返回该Map对象的Set视图,其中包含该Map中所有的键(key)。粗看起来,好像每次调用keySet都应该创建一个新的Set实例,但是,对于一个给定的Map对象,实际上每次调用keySet都返回同样的Set实例。虽然被返回的Set实例一般是可改变的,但是所有返回的对象在功能上是等同的:当其中一个返回对象发生变化的时候,所有其他的返回对象也要发生变化,因为它们是由同一个Map实例支撑的。
变量sum被声明成Long而不是long,意味着程序构造了大约 231 个多余的Long实例(大约每次往Long sum中增加long时构造一个实例)。结论很明显:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
真正正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。而且,数据库的许可可能限制你只能使用一定数量的连接。但是,一般而言,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用(footprint),并且还会损害性能。现代的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。
public boolean equals(Object obj) {
return (this == obj);
}
java.util.AbstractSet的equals方法:
public boolean equals(Object o) {
if (o==this)
return true;
if(!(o instanceof Set))
return false;
Collection c = (Collection) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
java.util.AbstractMap的equals方法:
public boolean equals(Object o) {
if (o==this)
return true;
if (!(o instanceof Map))
return false;
Map m = (Map) o;
if (m.size()!=size())
return false;
try {
Iterator> i = entrySet().iterator();
while(i.hasNext()) {
Entry e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value==null) {
if(!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if(!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。在这种情况下,无疑是应该覆盖equals方法的,以防它被意外调用:
@override public boolean equals(Object o) {
throw new AssertionError(); //Method is never called
}
这通常属于“值类(value class)”的情形。值类仅仅是一个表示值的类,例如Integer或者Date。程序员在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。
有一种“值类”不需要覆盖equals方法,即用实例受控确保“每个值至多只存在一个对象”的类。枚举类型就属于这种类。对于这样的类而言,逻辑相同与对象等同是一回事,因此Object的equals方法等同于逻辑意义上的equals方法。
对于既不是float也不是double类型的基本类型域,可以使用==操作符进行比较;对于对象引用域,可以递归地调用equals方法;对于float域,可以使用Float.compare方法;对于double域,则使用Double.compare。对float和double域进行特殊的处理是有必要的,因为存在着Float.NaN、-0.0f以及类似的double常量;详细信息请参考Float.equals的文档。对于数组域,则要把以上这些指导原则应用到每个元素上。如果数组域中的每个元素都很重要,就可以使用发行版本1.5中新增的其中一个Arrays.equals方法。
有些对象引用域包含null可能是合法的,所以,为了避免可能导致NullPointerException异常,则使用下面的习惯用法来比较这样的域:
(field==null ? o.field==null : field.equals(o.field))
域的比较顺序可能会影响到equals方法的性能。为了获得最佳的性能,应该最先比较最有可能不一致的域,或者是开销最低的域,最理想的情况是两个条件同时满足的域。你不应该去比较那些不属于对象逻辑状态的域,例如用于同步操作的Lock域。
覆盖equals时总要覆盖hashCode。
把任何一种别名形式考虑到等价的范围内,往往不会是个好主意。例如,File类不应该试图把指向同一个文件的符号链接(symbolic link)当作相等的对象来看。所幸File类没有这样做。
public boolean equals(MyClass o) {
...
}
问题在于,这个方法并没有覆盖Object.equals,因为它的参数应该是Object类型,相反,它重载(overload)了Object.equals。在原有equals方法的基础上,再提供一个“强类型(strongly typed)”的equals方法,只要这两个方法返回同样的结果,那么这就是可以接受的。在某些特定的情况下,它也许能够稍微改善性能,但是与增加的复杂性相比,这种做法是不值得的。@override注解的用法一致,就如本条目中所示,可以防止犯这种错误。这个equals方法不能编译,错误消息会告诉你到底哪里出了问题:
@Override public boolean equals(MyClass o) {
...
}
由于PhoneNumber类没有覆盖hashCode方法,从而导致两个相等的实例具有不相等的散列码,违反了hashCode的约定。因此,put方法把电话号码对象存放在一个散列桶(hash bucket)中,get方法却在另一个散列桶中查找这个电话号码。即使这两个实例正好被放到同一个散列桶中,get方法也必定会返回null,因为HashMap有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也不必检验对象的等同性。
- 把某个非零的常量值,比如说17,保存在一个名为result的int类型的变量中。
- 对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤:
a. 为该域计算int类型的散列码c:
i. 如果该域是boolean类型,则计算(f ? 1:0)。
ii. 如果该域是byte、char、short或者int类型,则计算(int)f。
iii. 如果该域是long类型,则计算(int)(f ^ (f>>>32))。
iv. 如果该域是float类型,则计算Float.floatToIntBits(f)。
v. 如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2.a.iii,为得到的long类型值计算散列值。
vi. 如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式(canonical representation)”,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常数,但通常是0)。
vii. 如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤b.2中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法。
b. 按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:
result=31×result+c;
返回result。
必须排除equals比较计算中没有用到的任何域,否则很有可能违反hashCode约定的第二条。
public int hashCode() {
int h = hash; //hash: cache the hash code for the string
if (h==0) { //h==0说明是第一次计算哈希值,hash域还没有缓存
int off = offset; // the offset is the first index of the storage that is used.
char val[] = value; // the value is used for character storage.
int len = count;
for (int i=0;i31*h + val[off++];
}
hash = h;
}
return h;
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
虽然被克隆对象有它自己的散列桶数组,但是,这个数组引用的链表与原始对象是一样的,从而很容易引起克隆对象和原始对象中不确定的行为。为了修正这个问题,必须单独地拷贝并组成每个桶的链表。下面是一种常见的做法:
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
// Iteratively copy the linked list headed by this Entry
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next)
p.next = new Entry(p.next.key, p.next.value, p.next.next);
return result;
}
}
@Override public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for(int i=0;iif (buckets[i] != null)
result.buckets[i] = buckets[i].deepCopy();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
... // Remainder omitted
}
如果专门为了继承而设计的类覆盖了clone方法,覆盖版本的clone方法就应该模拟Object.clone的行为:它应该被声明为protected、抛出CloneNotSupportedException异常,并且该类不应该实现Cloneable接口。这样做可以使子类具有实现或不实现Cloneable接口的自由,就仿佛它们直接扩展了Object一样。
简而言之,所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。此公有方法首先调用super.clone,然后修正任何需要修正的域。一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用代替原来指向这些对象的引用。
拷贝构造器的做法,以及静态工厂方法的变形,都比Cloneable/clone方法具有更多的优势:它们不依赖于某一种很有风险的、语言之外的对象创建机制;它们不要求遵守尚未制定好文档的规范;它们不会与final域的正常使用发生冲突;它们不会抛出不必要的受检查异常(checked exception);它们不需要进行类型转换。虽然你不可能把拷贝构造器或者静态工厂放到接口中,但是由于Cloneable接口缺少一个公有的clone方法,所以它也没有提供一个接口该有的功能。因此,使用拷贝构造器或者拷贝工厂来代替clone方法时,并没有放弃接口的功能特性。
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
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)已经完成,或者已经被取消,就不可能再对它重新调度。
// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set 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 s;
public ForwardingSet(Set 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 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[] 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 s = new InstrumentedSet(new TreeSet(cmp));
Set s2 = new InstrumentedSet(new HashSet(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来隐藏这些缺陷。
为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。
// 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就是个例子。简单实现就像个骨架实现,这是因为它实现了接口,并且是为了继承而设计的,但是区别在于它不是抽象的:它是最简单的可能的有效实现。你可以原封不动地使用,也可以看情况将它子类化。
使用抽象类来定义允许多个实现的类型,与使用接口相比有一个明显的优势:抽象类的演变比接口的演变要容易得多。如果在后续的发行版本中,你希望在抽象类中增加新的方法,始终可以增加具体方法,它包含合理的默认实现。然后,该抽象类的所有现有实现都将提供这个新的方法。对于接口,这样做是行不通的。
一般来说,要想在公有接口中增加方法,而不破坏实现这个接口的所有现有的类,这是不可能的。之前实现该接口的类将会漏掉新增加的方法,并且无法再通过编译。在为接口增加新方法的同时,也为骨架实现类增加同样的新方法,这样可以在一定程度上减小由此带来的破坏,但是,这样做并没有真正解决问题。所有不从骨架实现类继承的接口实现仍然会遭到破坏。
简而言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性和功能更为重要的时候。在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。最后,应该尽可能谨慎地设计所有的公有接口,并通过编写多个实现来对它们进行全面的测试。
// 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;
}
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_LENGTH_COMPARATOR = new StrLenCmp();
... // Bulk of class omitted
String类利用这种模式,通过它的CASE_INTENSITIVE_ORDER域,导出一个不区分大小写的字符串比较器。
简而言之,函数指针的主要途径就是实现策略(Strategy)模式。为了在Java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。
// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
... // Bulk of the class omitted
public Iterator 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实例。
在没有泛型之前,从集合中读取到的每一个对象都必须进行转换。如果有人不小心插入了类型错误的对象,在运行时的转换处理就会出错。有了泛型之后,可以告诉编译器每个集合中接受哪些对象类型。编译器自动地为你的插入进行转化,并在编译时告知是否插入了类型错误的对象。
有限制类型参数
递归类型限制>
有限制通配符类型 List extends Number>
泛型方法 staticList asList(E[] a)
类型令牌 String.class
// Won't compile - wildcards can require change in method body!
public static super T>> T max(List extends T> list) {
Iterator i = list.iterator();
T result = i.next();
while(i.hasNext()) {
T t = i.next();
if(t.compareTo(result) > 0)
result = t;
}
return result;
}
它意味着list不是一个List
Iterator extends T> i = list.iterator();
问题在于list的类型为List>,你不能把null之外的任何值放到List>中。幸运的是,有一种方式可以实现这个方法,无需求助于不安全的转换或者原生态类型(raw type)。这种想法就是编写一个私有的辅助方法来捕捉通配符类型。为了捕捉类型,辅助方法必须是泛型方法,像下面这样:
public static void swap(List> list, int i, int j) {
swapHelper(list, i, j);
}
// Private helper method for wildcard capture
private static void swapHelper(List list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
还要记住所有的comparable和comparator都是消费者。
// Typesafe heterogeneous container pattern - implementation
public class Favorites {
private Map, Object> favorites = new HashMap, Object>();
public void putFavorite(Class type, T instance) {
if (type==null)
throw new NullPointerException("Type is null");
favorites.put(type, instance);
}
public T getFavorite(Class type) {
return type.cast(favorites.get(type));
}
}
这里发生了一些微妙的事情。每个Favorites实例都得到一个称作favorites的私有Map
它先从favorites映射中获得与指定Class对象相对应的值。这正是要返回的对象引用,但它的编译时类型是错误的。它的类型只是Object(favorites映射的值类型),我们需要返回一个T。因此,getFavorite方法的实现利用Class的cast方法,将对象引用动态地转换(dynamically cast)成了Class对象所表示的类型。
cast方法是Java的cast操作符的动态模拟。它只检验它的参数是否为Class对象所表示的类型的实例。如果是,就返回参数;否则就抛出ClassCastException异常。
换句话说,你可以保存最喜爱的String或者String[],但不能保存最喜爱的List
Java 1.5发行版本中增加了两个新的引用类型家族:一种新的类称作枚举类型(enum type),一种新的接口称作注解类型(annotation type)。
// Enum type with data and behavior
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+24, 6.052e6),
EARTH(5.975e+24, 6.378e6),
MARS(6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN(5.685e+26, 6.027e7),
URANUS(8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius*radius);
}
public double mass() { return mass; }
public double radius() { return radius; }
public double surfaceGravity() { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。枚举天生就是不可变的,因此所有的域都应该为final的。
注意Plant就像所有的枚举一样,它有一个静态的values方法,按照声明顺序返回它的值数组。还要注意toString方法返回每个枚举值的声明名称,使得println和printf的打印变得更加容易。如果你不满意这种字符串表示法,可以通过覆盖toString方法对它进行修改。
幸运的是,有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体(constant-specific class body)中,用具体的方法覆盖每个常量的抽象apply方法。这种方法被称作特定于常量的方法实现(constant-specific method implementation):
// Enum type with constant-specific method implementations
public enum Operation {
PLUS { double apply(double x, double y){return x+y;}},
MINUS { double apply(double x, double y){return x-y;}},
TIMES { double apply(double x, double y){return x*y;}},
DIVIDE { double apply(double x, double y){return x/y;}};
abstract double apply(double x, double y);
}
即使你真的忘记了,编译器也会提醒你,因为枚举类型中的抽象方法必须被它所有常量中的具体方法所覆盖。
特定于常量的方法实现可以与特定于常量的数据结合起来。例如,下面的Operation覆盖了toString来返回通常与该操作关联的符号:
// Enum type with constant-specific class bodies and data
public enum Operation {
PLUS("+") {
double apply(double x, double y) { return x+y; }
},
MINUS("-") {
double apply(double x, double y) { return x-y; }
},
TIMES("*") {
double apply(double x, double y) { return x*y; }
},
DIVIDE("/") {
double apply(double x, double y) { return x/y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
abstract double apply(double x, double y);
}
枚举类型有一个自动产生的valueOf(String)方法,它将常量的名字转变成常量本身。
枚举构造器不可以访问枚举的静态域,除了编译时常量域之外。这一限制是有必要的,因为构造器运行的时候,这些静态域还没有被初始化。
你真正想要的就是每当添加一个枚举常量时,就强制选择一种加班报酬策略。幸运的是,有一种很好的方法可以实现这一点。这种想法就是将加班工资计算移到一个私有的嵌套枚举中,将这个策略枚举(strategy enum)的实例传到PayrollDay枚举的构造器中。之后PayrollDay枚举将加班工资计算委托给策略枚举,PayrollDay中就不需要switch语句或者特定于常量的方法实现了。虽然这种模式没有switch语句那么简洁,但更加安全,也更加灵活:
// The strategy enum pattern
enum PayrollDay {
MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY),
FRIDAY(PayType.WEEKDAY),
SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
// The strategy enum type
private enum PayType {
WEEKDAY {
double overtimePay(double hours, double payRate) {
return hours<=HOURS_PER_SHIFT ? 0 :
(hours - HOURS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
double overtimePay(double hours, double payRate) {
return hours * payRate / 2;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double overtimePay(double hrs, double payRate);
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
}
枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为。
那么什么时候应该使用枚举呢?每当需要一组固定常量的时候。当然,这包括“天然的枚举类型”,例如行星、一周的天数以及棋子的数目等等。但它也包括你在编译时就知道其所有可能值得其他集合,例如菜单的选项、操作代码以及命令行标记等。枚举类型中的常量集并不一定要始终保持不变。专门设计枚举特性是考虑到枚举类型的二进制兼容演变。
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
Enum规范中谈到ordinal时这么写道:“大多数程序员都不需要这个方法。它是设计成用于像EnumSet和EnumMap这种基于枚举的通用数据结构的。”除非你在编写的是这种数据结构,否则最好完全避免使用ordinal方法。
// EnumSet - a modern replacement for bit fields
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set