ITEM 30: FAVOR GENERIC METHODS
正如类可以是泛型的,方法也可以是范性的。操作参数化类型的静态实用程序方法通常是通用的。Collections 中的所有 “algorithm” 方法(如 binarySearch 和 sort )都是通用的。
编写泛型方法类似于编写泛型类型。考虑这个有缺陷的方法,它返回两个集合的并集:
// Uses raw types - unacceptable! (Item 26)
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
这个方法能通过编译,但会有两个warning:
“Union.java:5: warning: [unchecked] unchecked call to HashSet(Collection<? extends E>) as a member of raw type HashSet
Set result = new HashSet(s1);”
“Union.java:6: warning: [unchecked] unchecked call to addAll(Collection<? extends E>) as a member of raw type Set
result.addAll(s2); ”
要修复这些警告并使方法类型安全,请修改它的声明,以声明表示三个集合(两个参数和方法的返回值)的元素类型的类型参数,并在整个方法中使用这个类型参数。声明类型参数的类型参数列表位于方法的修饰符和返回类型之间。在本例中,类型参数列表为 <E>,返回类型设置为 Set<E>。对于泛型方法和泛型类型,类型参数的命名约定是相同的(item 29、item 68):
// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
对于简单的泛型方法来说,就是这样。此方法编译时不生成任何警告,并提供类型安全性和易用性。这里有一个简单的程序来测试这个方法。此程序不包含任何类型转换和编译,没有错误或警告:
// Simple program to exercise generic method
public static void main(String[] args) {
Set<String> guys = Set.of("Tom", "Dick", "Harry");
Set<String> stooges = Set.of("Larry", "Moe", "Curly");
Set<String> aflCio = union(guys, stooges);
System.out.println(aflCio);
}
当程序运行时,它将输出“[Moe, Tom, Harry, Larry, Curly, Dick]” (输出中元素的顺序依赖于实现)。
union方法的一个限制是这三个集合的类型(输入参数和返回值)必须完全相同。通过使用有界通配符类型(item 31),可以使方法更加灵活。
有时,您需要创建一个不可变但适用于许多不同类型的对象。因为泛型是通过擦除实现的(item 28),所以您可以为所有所需的类型参数化使用一个对象,但是您需要编写一个静态工厂方法来为每个请求的类型参数化重复分配对象。这个模式称为泛型单例工厂,用于函数对象(item 42),比如 Collections.reverseOrder,偶尔也用于 Collections.emptySet 之类的集合。
假设您想编写一个恒等函数分发器。库提供方法 Function.identity,所以没有理由自己写一个(item 59),但这样做是有益的。在请求一个新的标识函数对象时创建它是浪费时间的,因为它是无状态的。如果Java的泛型被具体化,那么每个类型都需要一个标识函数,但是由于它们被删除了,一个泛型单例就足够了。它是这样的:
// Generic singleton factory pattern
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() { return (UnaryOperator<T>) IDENTITY_FN;
}
IDENTITY_FN 转换为 UnaryFunction<T> 将产生一个未经检查的警告,因为UnaryOperator<Object> 对于每个 T 都不是一个 UnaryOperator<T>。但恒等函数很特别:它返回未修改的参数,所以我们知道它作为一个UnaryFunction<T>是类型安全的。因此,我们可以自信地抑制这个未经检查的的警告。一旦我们这样做了,代码编译就没有错误或警告。
下面是一个示例程序,它将我们的范型单例作为 UnaryOperator<String> 和 UnaryOperator<Number> 使用。与往常一样,它不包含强制转换和编译,没有错误或警告:
// Sample program to exercise generic singleton
public static void main(String[] args) {
String[] strings = { "jute", "hemp", "nylon" };
UnaryOperator<String> sameString = identityFunction();
for (String s : strings)
System.out.println(sameString.apply(s));
Number[] numbers = { 1, 2.0, 3L };
UnaryOperator<Number> sameNumber = identityFunction();
for (Number n : numbers)
System.out.println(sameNumber.apply(n));
}
允许类型参数由包含该类型参数本身的表达式约束,尽管这种情况相对较少。这就是所谓的递归类型边界。递归类型边界的一个常见用法是与 Comparable 接口结合使用,它定义了类型的自然顺序(item 14)。该接口如下图所示:
public interface Comparable<T> {
int compareTo(T o);
}
类型参数T定义了实现 Comparable 的类型的元素可以与之进行比较的类型。实际上,几乎所有类型都只能与自己类型的元素进行比较。例如,String实现Comparable, Integer实现Comparable,等等。
许多方法采用实现 Comparable 的元素集合来排序、在其中搜索、计算其最小值或最大值,等等。要做这些事情,需要集合中的每个元素都与集合中的其他元素相比较,换句话说,列表中的元素必须相互比较。下面是如何表达这种约束:
// Using a recursive type bound to express mutual comparability
public static <E extends Comparable<E>> E max(Collection<E> c);
类型绑定 <E extends Comparable<E>> 可以被解读为 “任何可以与自身进行比较的类型E”,这或多或少与相互可比性的概念相对应。
下面是与前面的声明一起使用的方法。它根据元素的自然顺序计算集合的最大值,编译时没有错误或警告:
// Returns max value in a collection - uses recursive type bound
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty())
throw new IllegalArgumentException("Empty collection");
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return result;
}
注意,如果列表为空,此方法将抛出 IllegalArgumentException。更好的替代方法是返回一个 Optional<E> (item 55)。
递归类型界限可以变得更加复杂,但幸运的是它们很少这样做。如果您理解这个习惯用法、它的通配符变体(item 31)和模拟的自类型习惯用法(item 2),您就能够处理您在实践中遇到的大多数递归类型边界。
总之,与泛型类型一样,泛型方法比要求客户端对输入参数和返回值进行显式强制转换的方法更安全、更易于使用。与类型一样,您应该确保您的方法可以在没有强制类型转换的情况下使用,这通常意味着使它们成为泛型。与类型类似,您应该泛化使用强制类型转换的现有方法。这使新用户更容易使用,而不会破坏现有的客户代码(item 26)。
网友评论