30. 优先使用泛型方法
正如类可以是泛型的,方法也可以是泛型的。 对参数化类型进行操作的静态工具方法通常都是泛型的。 集合中的所有“算法”方法(如binarySearc
h和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; }
此方法可以编译但有两个警告:
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>
。 类型参数的命名约定对于泛型方法和泛型类型是相同的(条目 29和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
方法的一个限制是所有三个集合(输入参数和返回值)的类型必须完全相同。 通过使用限定通配符类型( bounded wildcard types)(条目 31),可以使该方法更加灵活。
有时,需要创建一个不可改变但适用于许多不同类型的对象。 因为泛型是通过擦除来实现的(条目 28),所以可以使用单个对象进行所有必需的类型参数化,但是需要编写一个静态工厂方法来重复地为每个请求的类型参数化分配对象。 这种称为泛型单例工厂(generic singleton factory)的模式用于方法对象( function objects)(条目 42),比如Collections.reverseOrder
方法,偶尔也用于Collections.emptySet
之类的集合。
假设你想写一个恒等方法分配器( identity function dispenser)。 类库提供了Function.identity
方法,所以没有理由编写你自己的实现(条目 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>
是类型安全的,无论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)); }
虽然相对较少,类型参数受涉及该类型参数本身的某种表达式限制是允许的。 这就是所谓的递归类型限制(recursive type bound)。 递归类型限制的常见用法与Comparable
接口有关,它定义了一个类型的自然顺序(条目 14)。 这个接口如下所示:
public interface Comparable<T> { int compareTo(T o); }
类型参数T
定义了实现Comparable <T>
的类型的元素可以比较的类型。 在实际中,几乎所有类型都只能与自己类型的元素进行比较。 所以,例如,String
类实现了Comparable <String>
,Integer
类实现了Comparable <Integer>
等等。
许多方法采用实现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](http://e.compareTo(result)) > 0) result = Objects.requireNonNull(e); return result; }
请注意,如果列表为空,则此方法将引发IllegalArgumentException
异常。 更好的选择是返回一个Optional<E>
(条目 55)。
递归类型限制可能变得复杂得多,但幸运的是他们很少这样做。 如果你理解了这个习惯用法,它的通配符变体(条目 31)和模拟的自我类型用法(条目 2),你将能够处理在实践中遇到的大多数递归类型限制。
总之,像泛型类型一样,泛型方法比需要客户端对输入参数和返回值进行显式强制转换的方法更安全,更易于使用。 像类型一样,你应该确保你的方法可以不用强制转换,这通常意味着它们是泛型的。 应该泛型化现有的方法,其使用需要强制转换。 这使得新用户的使用更容易,而不会破坏现有的客户端(条目 26)。
31. 使用限定通配符来增加API的灵活性
如条目 28所述,参数化类型是不变的。换句话说,对于任何两个不同类型的Type1
和Type
,List <Type1>
既不是List <Type2>
子类型也不是其父类型。尽管List <String>
不是List <Object>
的子类型是违反直觉的,但它确实是有道理的。 可以将任何对象放入List <Object>
中,但是只能将字符串放入List <String>
中。 由于List <String>
不能做List <Object>
所能做的所有事情,所以它不是一个子类型(条目 10 中的里氏替代原则)。
相对于提供的不可变的类型,有时你需要比此更多的灵活性。 考虑条目 29中的Stack
类。下面是它的公共API:
public class Stack<E> { public Stack(); public void push(E e); public E pop(); public boolean isEmpty(); }
假设我们想要添加一个方法来获取一系列元素,并将它们全部推送到栈上。 以下是第一种尝试:
// pushAll method without wildcard type - deficient! public void pushAll(Iterable<E> src) { for (E e : src) push(e); }
这种方法可以干净地编译,但不完全令人满意。 如果可遍历的src
元素类型与栈的元素类型完全匹配,那么它工作正常。 但是,假设有一个Stack <Number>
,并调用push(intVal)
,其中intVal
的类型是Integer
。 这是因为Integer
是Number
的子类型。 从逻辑上看,这似乎也应该起作用:
Stack<Number> numberStack = new Stack<>(); Iterable<Integer> integers = ... ; numberStack.pushAll(integers);
但是,如果你尝试了,会得到这个错误消息,因为参数化类型是不变的:
StackTest.java:7: error: incompatible types: Iterable<Integer> cannot be converted to Iterable<Number> numberStack.pushAll(integers); ^
幸运的是,有对应的解决方法。 该语言提供了一种特殊的参数化类型来调用一个限定通配符类型来处理这种情况。 pushAll
的输入参数的类型不应该是“E的Iterable接口”,而应该是“E的某个子类型的Iterable接口”,并且有一个通配符类型,这意味着:Iterable <? extends E>
。 (关键字extends
的使用有点误导:回忆条目 29中,子类型被定义为每个类型都是它自己的子类型,即使它本身没有继承。)让我们修改pushAll
来使用这个类型:
// Wildcard type for a parameter that serves as an E producer public void pushAll(Iterable<? extends E> src) { for (E e : src) push(e); }
有了这个改变,Stack
类不仅可以干净地编译,而且客户端代码也不会用原始的pushAll
声明编译。 因为Stack
和它的客户端干净地编译,你知道一切都是类型安全的。
现在假设你想写一个popAll
方法,与pushAll
方法相对应。 popAll
方法从栈中弹出每个元素并将元素添加到给定的集合中。 以下是第一次尝试编写popAll
方法的过程:
// popAll method without wildcard type - deficient! public void popAll(Collection<E> dst) { while (!isEmpty()) dst.add(pop()); }
同样,如果目标集合的元素类型与栈的元素类型完全匹配,则干净编译并且工作正常。 但是,这又不完全令人满意。 假设你有一个Stack <Number>
和Object
类型的变量。 如果从栈中弹出一个元素并将其存储在该变量中,它将编译并运行而不会出错。 所以你也不能这样做吗?
Stack<Number> numberStack = new Stack<Number>(); Collection<Object> objects = ... ; numberStack.popAll(objects);
如果尝试将此客户端代码与之前显示的popAll
版本进行编译,则会得到与我们的第一版pushAll
非常类似的错误:Collection <Object>
不是Collection <Number>
的子类型。 通配符类型再一次提供了一条出路。 popAll
的输入参数的类型不应该是“E的集合”,而应该是“E的某个父类型的集合”(其中父类型被定义为E是它自己的父类型[JLS,4.10])。 再次,有一个通配符类型,正是这个意思:Collection <? super E>
。 让我们修改popAll
来使用它:
// Wildcard type for parameter that serves as an E consumer public void popAll(Collection<? super E> dst) { while (!isEmpty()) dst.add(pop()); }
通过这个改动,Stack类和客户端代码都可以干净地编译。
这个结论很清楚。 为了获得最大的灵活性,对代表生产者或消费者的输入参数使用通配符类型。 如果一个输入参数既是一个生产者又是一个消费者,那么通配符类型对你没有好处:你需要一个精确的类型匹配,这就是没有任何通配符的情况。
这里有一个助记符来帮助你记住使用哪种通配符类型:
PECS代表: producer-extends,consumer-super。
换句话说,如果一个参数化类型代表一个T
生产者,使用<? extends T>
;如果它代表T
消费者,则使用<? super T>
。 在我们的Stack
示例中,pushAll
方法的src
参数生成栈使用的E
实例,因此src
的合适类型为Iterable<? extends E>
;popAll
方法的dst
参数消费Stack
中的E
实例,因此ds
t的合适类型是Collection <? super E>
。 PECS助记符抓住了使用通配符类型的基本原则。 Naftalin和Wadler称之为获取和放置原则( Get and Put Principle )[Naftalin07,2.4]。
记住这个助记符之后,让我们来看看本章中以前项目的一些方法和构造方法声明。 条目 28中的Chooser
类构造方法有这样的声明:
public Chooser(Collection<T> choices)
这个构造方法只使用集合选择来生产类型T
的值(并将它们存储起来以备后用),所以它的声明应该使用一个extends T
的通配符类型。下面是得到的构造方法声明:
// Wildcard type for parameter that serves as an T producer public Chooser(Collection<? extends T> choices)
这种改变在实践中会有什么不同吗? 是的,会有不同。 假你有一个List <Integer>
,并且想把它传递给Chooser<Number>
的构造方法。 这不会与原始声明一起编译,但是它只会将限定通配符类型添加到声明中。
现在看看条目 30中的union
方法。下是声明:
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
两个参数s1
和s2
都是E
的生产者,所以PECS助记符告诉我们该声明应该如下:
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
请注意,返回类型仍然是Set <E>
。 不要使用限定通配符类型作为返回类型。除了会为用户提供额外的灵活性,还强制他们在客户端代码中使用通配符类型。 通过修改后的声明,此代码将清晰地编译:
Set<Integer> integers = Set.of(1, 3, 5); Set<Double> doubles = Set.of(2.0, 4.0, 6.0); Set<Number> numbers = union(integers, doubles);
如果使用得当,类的用户几乎不会看到通配符类型。 他们使方法接受他们应该接受的参数,拒绝他们应该拒绝的参数。 如果一个类的用户必须考虑通配符类型,那么它的API可能有问题。
在Java 8之前,类型推断规则不够聪明,无法处理先前的代码片段,这要求编译器使用上下文指定的返回类型(或目标类型)来推断E
的类型。union
方法调用的目标类型如前所示是Set <Number>
。 如果尝试在早期版本的Java中编译片段(以及适合的Set.of
工厂替代版本),将会看到如此长的错综复杂的错误消息:
Union.java:14: error: incompatible types Set<Number> numbers = union(integers, doubles); ^ required: Set<Number> found: Set<INT#1> where INT#1,INT#2 are intersection types: INT#1 extends Number,Comparable<? extends INT#2> INT#2 extends Number,Comparable<?>
幸运的是有办法来处理这种错误。 如果编译器不能推断出正确的类型,你可以随时告诉它使用什么类型的显式类型参数[JLS,15.12]。 甚至在Java 8中引入目标类型之前,这不是你必须经常做的事情,这很好,因为显式类型参数不是很漂亮。 通过添加显式类型参数,如下所示,代码片段在Java 8之前的版本中进行了干净编译:
// Explicit type parameter - required prior to Java 8 Set<Number> numbers = Union.<Number>union(integers, doubles);
接下来让我们把注意力转向条目 30中的max
方法。这里是原始声明:
public static <T extends Comparable<T>> T max(List<T> list)
以下是使用通配符类型的修改后的声明:
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
为了从原来到修改后的声明,我们两次应用了PECS。首先直接的应用是参数列表。 它生成T
实例,所以将类型从List <T>
更改为List<? extends T>
。 棘手的应用是类型参数T
。这是我们第一次看到通配符应用于类型参数。 最初,T
被指定为继承Comparable <T>
,但Comparable
的T
消费T
实例(并生成指示顺序关系的整数)。 因此,参数化类型Comparable <T>
被替换为限定通配符类型Comparable<? super T>
。 Comparable
实例总是消费者,所以通常应该使用Comparable<? super T>
优于Comparable <T>
。 Comparator
也是如此。因此,通常应该使用Comparator<? super T>
优于Comparator<T>
。
修改后的max
声明可能是本书中最复杂的方法声明。 增加的复杂性是否真的起作用了吗? 同样,它的确如此。 这是一个列表的简单例子,它被原始声明排除,但在被修改后的版本里是允许的:
List<ScheduledFuture<?>> scheduledFutures = ... ;
无法将原始方法声明应用于此列表的原因是ScheduledFuture
不实现Comparable <ScheduledFuture>
。 相反,它是Delayed
的子接口,它继承了Comparable <Delayed>
。 换句话说,一个ScheduledFuture
实例不仅仅和其他的ScheduledFuture
实例相比较: 它可以与任何Delayed
实例比较,并且足以导致原始的声明拒绝它。 更普遍地说,通配符要求来支持没有直接实现Comparable
(或Comparator
)的类型,但继承了一个类型。
还有一个关于通配符相关的话题。 类型参数和通配符之间具有双重性,许多方法可以用一个或另一个声明。 例如,下面是两个可能的声明,用于交换列表中两个索引项目的静态方法。 第一个使用无限制类型参数(条目 30),第二个使用无限制通配符:
// Two possible declarations for the swap method public static <E> void swap(List<E> list, int i, int j); public static void swap(List<?> list, int i, int j);
这两个声明中的哪一个更可取,为什么? 在公共API中,第二个更好,因为它更简单。 你传入一个列表(任何列表),该方法交换索引的元素。 没有类型参数需要担心。 通常,如果类型参数在方法声明中只出现一次,请将其替换为通配符。 如果它是一个无限制的类型参数,请将其替换为无限制的通配符; 如果它是一个限定类型参数,则用限定通配符替换它。
第二个swap
方法声明有一个问题。 这个简单的实现不会编译:
public static void swap(List<?> list, int i, int j) { list.set(i, list.set(j, list.get(i))); }
试图编译它会产生这个不太有用的错误信息:
Swap.java:5: error: incompatible types: Object cannot be converted to CAP#1 list.set(i, list.set(j, list.get(i))); ^ where CAP#1 is a fresh type-variable: CAP#1 extends Object from capture of ?
看起来我们不能把一个元素放回到我们刚刚拿出来的列表中。 问题是列表的类型是List <?>
,并且不能将除null外的任何值放入List <?>
中。 幸运的是,有一种方法可以在不使用不安全的转换或原始类型的情况下实现此方法。 这个想法是写一个私有辅助方法来捕捉通配符类型。 辅助方法必须是泛型方法才能捕获类型。 以下是它的定义:
public static void swap(List<?> list, int i, int j) { swapHelper(list, i, j); } // Private helper method for wildcard capture private static <E> void swapHelper(List<E> list, int i, int j) { list.set(i, list.set(j, list.get(i))); }
swapHelper
方法知道该列表是一个List <E>
。 因此,它知道从这个列表中获得的任何值都是E类型,并且可以安全地将任何类型的E
值放入列表中。 这个稍微复杂的swap
的实现可以干净地编译。 它允许我们导出基于通配符的漂亮声明,同时利用内部更复杂的泛型方法。 swap
方法的客户端不需要面对更复杂的swapHelper
声明,但他们从中受益。 辅助方法具有我们认为对公共方法来说过于复杂的签名。
总之,在你的API中使用通配符类型,虽然棘手,但使得API更加灵活。 如果编写一个将被广泛使用的类库,正确使用通配符类型应该被认为是强制性的。 记住基本规则: producer-extends, consumer-super(PECS)。 还要记住,所有Comparable
和Comparator
都是消费者。
32. 合理地结合泛型和可变参数
在Java 5中,可变参数方法(条目 53)和泛型都被添加到平台中,所以你可能希望它们能够正常交互; 可悲的是,他们并没有。 可变参数的目的是允许客户端将一个可变数量的参数传递给一个方法,但这是一个脆弱的抽象( leaky abstraction):当你调用一个可变参数方法时,会创建一个数组来保存可变参数;那个应该是实现细节的数组是可见的。 因此,当可变参数具有泛型或参数化类型时,会导致编译器警告混淆。
回顾条目 28,非具体化( non-reifiable)的类型是其运行时表示比其编译时表示具有更少信息的类型,并且几乎所有泛型和参数化类型都是不可具体化的。 如果某个方法声明其可变参数为非具体化的类型,则编译器将在该声明上生成警告。 如果在推断类型不可确定的可变参数参数上调用该方法,那么编译器也会在调用中生成警告。 警告看起来像这样:
warning: [unchecked] Possible heap pollution from parameterized vararg type List<String>
当参数化类型的变量引用不属于该类型的对象时会发生堆污染(Heap pollution)[JLS,4.12.2]。 它会导致编译器的自动生成的强制转换失败,违反了泛型类型系统的基本保证。
例如,请考虑以下方法,该方法是第127页上的代码片段的一个不太明显的变体:
// Mixing generics and varargs can violate type safety! static void dangerous(List<String>... stringLists) { List<Integer> intList = List.of(42); Object[] objects = stringLists; objects[0] = intList; // Heap pollution String s = stringLists[0].get(0); // ClassCastException }
此方法没有可见的强制转换,但在调用一个或多个参数时抛出ClassCastException异常。 它的最后一行有一个由编译器生成的隐形转换。 这种转换失败,表明类型安全性已经被破坏,并且将值保存在泛型可变参数数组参数中是不安全的。
这个例子引发了一个有趣的问题:为什么声明一个带有泛型可变参数的方法是合法的,当明确创建一个泛型数组是非法的时候呢? 换句话说,为什么前面显示的方法只生成一个警告,而127页上的代码片段会生成一个错误? 答案是,具有泛型或参数化类型的可变参数参数的方法在实践中可能非常有用,因此语言设计人员选择忍受这种不一致。 事实上,Java类库导出了几个这样的方法,包括Arrays.asList(T... a)
,Collections.addAll(Collection<? super T> c, T... elements)
,EnumSet.of(E first, E... rest)
。 与前面显示的危险方法不同,这些类库方法是类型安全的。
在Java 7中,SafeVarargs
注解已添加到平台,以允许具有泛型可变参数的方法的作者自动禁止客户端警告。 实质上,SafeVarargs
注解构成了作者对类型安全的方法的承诺。 为了交换这个承诺,编译器同意不要警告用户调用可能不安全的方法。
除非它实际上是安全的,否则注意不要使用@SafeVarargs
注解标注一个方法。 那么需要做些什么来确保这一点呢? 回想一下,调用方法时会创建一个泛型数组,以容纳可变参数。 如果方法没有在数组中存储任何东西(它会覆盖参数)并且不允许对数组的引用进行转义(这会使不受信任的代码访问数组),那么它是安全的。 换句话说,如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟这是可变参数的目的——那么该方法是安全的。
值得注意的是,你可以违反类型安全性,即使不会在可变参数数组中存储任何内容。 考虑下面的泛型可变参数方法,它返回一个包含参数的数组。 乍一看,它可能看起来像一个方便的小工具:
// UNSAFE - Exposes a reference to its generic parameter array! static <T> T[] toArray(T... args) { return args; }
这个方法只是返回它的可变参数数组。 该方法可能看起来并不危险,但它是! 该数组的类型由传递给方法的参数的编译时类型决定,编译器可能没有足够的信息来做出正确的判断。 由于此方法返回其可变参数数组,它可以将堆污染传播到调用栈上。
为了具体说明,请考虑下面的泛型方法,它接受三个类型T
的参数,并返回一个包含两个参数的数组,随机选择:
static <T> T[] pickTwo(T a, T b, T c) { switch(ThreadLocalRandom.current().nextInt(3)) { case 0: return toArray(a, b); case 1: return toArray(a, c); case 2: return toArray(b, c); } throw new AssertionError(); // Can't get here }
这个方法本身不是危险的,除了调用具有泛型可变参数的toArray
方法之外,不会产生警告。
编译此方法时,编译器会生成代码以创建一个将两个T
实例传递给toArray
的可变参数数组。 这段代码分配了一个Object []
类型的数组,它是保证保存这些实例的最具体的类型,而不管在调用位置传递给pickTwo
的对象是什么类型。 toArray
方法只是简单地将这个数组返回给pickTwo
,然后pickTwo
将它返回给调用者,所以pickTwo
总是返回一个Object []
类型的数组。
现在考虑这个测试pickTw
的main
方法:
public static void main(String[] args) { String[] attributes = pickTwo("Good", "Fast", "Cheap"); }
这种方法没有任何问题,因此它编译时不会产生任何警告。 但是当运行它时,抛出一个ClassCastException异常,尽管不包含可见的转换。 你没有看到的是,编译器已经生成了一个隐藏的强制转换为由pickTwo
返回的值的String []
类型,以便它可以存储在属性中。 转换失败,因为Object []
不是String []
的子类型。 这种故障相当令人不安,因为它从实际导致堆污染(toArray
)的方法中移除了两个级别,并且在实际参数存储在其中之后,可变参数数组未被修改。
这个例子是为了让人们认识到给另一个方法访问一个泛型的可变参数数组是不安全的,除了两个例外:将数组传递给另一个可变参数方法是安全的,这个方法是用@SafeVarargs
正确标注的, 将数组传递给一个非可变参数的方法是安全的,该方法仅计算数组内容的一些方法。
这里是安全使用泛型可变参数的典型示例。 此方法将任意数量的列表作为参数,并按顺序返回包含所有输入列表元素的单个列表。 由于该方法使用@SafeVarargs
进行标注,因此在声明或其调用站位置上不会生成任何警告:
// Safe method with a generic varargs parameter @SafeVarargs static <T> List<T> flatten(List<? extends T>... lists) { List<T> result = new ArrayList<>(); for (List<? extends T> list : lists) result.addAll(list); return result; }
决定何时使用SafeVarargs
注解的规则很简单:在每种方法上使用@SafeVarargs
,并使用泛型或参数化类型的可变参数,这样用户就不会因不必要的和令人困惑的编译器警告而担忧。 这意味着你不应该写危险或者toArray
等不安全的可变参数方法。 每次编译器警告你可能会受到来自你控制的方法中泛型可变参数的堆污染时,请检查该方法是否安全。 提醒一下,在下列情况下,泛型可变参数方法是安全的:
1.它不会在可变参数数组中存储任何东西
2.它不会使数组(或克隆)对不可信代码可见。 如果违反这些禁令中的任何一项,请修复。
请注意,SafeVarargs
注解只对不能被重写的方法是合法的,因为不可能保证每个可能的重写方法都是安全的。 在Java 8中,注解仅在静态方法和final实例方法上合法; 在Java 9中,它在私有实例方法中也变为合法。
使用SafeVarargs
注解的替代方法是采用条目 28的建议,并用List
参数替换可变参数(这是一个变相的数组)。 下面是应用于我们的flatten
方法时,这种方法的样子。 请注意,只有参数声明被更改了:
// List as a typesafe alternative to a generic varargs parameter static <T> List<T> flatten(List<List<? extends T>> lists) { List<T> result = new ArrayList<>(); for (List<? extends T> list : lists) result.addAll(list); return result; }
然后可以将此方法与静态工厂方法List.of
结合使用,以允许可变数量的参数。 请注意,这种方法依赖于List.of
声明使用@SafeVarargs
注解:
audience = flatten(List.of(friends, romans, countrymen));
这种方法的优点是编译器可以证明这种方法是类型安全的。 不必使用SafeVarargs
注解来证明其安全性,也不用担心在确定安全性时可能会犯错。 主要缺点是客户端代码有点冗长,运行可能会慢一些。
这个技巧也可以用在不可能写一个安全的可变参数方法的情况下,就像第147页的toArray
方法那样。它的列表模拟是List.of
方法,所以我们甚至不必编写它; Java类库作者已经为我们完成了这项工作。 pickTwo
方法然后变成这样:
static <T> List<T> pickTwo(T a, T b, T c) { switch(rnd.nextInt(3)) { case 0: return List.of(a, b); case 1: return List.of(a, c); case 2: return List.of(b, c); } throw new AssertionError(); }
main
方变成这样:
public static void main(String[] args) { List<String> attributes = pickTwo("Good", "Fast", "Cheap"); }
生成的代码是类型安全的,因为它只使用泛型,不是数组。
总而言之,可变参数和泛型不能很好地交互,因为可变参数机制是在数组上面构建的脆弱的抽象,并且数组具有与泛型不同的类型规则。 虽然泛型可变参数不是类型安全的,但它们是合法的。 如果选择使用泛型(或参数化)可变参数编写方法,请首先确保该方法是类型安全的,然后使用@SafeVarargs
注解对其进行标注,以免造成使用不愉快
33. 优先考虑类型安全的异构容器
泛型的常见用法包括集合,如Set <E>
和Map <K,V>
和单个元素容器,如ThreadLocal <T>
和AtomicReference <T>
。 在所有这些用途中,它都是参数化的容器。 这限制了每个容器只能有固定数量的类型参数。 通常这正是你想要的。 一个Set
有单一的类型参数,表示它的元素类型; 一个Map
有两个,代表它的键和值的类型;等等。
然而有时候,你需要更多的灵活性。 例如,数据库一行记录可以具有任意多列,并且能够以类型安全的方式访问它们是很好的。 幸运的是,有一个简单的方法可以达到这个效果。 这个想法是参数化键(key)而不是容器。 然后将参数化的键提交给容器以插入或检索值。 泛型类型系统用于保证值的类型与其键一致。
作为这种方法的一个简单示例,请考虑一个Favorites类,它允许其客户端保存和检索任意多种类型的favorite
实例。 该类型的Class对象将扮演参数化键的一部分。其原因是这Class
类是泛型的。 类的类型从字面上来说不是简单的Class
,而是Class <T>
。 例如,String.class
的类型为Class <String>
,Integer.class的
类型为Class <Integer>
。 当在方法中传递字面类传递编译时和运行时类型信息时,它被称为类型令牌(type token)[Bracha04]。
Favorites
类的API很简单。 它看起来就像一个简单Map类,除了该键是参数化的以外。 客户端在设置和获取favorites
实例时呈现一个Class对象。 这里是API:
// Typesafe heterogeneous container pattern - API public class Favorites { public <T> void putFavorite(Class<T> type, T instance); public <T> T getFavorite(Class<T> type); }
下面是一个演示Favorites
类,保存,检索和打印喜欢的String
,Integer
和Class
实例:
// Typesafe heterogeneous container pattern - client public static void main(String[] args) { Favorites f = new Favorites(); f.putFavorite(String.class, "Java"); f.putFavorite(Integer.class, 0xcafebabe); f.putFavorite(Class.class, Favorites.class); String favoriteString = f.getFavorite(String.class); int favoriteInteger = f.getFavorite(Integer.class); Class<?> favoriteClass = f.getFavorite(Class.class); System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName()); }
正如你所期望的,这个程序打印Java cafebabe Favorites
。 请注意,顺便说一下,Java的printf
方法与C语言的不同之处在于,应该使用%n
,而在C中使用\n
。%n
生成适用的特定于平台的行分隔符,该分隔符在很多但不是所有平台上都是\n
。
Favorites
实例是类型安全的:当你请求一个字符串时它永远不会返回一个整数。 它也是异构的:与普通Map不同,所有的键都是不同的类型。 因此,我们将Favorites
称为类型安全异构容器(typesafe heterogeneous container.)。
Favorites
的实现非常小巧。 这是完整的代码:
// 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)); } }
这里有一些微妙的事情发生。 每个Favorites
实例都由一个名为favorites
私有的Map<Class<?>, Object>
来支持。 你可能认为无法将任何内容放入此Map中,因为这是无限定的通配符类型,但事实恰恰相反。 需要注意的是通配符类型是嵌套的:它不是通配符类型的Map类型,而是键的类型。 这意味着每个键都可以有不同的参数化类型:一个可以是Class <String>
,下一个Class <Integer>
等等。 这就是异构的由来。
接下来要注意的是,favorites的Map的值类型只是Object。 换句话说,Map不保证键和值之间的类型关系,即每个值都是由其键表示的类型。 事实上,Java的类型系统并不足以表达这一点。 但是我们知道这是真的,并在检索一个favorite时利用了这点。
putFavorite
实现很简单:只需将给定的Class对象映射到给定的favorites的实例即可。 如上所述,这丢弃了键和值之间的“类型联系(type linkage)”;无法知道这个值是不是键的一个实例。 但没关系,因为getFavorites
方法可以并且确实重新建立这种关联。
getFavorite
的实现比putFavorite
更复杂。 首先,它从favorites Map中获取与给定Class对象相对应的值。 这是返回的正确对象引用,但它具有错误的编译时类型:它是Object(favorites map的值类型),我们需要返回类型T
。因此,getFavorite
实现动态地将对象引用转换为Class对象表示的类型,使用Class的cast
方法。
cast
方法是Java的cast操作符的动态模拟。它只是检查它的参数是否由Class对象表示的类型的实例。如果是,它返回参数;否则会抛出ClassCastException
异常。我们知道,假设客户端代码能够干净地编译,getFavorite
中的强制转换不会抛出ClassCastException
异常。 也就是说,favorites map中的值始终与其键的类型相匹配。
那么这个cast
方法为我们做了什么,因为它只是返回它的参数? cast
的签名充分利用了Class类是泛型的事实。 它的返回类型是Class对象的类型参数:
public class Class<T> { T cast(Object obj); }
这正是getFavorite
方法所需要的。 这正是确保Favorites类型安全,而不用求助一个未经检查的强制转换的T
类型。
Favorites类有两个限制值得注意。 首先,恶意客户可以通过使用原始形式的Class对象,轻松破坏Favorites实例的类型安全。 但生成的客户端代码在编译时会生成未经检查的警告。 这与正常的集合实现(如HashSet和HashMap)没有什么不同。 通过使用原始类型HashSet(条目 26),可以轻松地将字符串放入HashSet <Integer>
中。 也就是说,如果你愿意为此付出一点代价,就可以拥有运行时类型安全性。 确保Favorites永远不违反类型不变的方法是,使putFavorite
方法检查该实例是否由type表示类型的实例,并且我们已经知道如何执行此操作。只需使用动态转换:
// Achieving runtime type safety with a dynamic cast public <T> void putFavorite(Class<T> type, T instance) { favorites.put(type, type.cast(instance)); }
java.util.Collections
中有一些集合包装类,可以发挥相同的诀窍。 它们被称为checkedSet
,checkedList
,checkedMap
等等。 他们的静态工厂除了一个集合(或Map)之外还有一个Class对象(或两个)。 静态工厂是泛型方法,确保Class对象和集合的编译时类型匹配。 包装类为它们包装的集合添加了具体化。 例如,如果有人试图将Coin
放入你的Collection <Stamp>
中,则包装类在运行时会抛出ClassCastException
。 这些包装类对于追踪在混合了泛型和原始类型的应用程序中添加不正确类型的元素到集合的客户端代码很有用。
Favorites类的第二个限制是它不能用于不可具体化的(non-reifiable)类型(条目 28)。 换句话说,你可以保存你最喜欢的String
或String []
,但不能保存List <String>
。 如果你尝试保存你最喜欢的List <String>
,程序将不能编译。 原因是无法获取List <String>
的Class对象。 List <String> .class
是语法错误,也是一件好事。 List <String>
和List <Integer>
共享一个Class对象,即List.class
。 如果“字面类型(type literals)”List <String> .class
和List <Integer> .class
合法并返回相同的对象引用,那么它会对Favorites对象的内部造成严重破坏。 对于这种限制,没有完全令人满意的解决方法。
Favorites使用的类型令牌( type tokens)是无限制的:getFavorite
和putFavorite
接受任何Class对象。 有时你可能需要限制可传递给方法的类型。 这可以通过一个有限定的类型令牌来实现,该令牌只是一个类型令牌,它使用限定的类型参数(条目 30)或限定的通配符(条目 31)来放置可以表示的类型的边界。
注解API(条目 39)广泛使用限定类型的令牌。 例如,以下是在运行时读取注解的方法。 此方法来自AnnotatedElement
接口,该接口由表示类,方法,属性和其他程序元素的反射类型实现:
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
参数annotationType
是表示注解类型的限定类型令牌。 该方法返回该类型的元素的注解(如果它有一个);如果没有,则返回null。 本质上,注解元素是一个类型安全的异构容器,其键是注解类型。
假设有一个Class <?>
类型的对象,并且想要将它传递给需要限定类型令牌(如getAnnotation
)的方法。 可以将对象转换为Class<? extends Annotation>
,但是这个转换没有被检查,所以它会产生一个编译时警告(条目 27)。 幸运的是,Class类提供了一种安全(动态)执行这种类型转换的实例方法。 该方法被称为asSubclass
,并且它转换所调用的Class对象来表示由其参数表示的类的子类。 如果转换成功,该方法返回它的参数;如果失败,则抛出ClassCastException
异常。
以下是如何使用asSubclass
方法在编译时读取类型未知的注解。 此方法编译时没有错误或警告:
// Use of asSubclass to safely cast to a bounded type token static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) { Class<?> annotationType = null; // Unbounded type token try { annotationType = Class.forName(annotationTypeName); } catch (Exception ex) { throw new IllegalArgumentException(ex); } return element.getAnnotation( annotationType.asSubclass(Annotation.class)); }
总之,泛型API的通常用法(以集合API为例)限制了每个容器的固定数量的类型参数。 你可以通过将类型参数放在键上而不是容器上来解决此限制。 可以使用Class对象作为此类型安全异构容器的键。 以这种方式使用的Class对象称为类型令牌。 也可以使用自定义键类型。 例如,可以有一个表示数据库行(容器)的DatabaseRow
类型和一个泛型类型Column <T>
作为其键。
参考文章:www.jianshu.com/p/8bc1615c7…