Item 26 不要使用原始类型
原始类型的问题在于类型不确定,在编译阶段看不出来问题,而将隐患拖到运行时
//一个 原始类型集合,对集合的元素类型没有约束
private final Collection stamps = ... ;
//往邮票集合添加硬币,编译器会警告但不报错
stamps.add(new Coin( ... ));
//当程序运行到这里时
for (Iterator i = stamps.iterator(); i.hasNext(); )
//如果放入了非Stamp对象,这里就会类型转化异常
Stamp stamp = (Stamp) i.next();
开发的一个重要守则就是尽早发现问题排除隐患,能在编译时排查的问题就不要拖到运行时,使用泛型可以有效的在编译阶段排查错误,避免运行时类型转化错误
//参数化的集合,指定集合元素必须时Stamp类型
private final Collection<Stamp> stamps = ... ;
//编译错误,在编译阶段明确告诉你类型转化异常
//stamps.add(new Coin( ... ));
stamps.add(new Stamp());
for (Iterator<Stamp> i = stamps.iterator(); i.hasNext(); )
//编译器做了隐式转化,因为确定元素是Stamp类型,所以不会有转化错误
Stamp stamp = i.next();
Java为了和之前没有泛型时代的代码兼容,编译的时候泛型信息会被擦除,在取出元素的时候会隐式的帮你做类型转换,正是有参数类型限制,这种转化可以确保是安全的。
那原始类型的集合和参数类型为Object的有什么区别,比如List和List<Object>,它们都可以插入任意对象,但却是有区别的,前者逃避泛型检查,而后者是明确参数类型为Object。指定参数类型的List,如List<String>是List的子类型却不是List<Object>的子类型,看下面的示例
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
//编译报错
safeAdd(strings, Integer.valueOf(42));
String s = strings.get(0);
}
//由于List<String>是List的子类型,这里编译不会报错,只会警告,但类型却是不安全的,因为原始类型逃避泛型检查
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
//List元素必须是Object类型,否则编译报错
private static void safeAdd(List<Object> list, Object o) {
list.add(o);
}
如果参数类型不重要,使用原始类型作为形参确实很方便,没有约束,但有很大隐患,实际开发中有一个更安全的替代方案:无上限通配符,使用这种参数类型的集合,除了Null你不能添加任何元素,也取不出来任何元素
public static void main(String[] args) {
Set<String> s1 = new HashSet<>();
s1.add("one");
Set<Integer> s2 = new HashSet<>();
s2.add(12);
int result = cacl(s1, s2);
System.out.println(result);
}
//不在乎Set集合元素的类型,但有需要类型安全
private static int cacl(Set<?> s1, Set<?> s2) {
int count = 0;
for (Object object : s2) {
if (s1.contains(object)) {
count++;
}
}
return count;
}
但有些例外,必须使用原始类型,一个就是类字面量
//合法
List.class, String[].class, and int.class
//不合法
List<String>.class and List<?>.class
另外一个就是instancof类型检查,因为泛型类型在运行时被擦除,所以除了无上限通配符参数类型外,其它参数类型使用instanceof都不合法,但这个无上限的通配符用上显得多余,所以使用instanceof判断就直接使用原始类型了,下面是推荐写法
if (o instanceof Set) {
//必须使用<?>,说明是受检的转换,不会编译警告
Set<?> s = (Set<?>) o;
}
Item 27 消除非受检警告
尽可能消除编译阶段提示的每个未受检警告,这样可以极大的提高代码运行时的安全。有些时候实现没有办法消除未受检的警告,但你又确定代码是类型安全的,这种情况下可以使用@SuppressWarnings("unchecked")注解明确告诉编译器这个地方不需要警告,但这个注解的使用要注意两点
- 只有在你确定代码是类型安全的才使用,如果滥用只让编译的结果好看一点,但会隐藏问题给运行时埋下隐患
- 尽量精确的覆盖作用范围,粒度要小,能用在本地变量上就不要用在方法上,能用在方法上就不要用在类上,使用的地方需要给出注释说明理由
以ArrayList的toArray一个方法为例,这是来自Android SDK的源码,@SuppressWarnings("unchecked")直接作用在方法上,而且没有说明理由,其实你是看不出来这个注解对 Arrays.copyOf还是 System.arraycopy起作用或者两个方法都需要,如果以后里面添加一个类型不安全的方法,也会被抑制掩盖
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
所以作者推荐的做法是这样的,因为return语句不能使用注解,为了让这个注解的作用范围更加精确,声明了本地变量result让这个注解作用其上,同时需要说明理由,这样代码的维护就更加明朗和安全。
public <T> T[] toArray(T[] a) {
if (a.length < size) {
//这个转换是安全的,因为数组和我们的参数类型都是T
@SuppressWarnings("unchecked")
T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
return result;
}
System.arraycopy(elements, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
总结有两点
- 重视未受检警告
- 准确的使用@SuppressWarnings("unchecked")注解,并添加注释说明理由
Item28列表优先于数组
为啥优先选择列表呢?因为列表能比数组更早的发现风险,这要从数组不同于列表的两个关键点说起
数组是协变类型(Covariant),意指如果Child是Parent的子类型,那么Child[]也是Parent[]的子类型,但泛型是不变的,List<Child>和List<Parent>没有类型关系。数组的协变特征会埋下隐患
//因为数组是协变类型,这个编译是没问题的,却也掩盖了问题
Object[] objectArray = new Long[1];
//运行时抛异常ArrayStoreException
objectArray[0] = "I don't fit in";
数组是具体的(reified),数组在运行时是知道元素类型的,而泛型仅在编译时限制元素类型,运行时元素类型会被擦除,这是为了和Java5之前的版本兼容。
正是由于这两点,泛型和数组无法配合使用,new List<E>[], new List<String>[], new E[]这些表达式都是不合法的,因为泛型数组类型不安全,下面通过反证法说明泛型数组是不应该合法的,如果合法那么泛型的安全机制都荡然无存
//首先假如泛型数组是合法的,下面这个数组元素类型是List<String>
List<String>[] stringLists = new List<String>[1];
//又创建一个列表,类型是List<Integer>
List<Integer> intList = List.of(42);
//因为数组是协变类型,List<String>是子Object类型,所以List<String>[]也是Object[]的子类型
Object[] objects = stringLists;
//因为泛型擦除,运行时List<String>[]变成List[],list<Integer>变成list,所以这里不会抛ArrayStoreException异常
objects[0] = intList;
//但最后编译器想把取出的元素转化成String,实际取出的元素是Integer,于是就会抛ClassCastException
String s = stringLists[0].get(0);
技术上来说,像E、E[]、List<E>这样的类型都是非具体化的,通俗讲就是运行时的信息表达要比编译时少,参数化类型唯一可具体化的是无上限通配符,如 List<?> and Map<?,?>,极少用到但却是合法的。下面代码编译运行都是成功的
//可创建无上限通配符类型的数组
ArrayList<?>[] listArray = new ArrayList<?>[10];
ArrayList<String> strings = new ArrayList<>();
strings.add("abc");
listArray[0] = strings;
ArrayList<Integer> integers = new ArrayList<>();
integers.add(10);
listArray[1] =integers;
//参数类型为无上限通配符的List<?>既不能添加元素也不能取出,但可以移除、比较
System.out.println(listArray[0].remove("abc"));//true
System.out.println(listArray[1].contains(10));//true
因为可变参数本质也是数组,所以它和泛型也配合不好,有令人不解的警告,可以使用SafeVarargs注解解决这个问题。如果在使用泛型数组时出现错误或警告,最好使用泛型列表替代。下面的代码使用的就是泛型数组,虽然我们确信转换是安全的,也可以通过注解让警告消失,但如果没有警告会更好
public class Chooser<T> {
private final T[] choiceArray;
//参数类型都是T,是安全的
@SuppressWarnings("unchecked")
public Chooser(Collection<T> choices) {
choiceArray = (T[]) choices.toArray();
}
// choose method unchanged}
下面使用泛型列表实现,虽然性能不及数组,但没有任何警告和错误,类型是安全的
public class Chooser<T> {
private final List<T> choiceList;
public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices);
}
public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
数组和泛型区别小结如下,如果数组和泛型混用有问题,优先使用列表
数组 | 泛型 |
---|---|
协变的,可具体化的 | 不变的,类型可擦除的 |
运行时类型安全,编译时类型不安全 | 编译时类型安全,运行时类型不安全 |
Item29首选泛型
在Item28中建议当遇到数组与列表时,优先考虑使用列表,但Java并不先天性的支持列表,Java列表是基于数组实现的,如ArrayList,这个时候就必须借助数组。下面是个简单的演示
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
//elements 是私有的,引用没有外泄,包含元素只有push方法的E,所以确定是类型安全的
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null;
return result;
}
}
上面这个泛型的实现还是很简洁的,只需要做一次类型转换,但缺点是有堆污染,也就是数组的编译时类型和运行时类型不一样,除非E是Object类型,即使这种情况下堆污染是无害的,也有些令人不爽,但我们还有下面的方案:使用Object数组,对取出的元素内部转换,而不是让客户端去做类型转换,这也是ArrayList的做法。
public class Stack<E> {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
//push时的元素只能是E,所以这里是类型安全的
@SuppressWarnings("unchecked")
E result = (E) elements[--size];
elements[size] = null;
return result;
}
}
上面泛型的参数类型只要是引用就是合法的,有些场景我们需要对参数类型进行限制,也就是有限制的类型参数,如下
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
E extends Delayed说明参数类型必须是Delayed的子类,这样客户端在使用Delayed方法时就不需要冒险强转。泛型比那些需要在客户端做转换的类型要简单和安全,如有可能尽量泛型化。
Item 30 首选泛型方法
和类一样,方法也可以从泛型中获得同样的好处,尤其是静态工具方法,一个简单的静态泛型方法如下,特殊的地方在于修饰符static和返回值Set<E> 之间需要有类型参数<E>,这个泛型方法的两个输入参数和返回参数类型是一样的,如果使用有限制的通配符会更加灵活,这个下一节说明。
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
书中介绍的常用于函数的泛型单列工厂模式,没看出来有什么用途,暂时跳过。
// 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;
}
在深入理解 Java Object一文中介绍的比较两个对象是否相等的equals方法,就会发现使用的不是泛型,实现比较的代码就比较冗长且模版化
PhoneNumber.java
@Override
public boolean equals(Object o) {
//判断引用是否相等
if (o == this) {
return true;
}
if (!(o instanceof PhoneNumber)) {
return false;
}
PhoneNumber pNum = (PhoneNumber) o;
...
而对象如果要比较排序的话,通常需要实现下面这个泛型接口
public interface Comparable<T> {
int compareTo(T o);
}
类型参数T限制了需要比较的对象的类型,因为绝大部分比较都是在同类型之间进行,这个方法的复写就比equals简洁的多
PhoneNumber.java
public class PhoneNumber implements Comparable<PhoneNumber> {
@Override
public int compareTo(PhoneNumber phoneNumber) {
int result = Short.compare(areaCode, phoneNumber.areaCode);
if (result == 0) {
result = Short.compare(prefix, phoneNumber.prefix);
if (result == 0) {
result = Short.compare(linNum, phoneNumber.linNum);
}
}
return result;
}
}
Comparable<T>接口出现在泛型方法中通常都伴随着一个令人头疼的名字:递归类型限制,如下所示的<E extends Comparable<E>>
public static <E extends Comparable<E>> E max(Collection<E> c);
类型限制<E extends Comparable<E>>可以读作针对可以与自身进行比较的每个类型E,也就是列表中的每个元素E都是可以互相比较的。
总之,泛型方法和泛型一样,不需要转化参数就能使用,这样更加安全简洁,所以尽可能将方法泛型化。
Item 31 使用有限制通配符提升API的灵活性
在Item 28中提到过:参数化类型是不可变的。比如Integer是Number的子类,但List<Integer>并不是List<Number>的子类,导致下面的例子编译不通过,这种无限制的类型参数降低了代码的灵活性
public class Test {
public static void main(String[] args) {
List<Integer> integers=new ArrayList<>(10);
integers.add(2);
//print(integers);编译不通过
}
public static void print(List<Number> number){
System.out.println(number.toString());
}
}
还好Java提供了一种有限制的通配符类型解决这类问题,我们对print方法作如下修改,这时输入参数的含义就不是Number的集合,而是Number的某个子类的集合(包括Number自己)。但示例仅仅说明有限制的通配符类型,并没有涉及泛型
public class Test {
public static void main(String[] args) {
List<Integer> integers=new ArrayList<>(10);
integers.add(2);
print(integers);
}
//使用有限制的通配符类型
public static void print(List<? extends Number> number){
System.out.println(number.toString());
}
}
在Item 29我们有个泛型类Stack
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
如果我们添加一个方法addAll,按照之前的思路会是下面这个样子
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
如果一个名为Obj的对象是E的子类,调用push(Obj)没有问题,但如前文所述,调用pushAll(Iterable<Obj>)就不行,解决方案就是改成下面这个样子
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
如果再添加一个方法,把Stack所有元素弹出到某个集合中,根据经验应该写成这样,其实这样编译不通过的,Java中父类引用可以指向子类对象,但反过来不行,下面就犯了这个错误,dst中的引用是E的子类类型,pop返回的是E,就好像往List<Integer>添加Nubmer,所以无法添加
public void popAll(Collection<? extends E> dst) {
while (!isEmpty())
dst.add(pop());
}
知道错误在哪就好办了,只要 限定dst中元素引用是E的父类类型即可,只要把extends改成super即可
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) dst.add(pop());
}
关于使用extends还是super有一个口诀
- 如果输入参数是生产者,如addAll的参数,使用extends
- 如果是消费者,如popAll的参数,使用super
在上一节有这样一个泛型方法,之前也提到了这种输入参数和返回参数类型都一致的泛型不够灵活,只能是同一种类型
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
这个方法如果想把Integer类型和Double类型合并就做不到,所以需要使用有限制的通配符类型,两个参数都是生产者,修改如下,注意返回参数类型没变
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
这种带泛型的有限通配符类型方法使用起来就更加灵活
public static void main(String[] args) {
//Set没有of这个方法,编译是不通过的,为了演示简洁
Set<Integer> integers = Set.of(1, 3, 5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
//Java 8可以推导出E的类型为Number
Set<Number> numbers = union(integers, doubles);
//Java 8 之前无法推导出,需要使用显示的类型参数
// Set<Number> numbers2 = Test.<NUmber>union(integers, doubles);
}
现在回头看Item 30的max方法,发现也有优化空间
- 参数c是生产者,从Collection<E>改成Collection<? extends E>
- Comparable始终是消费者,Comparable<? super E>优于Comparable<E>
//优化前
public static <E extends Comparable<E>> E max(Collection<E> c);
//优化后
public static <E extends Comparable<? super E>> E max(Collection<? extends E> c);
举个例子说明这样优化的好处,下面代码来自JDK。Delayed接口继承了Comparable接口,类型参数为Delayed
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
ScheduledFuture又继承了Delayed接口,所以间接的继承了Comparable接口,但是Comparable的类型参数不是ScheduledFuture,而是父类Delayed
public interface ScheduledFuture<V> extends Delayed, Future<V> {
}
所以下面这个集合就不能传入优化前的max方法
List<ScheduledFuture<?>> scheduledFutures = ... ;
因为ScheduledFuture没有 extends Comparable< ScheduledFuture >,而是extends Comparable<Delayed >,Delayed是Comparable的父类。但优化后的max就可以,因为Comparable的类型参数声明为E,也就是这里的ScheduledFuture的父类。
关于类型参数和通配符还有一些值得讨论,比如下面两个交换元素的静态方法,如果类型参数只在方法中出现一次,就可以用通配符替换
//使用类型参数实现
public static <E> void swap(List<E> list, int i, int j);
//使用通配符实现,更简单
public static void swap(List<?> list, int i, int j);
虽然推荐第二种方法,但List<?>有一个问题,就是除了Null,不能添加其它元素,好在我们有一个辅助方法弥补这个缺陷
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
//使用私有的方法捕获通配符类型
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
总的来说使用通配符让API更加灵活,基本原则就是
- 生产者使用extends
- 消费者使用super,所有的comparables 和 comparators 都是消费者
Item 32 谨慎结合泛型和可变参数
可变参数可以极大的方便方法的调用,它允许给一个方法传递可变数量的参数,如下所示
public static void main(String[] args) {
show("one","two");
show("one","two","three");
}
public static void show(String... msgs) {
for (String string : msgs) {
System.out.println(string);
}
}
但这样你可能还不满足,参数String是具体类型,如果改成下面这样的泛型参数岂不更妙,
public static void main(String[] args) {
show("one","two");
show(1,3,4);
show(1.2f,3,4f);
}
public static <T> void show(T... msgs) {
for (T string : msgs) {
System.out.println(string);
}
}
虽然运行是没问题的,但有些智能编辑器会警告你:msgs这个可变参数会导致堆污染。可变参数是这样实现的,当调用方法时,创建数组来保存可变长的参数,泛型在编译的时候被擦除,所以所以T... msgs就变成了Object[],但在运行的时候,这个应用指向的类型可能是String[]、Interger[]等,也就是编译时类型和运行时类型不匹配导致的堆污染,容易出现类型转换异常。看下面这个示例。
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
//stringLists=List<String>[],数组时协变类型,没有问题
Object[] objects = stringLists;
//object[0]的引用是参数化类型List<String>,指向了参数化类型对象List<Integer>,导致堆污染
objects[0] = intList;
// 虽然没有显式的cast,但会报ClassCastException
String s = stringLists[0].get(0);
}
在Item 28里说过像new E[],new List<String>[]这样的显式的泛型数组或参数化类型数组是不合法的,但可变参数却可以隐式的使用它们,这是因为泛型和参数化类型的数组在实际开发中实在太好用了,就破例允许这种不一致的存在,下面是JDK一些源码示例。
Arrays.java
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
//注意:此处ArrayList为Arrays的私有静态内部类
return new ArrayList<>(a);
}
Collections.java
@SafeVarargs
public static <T> boolean addAll(Collection<? super T> c, T... elements) {
boolean result = false;
for (T element : elements)
result |= c.add(element);
return result;
}
EnumSet.java
@SafeVarargs
public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) {
EnumSet<E> result = noneOf(first.getDeclaringClass());
result.add(first);
for (E e : rest)
result.add(e);
return result;
}
作者只有在确定可变参数是类型安全的,才可以使用@SafeVarargs注解告知编译器不要警告,那怎样才是安全的,需要满足以下条件
- 不要使用可变参数存储任何值,很容易导致类型不匹配
- 不要暴露可变参数的引用
//使用可变参数存储了其它值,违反第一条
objects[0] = intList;
//暴露了可变参数引用,违反了第二条
static <T> T[] toArray(T... args) {
return args;
}
下面一个示例使用了上面的toArray方法,编译运行都是正常的,但要注意返回数组的类型是由传入的参数的编译时类型决定的,编译器可能没有足够的信息作准确的判断
public static void main(String[] args) {
//传人的参数编译时类型为String
String[] msg=toArray("one","two");
//为Integer
Integer[] integer=toArray(1,3,4);
}
//下面是反编译的字节码 参数类型都变成了Object,但做了正确的类型转换
invokestatic Method toArray:([Ljava/lang/Object;)[Ljava/lang/Object;
checkcast class "[Ljava/lang/String;"
invokestatic Method toArray:([Ljava/lang/Object;)[Ljava/lang/Object;
checkcast class "[Ljava/lang/Integer;"
如果上面这个可以使用,下面这个应该也没什么问题
public static void main(String[] args) {
String[] reuslt=pickTwo("t1", "t2");
}
static <T> T[] pickTwo(T a, T b,) {
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
}
//没有checkcast,返回的就是Object数组,实际类型信息丢失
invokestatic Method toArray:([Ljava/lang/Object;)[Ljava/lang/Object;
invokestatic Method pickTwo(Ljava/lang/Object;Ljava/lang/Object;)[Ljava/lang/Object;
checkcast class "[Ljava/lang/String;"
实际上编译的时候确实没有问题,但运行起来就会报错:ava.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
因为此时toArray接受到的参数编译时类型为Object,所以返回的是Object数组,pickTwo接受到的也是Object数组,强转成String[]就报错。如果如下去除泛型,确定pickTwo的参数类型,也不会报错,只是没什么意义。
static String[] pickTwo(String t1,String t2){
return toArray(t1,t2);
}
下面是一个典型的正确使用泛型可变参数的案例,注意:@SafeVarargs只能用在静态方法、不可覆盖方法和私有方法上(Java 9)
@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;
}
如果你不想使用麻烦的泛型可变参数,可以使用集合替代,并配合Java 9中的List.of,它使用了@safeVarargs注解,是类型安全的
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;
}
不过下面原书的例子我觉得是有问题的,编译不通过,参数类型不匹配
audience = flatten(List.of(friends, romans, countrymen));
前面有问题的代码就可以用集合解决,
public static void main(String[] args) {
List<String> attributes = pickTwo("Good", "Fast", "Cheap");
}
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();
}
总的来说,可变参数底层使用的是数组,和泛型配合不好,如果可变参数要结合泛型,一定要遵守下面事项,保证安全才能用@safeVarargs注解
- 它没有在可变参数数组中保存任何值
- 它没有对不被信任的代码开发该数组
Item33 优选类型安全的异构容器
虽然按照规则使用泛型可以保证类型是安全的,但如果和原始类型混用泛型就有类型安全隐患,下面段代码编译的时候有unchecked warning,但可以运行,也就是整数1添加到了类型参数为String的集合中,只要不取出来涉及类型转换就不会发现问题
List<String> a=new ArrayList<>();
//使用原始类型绕过泛型检查
List b=a;
//成功添加
b.add(1);
这是一种隐患,对于错误应该越早发现越好。Java SDK的Conllections工具类提供如下解决方法,除了泛型,还添加了String.class这个字面量,其传到方法作为参数表示的是Class<String>,它可以作为类型令牌
//checkedList返回一个对ArrayList的包装类
List<String> c=Collections.checkedList(new ArrayList<>(), String.class) ;
List d=c;
//添加失败
d.add(1);
//Collections.java
public static <E> List<E> checkedList(List<E> list, Class<E> type) {
return (list instanceof RandomAccess ?
new CheckedRandomAccessList<>(list, type) :
new CheckedList<>(list, type));
}
上面这段代码编译的时候也有unchecked ,但运行就会报ClassCastException,原理就是添加之前利用Class<T>这个类型令牌作类型检查
Collections.CheckedCollection.java
public boolean add(E e) {
return c.add(typeCheck(e));
}
E typeCheck(Object o) {
//类型检查
if (o != null && !type.isInstance(o))
throw new ClassCastException(badElementMsg(o));
return (E) o;
}