与 C 和 C++ 等更传统的语言相比,Java 甚至更需要度量尝试优化的效果,因为 Java 的性能模型更弱:各种基本操作的相对成本没有得到很好的定义。程序员编写的内容和 CPU 执行的内容之间的「抽象鸿沟」更大,这使得可靠地预测优化的性能结果变得更加困难。有很多关于性能的传说流传开来,但最终被证明是半真半假或彻头彻尾的谎言。
// Broken - Inappropriate use of inheritance! publicclassInstrumentedHashSet<E> extendsHashSet<E> { // The number of attempted element insertions privateintaddCount=0;
@OverridepublicbooleanaddAll(Collection<? extends E> c) { addCount += c.size(); returnsuper.addAll(c); }
publicintgetAddCount() { return addCount; } }
InstrumentedSet 类的设计是通过存在的 Set 接口来实现的,该接口包含 HashSet 类的功能特性。除了功能强大,这个设计是非常灵活的。InstrumentedSet 类实现了 Set 接口,并有一个构造方法,其参数也是 Set 类型的。本质上,这个类把 Set 转换为另一个类型 Set, 同时添加了计数的功能。与基于继承的方法不同,该方法仅适用于单个具体类,并且父类中每个需要支持构造方法,提供单独的构造方法,所以可以使用包装类来包装任何 Set 实现,并且可以与任何预先存在的构造方法结合使用:
1 2
Set<Instant> times = newInstrumentedSet<>(newTreeSet<>(cmp)); Set<E> s = newInstrumentedSet<>(newHashSet<>(INIT_CAPACITY));
InstrumentedSet 类甚至可以用于临时替换没有计数功能下使用的集合实例:
1 2 3 4
staticvoidwalk(Set<Dog> dogs) { InstrumentedSet<Dog> iDogs = newInstrumentedSet<>(dogs); ... // Within this method use iDogs instead of dogs }
只有在子类真的是父类的子类型的情况下,继承才是合适的。 换句话说,只有在两个类之间存在「is-a」关系的情况下,B 类才能继承 A 类。 如果你试图让 B 类继承 A 类时,问自己这个问题:每个 B 都是 A 吗? 如果你不能明确的以“是的”来回答这个问题,那么 B 就不应该继承 A。如果答案是否定的,那么 B 通常包含一个 A 的私有实例,并且暴露一个不同的 API :A 不是 B 的重要部分 ,只是其实现细节。
// Unnecessary functional interface; use a standard one instead. @FunctionalInterface interfaceEldestEntryRemovalFunction<K,V>{ booleanremove(Map<K,V> map, Map.Entry<K,V> eldest); }
It is imperative that the user manually synchronize on the returned map when iterating over any of its collection views:
当用户遍历其集合视图时,必须手动同步返回的 Map:
1 2 3 4 5 6 7
Map<K, V> m = Collections.synchronizedMap(newHashMap<>()); Set<K> s = m.keySet(); // Needn't be in synchronized block ... synchronized(m) { // Synchronizing on m, not s! for (K key : s) key.f(); }
public E pop() { if (size == 0) thrownewEmptyStackException(); Eresult= elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; } ... // no changes in isEmpty or ensureCapacity }
// The elements array will contain only E instances from push(E). // This is sufficient to ensure type safety, but the runtime // type of the array won't be E[]; it will always be Object[]! @SuppressWarnings("unchecked") publicStack() { elements = (E[]) newObject[DEFAULT_INITIAL_CAPACITY]; }
// Little program to exercise our generic Stack publicstaticvoidmain(String[] args) { Stack<String> stack = newStack<>(); for (String arg : args) stack.push(arg); while (!stack.isEmpty()) System.out.println(stack.pop().toUpperCase()); }
// Uses raw types - unacceptable! [Item 26] publicstatic Set union(Set s1, Set s2) { Setresult=newHashSet(s1); result.addAll(s2); return result; }
此方法可以编译但有两个警告:
1 2 3 4 5 6 7 8
Union.java:5: warning: [unchecked] unchecked call to HashSet(Collection<? extends E>) as a member of raw type HashSet Setresult=newHashSet(s1); ^ Union.java:6: warning: [unchecked] unchecked call to addAll(Collection<? extends E>) as a member of raw type Set result.addAll(s2); ^
// Returns max value in a collection - uses recursive type bound publicstatic <E extendsComparable<E>> E max(Collection<E> c) { if (c.isEmpty()) thrownewIllegalArgumentException("Empty collection"); Eresult=null; for (E e : c) if (result == null || e.compareTo(result) > 0) result = Objects.requireNonNull(e); return result; }
Chooser.java:9: error: incompatible types: Object[] cannot be converted to T[] choiceArray = choices.toArray(); ^ where T is a type-variable: T extendsObject declared in classChooser
没什么大不了的,将 Object 数组转换为 T 数组:
1
choiceArray = (T[]) choices.toArray();
这没有了错误,而是得到一个警告:
1 2 3 4 5 6
Chooser.java:9: warning: [unchecked] unchecked cast choiceArray= (T[]) choices.toArray(); ^ required: T[], found: Object[] where T is a type-variable: T extendsObject declared in classChooser
编译器告诉你在运行时不能保证强制转换的安全性,因为程序不会知道 T 代表什么类型——记住,元素类型信息在运行时会被泛型删除。 该程序可以正常工作吗? 是的,但编译器不能证明这一点。 你可以证明这一点,在注释中提出证据,并用注解来抑制警告,但最好是消除警告的原因(详见第 27 条)。
在CollectionClassifier示例中,程序的目的是通过基于参数的运行时类型自动调度到适当的方法重载来辨别参数的类型,就像 Wine 类中的 name 方法一样。 方法重载根本不提供此功能。 假设需要一个静态方法,修复CollectionClassifier程序的最佳方法是用一个执行显式instanceof测试的方法替换classify的所有三个重载:
1 2 3 4
publicstatic String classify(Collection<?> c) { return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection"; }
因为重写是规范,而重载是例外,所以重写设置了人们对方法调用行为的期望。 正如CollectionClassifier示例所示,重载很容易混淆这些期望。 编写让程序员感到困惑的代码的行为是不好的实践。 对于 API 尤其如此。 如果 API 的日常用户不知道将为给定的参数集调用多个方法重载中的哪一个,则使用 API 可能会导致错误。 这些错误很可能表现为运行时的不稳定行为,许多程序员很难诊断它们。 因此,应该避免混淆使用重载。
for (inti=0; i < 3; i++) { set.remove(i); list.remove((Integer) i); // or remove(Integer.valueOf(i)) }
前一个示例所演示的令人混乱的行为是由于List<E>接口对remove方法有两个重载:remove(E)和remove(int)。在 Java 5 之前,当 List 接口被“泛型化”时,它有一个remove(Object)方法代替remove(E),而相应的参数类型 Object 和 int 则完全不同。但是,在泛型和自动装箱的存在下,这两种参数类型不再完全不同了。换句话说,在语言中添加泛型和自动装箱破坏了 List 接口。幸运的是,Java 类库中的其他 API 几乎没有受到类似的破坏,但是这个故事清楚地表明,自动装箱和泛型在重载时增加了谨慎的重要性。
正如在条目 45 中所讨论的,一些任务最好使用 Stream 来完成,一些任务最好使用迭代。下面是一个传统的 for 循环来遍历一个集合:
1 2 3 4 5
// Not the best way to iterate over a collection! for (Iterator<Element> i = c.iterator(); i.hasNext(); ) { Elemente= i.next(); ... // Do something with e }
下面是迭代数组的传统 for 循环的实例:
1 2 3 4
// Not the best way to iterate over an array! for (inti=0; i < a.length; i++) { ... // Do something with a[i] }
这些习惯用法比 while 循环更好(详见第 57 条),但是它们并不完美。迭代器和索引变量都很混乱——你只需要元素而已。此外,它们也代表了出错的机会。迭代器在每个循环中出现三次,索引变量出现四次,这使你有很多机会使用错误的变量。如果这样做,就不能保证编译器会发现到问题。最后,这两个循环非常不同,引起了对容器类型的不必要注意,并且增加了更改该类型的小麻烦。
for-each 循环(官方称为「增强的 for 语句」)解决了所有这些问题。它通过隐藏迭代器或索引变量来消除混乱和出错的机会。由此产生的习惯用法同样适用于集合和数组,从而简化了将容器的实现类型从一种转换为另一种的过程:
1 2 3 4
// The preferred idiom for iterating over collections and arrays for (Element e : elements) { ... // Do something with e }
当看到冒号(:) 时,请将其读作「in」。因此,上面的循环读作「对于元素 elements 中的每个元素 e」。使用 for-each 循环不会降低性能,即使对于数组也是如此:它们生成的代码本质上与手工编写的代码相同。
当涉及到嵌套迭代时,for-each 循环相对于传统 for 循环的优势甚至更大。下面是人们在进行嵌套迭代时经常犯的一个错误:
1 2 3 4 5 6 7 8 9 10 11 12
// Can you spot the bug? enumSuit { CLUB, DIAMOND, HEART, SPADE } enumRank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING } ... static Collection<Suit> suits = Arrays.asList(Suit.values()); static Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = newArrayList<>(); for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); ) deck.add(newCard(i.next(), j.next()));
/ Fixed, but ugly - you can do better! for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) { Suitsuit= i.next(); for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); ) deck.add(newCard(suit, j.next())); }
相反,如果使用嵌套 for-each 循环,问题就会消失。生成的代码也尽可能地简洁:
1 2 3 4
// Preferred idiom for nested iteration on collections and arrays for (Suit suit : suits) for (Rank rank : ranks) deck.add(newCard(suit, rank));