第五章 泛型
27. 消除非检查警告
28. 列表优于数组
数组是协变的,泛型是不变的
- 数组在两个重要方面与泛型不同。 首先,数组是协变的(covariant)。 这个吓人的单词意味着如果 Sub 是 Super 的子类型,则数组类型
Sub[]
是数组类型Super[]
的子类型。 - 相比之下,泛型是不变的(invariant): 对于任何两种不同的类型 List 和 List ,既不是 List 的子类型也不是父类型。 你可能认为这意味着泛型是不足的,但可以说是数组缺陷。
数组被具体化了,
- 意味着数组在运行时知道并强制 执行它们的元素类型。如果尝试将一个 String 放入 Long 数组中,得到一个
ArrayStoreException 异常。 - 相反,泛型通过擦除(erasure)来实现。 这意味着它们只在编译时执行 类型约束,并在运行时丢弃(或擦除)它们的元素类型信息。 擦除是允许泛型类型与不使用泛型的遗留代码自由互 操作(条目 26),从而确保在 Java 5 中平滑过渡到泛型。
具体见:Java 泛型的实现原理
数组和泛型具有非常不同的类型规则。 数组是协变和具体化的; 泛型是不变的,类型擦除的。 因此,数组 提供运行时类型的安全性,但不提供编译时类型的安全性,反之亦然。 一般来说,数组和泛型不能很好地混合工 作。 如果你发现把它们混合在一起,得到编译时错误或者警告,你的第一个冲动应该是用列表来替换数组。
29. 优先考虑泛型
泛型类型比需要在客户端代码中强制转换的类型更安全,更易于使用。 当你设计新的类型时,确保它们 可以在没有这种强制转换的情况下使用。 这通常意味着使类型泛型化。 如果你有任何现有的类型,应该是泛型的但 实际上却不是,那么把它们泛型化。 这使这些类型的新用户的使用更容易,而不会破坏现有的客户端(条目 26)
30. 优先使用泛型方法
像泛型类型一样,泛型方法比需要客户端对输入参数和返回值进行显式强制转换的方法更安全,更易于使 用。 像类型一样,你应该确保你的方法可以不用强制转换,这通常意味着它们是泛型的。 应该泛型化现有的方法, 其使用需要强制转换。 这使得新用户的使用更容易,而不会破坏现有的客户端(条目 26)
31. 使用限定通配符来增加 API 的灵活性
在你的 API 中使用通配符类型,虽然棘手,但使得 API 更加灵活。 如果编写一个将被广泛使用的类库,正 确使用通配符类型应该被认为是强制性的。 记住基本规则: producer-extends, consumer-super(PECS)。 还要记 住,所有 Comparable 和 Comparator 都是消费者。
33. 优先考虑类型安全的异构容器
Favorites 实例是类型安全的:当你请求一个字符串时它永远不会返回一个整数。 它也是异构的:与普通 Map 不同,所有的键都是不同的类型。 因此,我们将 Favorites 称为类型安全异构容器(typesafe heterogeneous container)。
// Typesafe heterogeneous container pattern - implementation
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public<T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public<T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
} }
泛型 API 的通常用法(以集合 API 为例)限制了每个容器的固定数量的类型参数。 你可以通过将类型参数 放在键上而不是容器上来解决此限制。 可以使用 Class 对象作为此类型安全异构容器的键。 以这种方式使用的
对象称为类型令牌。 也可以使用自定义键类型。 例如,可以有一个表示数据库行(容器)的 类型和一个泛型类型 Column 作为其键。
第六章 枚举和注解
34. 使用枚举类型替代整型常量
35. 使用实例属性替代序数
枚举规范对此 方法说道:“大多数程序员对这种方法没有用处。 它被设计用于基于枚举的通用数据结 构,如 EnumSet 和 。“除非你在编写这样数据结构的代码,否则最好避免使用 ordinal 方法。
//这样写
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;
}
}
36. 使用 EnumSet 替代位属性
37. 使用 EnumMap 替代序数索引
如果Map的key是enum类型,推荐使用EnumMap,既保证速度,也不浪费空间。
38. 使用接口模拟可扩展的枚举
// 暂略~~
第七章 lambda 表达式和 Stream流
42. lambda 表达式优于匿名类
- 匿名类实现的排序:
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
} });
匿名类适用于需要函数对象的经典面向对象设计模式,特别是策略模式
。然而,匿名类的冗长,使得 Java 中的函数式编程成为一种吸引人的 前景。 在 Java 8 中,语言形式化了这样的概念,即使用单个抽象方法的接口是特别的,应该得到特别的对 待。 这些接口现在称为函数式接口,并且该语言允许你使用 lambda 表达式或简称 lambda 来创建这些接口的实例。 Lambdas 在功能上与匿名类相似,但更为简洁。 下面的代码使用 lambdas 替换上面的匿名类。 样板不见了,行为清晰明了:
- lambda 实现排序
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words,
(s1, s2) -> Integer.compare(s1.length(), s2.length()));
使用比较器构造 方法代替 lambda,则代码中的比较器可以变得更加简洁(条目 14,43)
Collections.sort(words, comparingInt(String::length));
lambda 不要超过三四行,另外,就是如果需要访问实例属性或方法,那么常量 特定的类主体仍然是行之有效的方法。
从 Java 8 开始,lambda 是迄今为止表示小函数对象的最佳方式。 除非必须创建非函数式接口类型 的实例,否则不要使用匿名类作为函数对象。
43. 方法引用优于 lambda 表达式
lambda 优于匿名类的主要优点是它更简洁。Java 提供了一种生成函数对象的方法,比 lambda 还要简洁,那就 是:方法引用( method references)。
下面是一段程序代码片段,它维护一个从任意键到整数值的映射。如果将该值 解释为键的实例个数,则该程序是一个多重集合的实现。该代码的功能是,根据键找到整数值,然后在此基础上加 1:
map.merge(key, 1, (count, incr) -> count + incr);
总之,方法引用通常为 lambda 提供一个更简洁的选择。 如果方法引用看起来更简短更清晰,请使用它们;否 则,还是坚持 lambda。
44. 优先使用标准的函数式接口
java.util.function 包提供了大量标准函数式接口供你使用
。 如果其中一个标准函数式接口完成这项工作,则通常应该优先使用它,而不是专门构建的函数式接口。
在 java.util.Function 中有43个接口。不能指望全部记住它们,但是如果记住了六个基本接口,就可以在需要它们时派生出其余的接口
。基本接口操作于对象引用类型。
- Operator 接口表示方法的结果和参数类型相同。
- Predicate 接口表示其方法接受一个参数并返回一个布尔值。
- Function 接口表示方法其参数和返回类型不同。
- Supplier 接口表示一个不接受参数和返回值 (或“供应”) 的方法。
- Consumer 表示该方法接受一个参数 而不返回任何东西,本质上就是使用它的参数。
六种基本函数式接口概述如下:
45. 明智审慎地使用 Stream
- 过度使用流使程序难于阅读和维护。
- 在没有显式类型的情况下,仔细命名 lambda 参数对于流管道的可读性至关重要。
- 但理想情况下,应该避免使用流来处理 char 值。
使用 Stream 的一些场景
- 统一转换元素序列
- 过滤元素序列
- 使用单个操作组合元素序列 (例如添加、连接或计算最小值)
- 将元素序列累积到一个集合中,可能通过一些公共属性将它们分组
- 在元素序列中搜索满足某些条件的元素
46. 优先考虑流中无副作用的函数(暂略)
47. 优先使用 Collection 而不是 Stream 来作为方法的 返回类型
在编写返回元素序列的方法时,请记住,某些用户可能希望将它们作为流处理,而其他用户可能希望迭代 方式来处理它们。 尽量适应两个群体。 如果返回集合是可行的,请执行此操作。 如果已经拥有集合中的元素,或者 序列中的元素数量足够小,可以创建一个新的元素,那么返回一个标准集合,比如 ArrayList 。 否则,请考虑实 现自定义集合,就像我们为幂集程序里所做的那样。 如果返回集合是不可行的,则返回流或可迭代的,无论哪个看 起来更自然。 如果在将来的 Java 版本中, Stream 接口声明被修改为继承 Iterable ,那么应该随意返回流,因 为它们将允许流和迭代处理。
48. 谨慎使用流并行
通常,并行性带来的性能收益在 ArrayList 、 HashMap 、 HashSet 和 ConcurrentHashMap 实例、数 组、 int 类型范围和 long 类型的范围的流上最好
并行化一个流不仅会导致糟糕的性能,包括活性失败(liveness failures);它会导致不正确的结果和不可预知的 行为 (安全故障)。
在适当的情况下, 只需向流管道添加一个 parallel 方法调用,就可以实现处理器内核数量的近似线性加速。
总之,甚至不要尝试并行化流管道,除非你有充分的理由相信它将保持计算的正确性并提高其速度。不恰当地并行化流的代价可能是程序失败或性能灾难。如果您认为并行性是合理的,那么请确保您的代码在并行运行时保持正确,并在实际情况下进行仔细的性能度量。如果您的代码是正确的,并且这些实验证实了您对性能提高的怀疑,那么并且只有这样才能在生产代码中并行化流。
第八章 方法
49. 检查参数有效性
- 对于公共方法和受保护方法,请使用 Java 文档 注解来记在在违反参数值限制时将引发的异常(条目 74),通常,生成的异常是 ,
IndexOutOfBoundsException
或NullPointerException
- 在 Java 7 中添加的
Objects.requireNonNull
方 法灵活方便,因此没有理由再手动执行空值检查。 - 在 Java 9 中,java.util.Objects 类中添加了范围检查工具。 此工具包含三个方法:
checkFromIndexSize
,checkFromToIndex
和checkIndex
。 此工具不如空检查方法灵活。 它不允许指定自己的异常详细消息,它仅用于列表和数组索引
。 它不处理闭合范围(包含两个端点)。 但如果它能满足你的需要,那就很方便了。 - 检查方法中未使用但存储以供以后使用的参数的有效性尤为重要
50. 必要时进行防御性拷贝
每次编写在内部数据结构中存储对客户端提供的对象的引用的方法或构造函数时,请考虑客户端提供的对象是否可能是可变的。
- 如果是,请考虑在将对象输入数据结构后,你的类是 否可以容忍对象的更改。
- 如果答案是否定的,则必须防御性地拷贝对象,并将拷贝输入到数据结构中,以替代原始 数据结构。 例如,如果你正在考虑使用客户端提供的对象引用作为内部 set 实例中的元素或作为内部 map 实例中的键,您应该意识到如果对象被修改后插入,对象的 set 或 map 的不变量将被破坏。
在将内部组件返回给客户端之前进行防御性拷贝也是如此。无论你的类是否是不可变的,在返回对可访问的内部组件的引用之前,都应该三思。可能的情况是,应该返回一个防御性拷贝。记住,非零长度数组总是可变的。因此, 在将内部数组返回给客户端之前,应该始终对其进行防御性拷贝。或者,可以返回数组的不可变视图。这两项技术都 记载于条目 15。
51. 仔细设计方法签名
- 对于参数类型,优先选择接口而不是类(条目 64)。如果有一个合适的接口来定义一个参数,那么使用它来支持一个实现该接口的类。例如,没有理由在编写方法时使用 HashMap 作为输入参数,相反,而是使用 Map 作为参数,这 允许传入 HashMap、TreeMap、ConcurrentHashMap、TreeMap 的子 Map(submap)或任何尚未编写的 Map 实现。通过使用的类而不是接口,就把客户端限制在特定的实现中,如果输入数据碰巧以其他形式存在,则强制执行不必要 的、代价高昂的复制操作。
52. 明智审慎地使用重载
53. 明智审慎地使用可变参数
54. 返回空的数组或集合,不要返回 null
return. Collections.emptyXXXXXX()
- 总之,永远不要返回 null 来代替空数组或集合。它使你的 API 更难以使用,更容易出错,并且没有性能优势。
55. 明智审慎地返回 Optional
- 如果发现自己编写的方法不能总是返回值,并且认为该方法的用户在每次调用时考虑这种可能性很重要, 那么或许应该返回一个 Optional 的方法。但是,应该意识到,返回 Optional 会带来实际的性能后果;对于性能关键 的方法,最好返回 null 或抛出异常。最后,除了作为返回值之外,不应该在任何其他地方中使用 Optional。