30 优先考虑泛型方法
静态工具方法尤其适合于泛型化
编写泛型方法类似于编写泛型类:
public static Set union(Set s1, Set s2) { Set result = new HashSet(s1); result.addAll(s2); return result; }
上面的类有两个警告信息:
如果修复这些警告,要将方法声明修改为声明一个类型参数:
public static <E> Set<E> union(Set<E> s1, Set<E> s2) { Set<E> result = new HashSet<>(s1); result.addAll(s2); return result; }
这个例子中,类型参数列表是,返回类型是Set
使用上面的方法也很简单:
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)(Set<? extends xxx>),可以使该方法更加灵活。
除此之外,还有递归类型限制(recursive type bound)的概念:通过包含类型参数本身的表达式来限制类型参数。
递归类型限制的一个经典用法和Comparable接口有关:
public interface Comparable<T> { int compareTo(T o); }
类型参数T,可以与实现Comparable<T> 的类型的元素进行比较。例如,String 类实现了Comparable<String>,Integer 类实现了Comparable<Integer>,几乎所有类型都只能与同类型的元素比较。
public static <E extends Comparable<E>> E max(Collection<E> c);
<
E extends Comparable <E >> 可以理解为「任何可以与自己比较的类型E」
下面的代码实现了计算最大值的功能:
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; }
这里更好的选择是返回一个Optional<E>
,这一点将在后面说明
31 利用限定通配符来提升API的灵活性
相对于提供的不可变的类型,有时需要更多的灵活性,在第29条里写了一个Stack的API:
public class Stack<E> { public Stack(); public void push(E e); public E pop(); public boolean isEmpty(); }
如果要添加一个方法来将多个元素放到栈里:
public void pushAll(Iterable<E> src) { for (E e : src) push(e); }
如果此时声明了一个Stack<Number>
,尝试插入Integer数据就会报错:
Stack<Number> numberStack = new Stack<>(); Iterable<Integer> integers = ... ; numberStack.pushAll(integers);
Java提供了一种特殊的参数化类型——限定通配符类型(bounded wildcard type),pushAll的输入参数类型应该是「E的某个子类型的Iterable接口」,用代码表示就是Iterable<? extends E>
:
public void pushAll(Iterable<? extends E> src) { for (E e : src) push(e); }
假设还想写一个popAll
方法,与pushAll
方法相对应:popAll
方法从栈中弹出每个元素并将元素添加到给定的集合中。
public void popAll(Collection<E> dst) { while (!isEmpty()) dst.add(pop()); }
假设还想写一个popAll
方法,与pushAll
方法相对应:popAll
方法从栈中弹出每个元素并将元素添加到给定的集合中。
public void popAll(Collection<E> dst) { while (!isEmpty()) dst.add(pop()); }
假设有一个Stac<Number>
和Collection<Object>
类型的变量,从栈中弹出一个元素并将其存储在该变量中:
Stack<Number> numberStack = new Stack<Number>(); Collection<Object> objects = ... ; numberStack.popAll(objects);
会得到与第一版pushAll 非常类似的错误:Collection<Object> 不是Collection<Number> 的子类型。popAll的输入参数的类型不应该是「E的集合」,而应该是「E的某个父类型的集合」。使用Collection<? super E>修改上面的代码:
public void popAll(Collection<? super E> dst) { while (!isEmpty()) dst.add(pop()); }
所以结论就很明显:为了获得最大的灵活性,对代表生产者和消费者的输入参数使用通配符类型。
PECS
这是一个记忆的口诀,producer-extends,consumer-super:
如果一个参数化类型代表一个T 生产者,使用<? extends T>;如果它代表T 消费者,则使用<? super T>。
Stack 示例中,pushAll方法的src 参数生成栈使用的E 实例,因此src的合适类型为Iterable<? extends E>;popAll方法的dst 参数消费Stack 中的E 实例,因此dst的合适类型是Collection <? super E>。
下面就用一些例子来说明这个口诀如何使用的:
第28条中的Chooser类构造方法
public Chooser(Collection<T> choices)
这个构造方法只使用集合选择来生产类型T的值,所以它的声明应该使用一个extends T
的通配符类型:
public Chooser(Collection<? extends T> choices)
30条中的union 方法
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
两个参数s1和s2都是E的生产者:
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
注意,返回类型仍然是Set。不要使用限定通配符类型作为返回类型,修改之后的用法如下:
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);
第30条中的max 方法
public static <T extends Comparable<T>> T max(List<T> list)
public static <T extends Comparable<T>> T max(List<T> list)
修改之后的声明为:
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
为了从原来到修改后的声明,需要应用两次PECS,先应用到参数 list,它产生T 实例,所以将类型从List<T>更改为List<? extends T>;最初,T 被指定为继承Comparable<T>,但 Comparable 的 T 消费 T 实例,所以使用Comparable<? super T> 代替 Comparable<T>
通过这个例子也可以得到两条结论:
32 合理地结合泛型和可变参数
可变参数和泛型不能很好的搭配使用,可变参数的目的是允许客户端将一个可变数量的参数传递给一个方法,但这是一个脆弱的抽象(leaky abstraction):当你调用一个可变参数方法时,会创建一个数组来保存可变参数
当参数化类型的变量引用不属于该类型的对象时会发生堆污染
例如:
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异常。所以将值保存在泛型可变参数数组参数中是不安全的。
这个例子引发了一个有趣的问题:为什么声明一个带有泛型可变参数的方法是合法的,当显式创建一个泛型数组是非法的呢?
答案是:具有泛型的可变参数的方法在实践中可能非常有用,例如Arrays.asList(T... a),Collections.addAll(Collection<? super T> c, T... elements),EnumSet.of(E first, E... rest),因此设计人员选择忍受这种不一致。
在Java 7中,增加了@SafeVarargs注解,@SafeVarargs注解表示了作者对该方法是类型安全的承诺,对于每一个带有泛型可变参数的方法,都要用@SafeVarargs注解。
注意:如果不在可变参数的数组中保存任何值,这也可能破坏类的安全性,例如:
static <T> T[] toArray(T... args) { return args; }
上面的代码看起来没什么,但实际上它很危险:该数组的类型由传递给方法的参数的编译时类型决定,由于此方法返回其可变参数数组,它可以将堆污染传播到调用栈上。
举个例子你就明白了:
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 }
编译器会生成代码,以创建一个可变参数数组,将两个T实例传递给toArray
,toArray方法的返回值是Object[ ]
,就容易像dangerous
一样产生堆污染,例如:
public static void main(String[] args) { String[] attributes = pickTwo("Good", "Fast", "Cheap"); }
当运行它时,抛出一个ClassCastException异常,原因是pickTwo返回的值上产生了一个隐藏的String[ ]转换,Object不是String的子类,所以会报错。
所以允许另一个方法访问一个泛型的可变参数数组是不安全的,但有两种情况例外:
将数组传给用@SafeVarargs注解过的方法
将数组传给一个非可变参数的方法,该方法仅计算数组内容部分
下列代码是一个安全使用泛型可变参数的典型例子,将入参合并为一个List:
@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注解只能用在无法被覆盖的方法上,因为不可能保证每个重写方法都是安全的。在Java 8中,该注解仅能用在static和final方法上。
如果不想用SafeVarargs注解,可以采用第28条里面的建议,用一个List参数代替可变参数:
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 结合使用,来允许可变数量的参数:
audience = flatten(List.of(friends, romans, countrymen));
这种方法的优点是编译器可以证明这种方法是类型安全的。
List.of 声明使用了@SafeVarargs 注解
这个技巧也可以用在toArray方法上:
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"); }
生成的代码是安全的,因为它只使用了泛型,没有用到数组。
33 优先考虑类型安全的异构容器
泛型最常用于集合,Set<E>、Map<K, V>等,以及单个元素的容器,ThreadLocal<T>、AtomicReference<T>等,这里有一个限制就是每个容器只能有固定数量的泛型参数。
有时需要更多的灵活性,例如数据库有很多列,如果能安全地访问所有列就好了,其实这个需求借助Map就可以实现,将key进行参数化替代将容器参数化。
以Favorites类为例,它允许其客户端保存和检索任意多种类型的favorite实例:
public class Favorites { public <T> void putFavorite(Class<T> type, T instance) {...}; public <T> T getFavorite(Class<T> type) {...}; }
Class对象将扮演参数化key的一部分,入参的类型变为Class<T>
例如:String.class的类型为Class<String>,Integer.class的类型为Class<Integer>。
当一个类的字面被用在方法中时,被称为类型令牌。
下面是API的用法:
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
- Favorites是类型安全的:当请求String时不会返回其他的类型;
- Favorites也是异构的,所有Key都是不同类型的。
所以Favorites就是一个类型安全的异构容器
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的Map的value类型是Object,所以Map并不能保证key - value的类型一定相同
getFavorite方法的先从favorites的映射中获得与指定Class对象对应的值,它的类型只是个Object,但我们需要返回一个T,所以使用了cast方法将对象引用动态转换成Class对应的类型
cast方法只检验它的参数是否为Class对象所表示的类型的实例,如果是则返回参数,否则就抛出ClassCastException异常
确保Favorites不违背它的类型约束条件的方式是在插入元素的时候检查一下待插入元素的类型:让putFavorite方法检验instance是否真的是type所表示的类型的实例:
有一些集合包装类采用了同样的技巧,如checkSet、checkedList、checkedMap
此外Favorites类也不能用在不可具体化的类型中,即可以保存最喜爱的String或String[ ],但不能保存List<String>。原因是List<String>.class语法错误,但这也是一件好事,防止List<String>和List<Integer>共享同一个Class 对象(List.class)
有时可能我们要限制可传递给方法的类型。这可以通过有限制的类型令牌(bounded type token)来实现。
Java里面注解API就广泛使用了有限制的类型令牌,例如:
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
入参 annotationType 是表示注解类型的有限值的类型令牌。如果element有这种类型的注解,该方法返回
被注解元素的本质上是一个类型安全的异构容器,key是注解类型
假设有一个Class<?>类型的对象,将它传给getAnnotation时,可以将对象转换成Class<? extends Annotation>,这种转换是unchecked的,所以会有编译时警告。Class提供了一个安全执行这种转换的方法:asSubClass,它将调用它的Class对象转换成入参类的一个子类:
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)); }