《Effective Java》笔记23-29:泛型
# 第23条:请不要在新代码中使用原生态类型
声明中具有一个或者多个类型参数的类或者接口,就是泛型类或者泛型接口,统称为泛型。如 List<E>
。
每种泛型可以定义一种参数化的类型,格式为:先是类或者接口的名称,接着用尖括号(<>
)把对应于泛型的类型参数的实际类型参数列表括起来。如 List<String>
是一个参数化的类型,表示元素类型为 String 的列表。
每个泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称,也是没有泛型之前的类型。如:List<E>
相对应的原生态类型是 List
。
如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。既然不应该使用原生态类型,为什么 Java 设计还要允许使用它们呢?这是为了提供兼容性,要兼容以前没有使用泛型的 Java 代码。
原生态类型 List
和参数化的类型 List<Object>
之间到底有什么区别呢?不严格地说,前者逃避了泛型检查,后者则明确告知编译器,它能够持有任意类型的对象。
你可以将 List<String>
传递给 List
的参数,但不能将它传递给类型 List<Object>
的参数。泛型有子类型化的规则:List<String>
是原生态类型 List 的一个子类型,而不是参数化类型 List。因此,如果用不用像 List 这样的原生态类型,就会失掉类型安全性,但是如果使用像 List<Object>
这样的参数化类型,则不会。
在不确定或者不在乎集合中的元素类型的情况下,也许会使用原生态类型,这是很危险的。Java 提供了一种安全的替代方法,称作无限制的通配符,可以使用一个问号代替。例如,泛型 Set<E>
的无限通配符为 Set<?>
(读作某个类型的集合)。
在无限制通配类型 Set<?>
和原生态类型 Set
之间有什么区别呢?Set 可以将任何元素放进使用原生态类型的集合中,因此很容易破坏该集合的类型约束条件;但不能将任何元素(除了 null 之外)放到 Set<?>
中。
这条规则有两个例外,这是因为“泛型信息在运行时就会被擦除”:
- 在获取类信息中必须使用原生态类型(数组类型和基本类型也算原生态类型),规范不允许使用参数化类型。换句话说:List.class,String[].class 和 int.class 都是合法,但是
List<String>.class
和List<?>.class
都是不合法的。 - 这条规则的第二个例外与
instanceof
操作符有关,由于泛型信息在运行时已被擦除,因此在参数化类型而不是无限制通配符类型上使用 instanceof 操作符是非法的,用无限制通配符类型代替原生态类型,尖括号<>
和问号?
就显得多余了。下面是利用泛型来使用 instanceof 操作符的首先方法:
if (o instanceof set) {
Set<?> m = (Set<?>)o;
// ...
}
术语介绍:
- 参数化的类型:
List<String>
- 实际类型参数:
String
- 泛型:
List<E>
- 形式类型参数:
E
- 无限制通配符类型:
List<?>
- 原生态类型:
List
- 有限制类型参数:
List<E extends Number>
- 递归类型限制:
List <T extends Comparable<T>>
- 有限制通配符类型:
List<? extends Number>
- 泛型方法:
static<E> List<E> asList(E[] a)
- 泛型令牌:
String.class
第24条:消除非受检警告
用泛型编程时,会遇到许多编译器警告:非受检强制转换警告、非受检方法调用警告、非受检普通数组创建警告,以及非受检转换警告。
要尽可能地消除每一个非受检警告。如果消除了所有警告,就可以确保代码是类型安全的。
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,只有在这种情况下才可以用一个 @SuppressWarnings("unchecked")
注解来禁止这条警告。
SuppressWarnings 注解可以用在任何粒度的级别中,从单独的局部变量到整个类都可以。应该始终在尽可能小的范围中使用 SuppressWarnings 注解。它通常是个变量声明,或者是非常简短的方法或者构造器。
将 SuppressWarnings 注解放在 return 语句中是非法的,应该声明一个局部变量来保存返回值,并注解其声明。
public <T> T[] toArray(T[] a) {
if (a.length < size) {
//TODO: 加入更多的注释,以便后面的维护者可以非常清楚该转换是安全的。
@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;
}
总而言之,非受检警告很重要,不要忽略它们。每一条警告都表示可能在运行时抛出 ClassCastException 异常。要尽最大的努力消除这些警告。如果无法消掉同时确实是类型安全的,就可以在尽可能小的范围中,用 @SuppressWarnings("unchecked") 注解来禁止这条警告。要用注释把禁止该警告的原因记录下来。
第25条:列表优先于数组
数组与泛型相比,有两个重要的不同点:首先,数组是协变的,如 Sub 为 Super 的子类型,那么数组类型 Sub[] 就是 Super[] 的子类型。但泛型则是不可变的,对于任意两个不同的类型 Type1 和 Type2,List<Type1>
与 List<Type2>
没有任何父子关系。
下面的代码片段是合法的:
Object[] objectArray = new Long[1];
objectArray[0]= ""; // 运行时抛异常
但下面这段代码则在编译时就不合法:
List<Object> ol = new ArrayList<Long>(); // 编译时就不能通过
ol.add("");
利用数组,你会在运行时才可以发现错误,而利用列表,则可以在编译时发现错误。
数组与泛型之间的第二大区别在于,数组是具体化的。因此数组会在运行时才知道并检查它们的元素类型约束。相比,泛型则是通过擦除来实现的。因此泛型只在编译时强化它们的类型信息,并在运行时丢弃它们的元素类型信息。
由于上述这些根本的区另,因此数组和泛型不能很好混合使用。例如,创建泛型、参数化类型或者类型参数的数组是非法的,如:new List<E>[]
、new List<String>[]
、new E[]
都是非法的。
为什么不允许创建泛型数组呢?看具体例子:
List<String>[] stringLists= new List<String>[1]; // 1
List<Integer> intList = Arrays.asList(42); // 2
Object[] objects = stringLists; // 3
objects[0] = intList; // 4
String s = stringLists[0].get(0); // 5
这里首先假设第一行可以,其他行本身编译是没有问题的,但运行到 5 行时肯定会抛出 ClassCastException 异常。为了防止出现这种情况,创建泛型数组第1行就不允许了。
从技术角度说,像 List<String>
、List<E>
、E
这样的类型应称作为不可具体化的类型。直观地说,不可具体化的类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。唯一可具体化的参数化类型是无限制的符类型,如 List<?>
和 Map<?,?>
,虽然不常用,但是创建无限制通配类型的数组是合法。
当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型 List<E>
,而不是 E[]
。这样可以会损失一些性能或者简洁性,但是挽回的是更高的类型安全性和互用性。
第26条:优先考虑泛型
考虑第6条中的堆栈实现,将它定义成泛型类。
第一种是将 elements 定义成类型参数数组:
public class Stack<E> {
private E[] elements; // 定义成类型参数数组
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
@SuppressWarnings("unchecked")
public Stack() {
// elements = new E[DEFAULT_INITIAL_CAPACITY]; // 无法创建不可具体化的类型数组
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 创建一个 Object 的数组,并将它转换成泛型数组类型
// 还是会有警告,可以加上 SuppressWarnings 注解
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // 解除过期引用
return result;
}
//...
}
第二种是将 elements 域的类型从 E[] 改为 Object[]:
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) {
// ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
// result = (E)elements[--size]; // 这行会报错
@SuppressWarnings("unchecked")
E result = (E)elements[--size]; // 装换为 E
elements[size] = null;
return result;
}
//...
}
禁止数组类型的未受检转换比禁止标量类型的更加危险,所以建议采用第二种方案。但第二种方案可能需要多次转换为 E
,而不是只转换一次为 E[]
,所以第一种方案更常用。
总之,使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型时,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的,只要时间允许,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端。
第27条:优先考虑泛型方法
静态工具方法尤其适合泛型方法。Collections 工具类中的所有算法方法都泛型化了。
public class Union {
// 泛型方法
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
}
union 方法的局限性在于,三个集合的类型(两个输入参数和一个返回值)必须全部相同。利用有限制的通配符类型,可以使这个方法变得更加灵活。
泛型方法的一个显著特性是,无需明确指定类型参数的值,不像调用泛型构造器的时候是必须指定的。编译器通过检查方法参数的类型来计算出类型参数的值。对于上面两个参数都是 Set<String>
类型,因此知道类型参数E必须为 String,这个过程称作为类型推导。
递归类型限制最普遍的用途与 Comparable 接口有关,<T extends Comparable<T>>
,可以读作“针对可以与自身进行比较的每个类型 T”
例如根据元素的自然顺序计算列表的最大值:
public static <T extends Comparable<T>> T max(List<T> list) {
Iterator<T> i = list.iterator();
T result = i.next();
while (i.hasNext()) {
T t = i.next();
if (t.compareTo(result) > 0) {
result = t;
}
}
return result;
}
总之,泛型方法就像泛型一样,使用起来比要求客户端转换输出参数并返回值的方法来得更加安全,也更加容易,就像类型一样,你应该确保新的方法可以不用转换就能使用,这通常意味着要将它们泛型化。
第28条:利用有限制通配符来提升 API 的灵活性
? extends E
现在在前面前面第 26 条中的 Stack<E>
中加上以下方法,以便将某个集合一次性放入到栈中:
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
这么写如果 src 的元素类型与 Stack 完全一致,就没有问题。但是假如有个 Stack<Number>
,并且调用了 push(intVal),这里的 intVal 就是 Integer 类型,因为 Integer 是 Number 的一个子类型。
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
numberStack.pushAll(integers);
这么做,这就好比要将 Iterable<Integer>
赋值给 Iterable<Number>
一样,这显然是不可以的,因为 Iterable<Integer>
不是 Iterable<Number>
的子类型。这样就显得缺少灵活性了,幸好有限制的通配符类型可以帮我们解决:
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
经过上面的修改后 src 不只是可以接受 E 类型的 Iterable,还可以接受 E 的子类型 Iterable。
? super E
现在编写一个 popAll 对应的方法,它从栈中弹出每个元素,并将这些元素到传进去参数集合中,下面如果这样设计:
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
应用代码如下:
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = new ArrayList<Object>();
numberStack.popAll(objects);
不能将一个 Collection<Object>
赋值给 Collection<Number>
,有这样一个限制通配类型来解决这个问题:
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
现在 dst 不只是可以接受 E 类型的 Collection 了,而且还可以接受 E 的父类型 Collection。
总结
为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者又是消费者,那么通配符就没什么用了,因为这时候需要的是严格的类型匹配。如果参数化类型表示一个 T 生产者,就使用 <? extends T>
,如果它表示一个 T 消费者,就使用 <? super T>
。
显式的类型参数
将第 27 条的 union 方法修改一下,让它能同时接受 Integer 与 Double 集合,由于这两个集合是生产者,所以使用 <? extends E>
限制通配符:
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) ;
注意不要使用通配符类型作为返回类型。
如果有这以下两个集合:
Set<Integer> integers = new HashSet<Integer>();
Set<Double> doubles = new HashSet<Double>();
Set<Number> numbers =Union.union(integers, doubles); // 编译不能通过
只能是这样使用显式的类型参数,而不使用类型推导:
Set<Number> numbers =Union.<Number>union(integers, doubles);
或者使用类型推导,则只能以通配类型来接受:
Set<? extends Number> numbers = union(integers, doubles);
优先使用 Comparable<? super T>
接下来,我们将第27条的 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) {
//只能使用通配类型来接受,因为iterator()方法返回的为Iterator<E> 类型,又Iterator<Object>并不是Iterator<String>的父类,所以这里也需要修改一下
Iterator<? extends T> i = list.iterator(); // 1
//但这里不需要使用通配类型来接收,因为next()返回的类型直接就是类型参数E,而不像上面返回的为Iterator<E>泛型类型
T result = i.next(); // 2
while (i.hasNext()) { // 3
T t = i.next(); // 4
if (t.compareTo(result) > 0) { // 5
result = t;
}
}
return result;
}
因为 list 只用来读取或生产元素(第1、2、4行都是从 list 中读),所以从 List<T>
修改成 List<? extends T>
,让 list 可以接受 T 及其它的子类。
而 Comparable<T>
应该修改成 Comparable<? super T>
,因为 Comparable 只是来消费 T 的实例(第5行属于消费,因为 T 的实例调用带有泛型类型参数的 compareTo 方法)。
假如现在有以下两个接口:
interface I1 extends Comparable<I1> {}
interface I2 extends I1 {}
如果上面不这样修改的话,下面第二行将不适用:
max(new ArrayList<I1>());
max(new ArrayList<I2>());
现在我们具体的分析一下上面代码:如果 Comparable<T>
不修改成 Comparable<? super T>
,第一行还是可正常运行,但是第二行则不可以,因为此时的 T 为 I2,而 I2 又没有实现 Comparable 接口,而方法声明 <T extends Comparable<T>>
部分则要求 I2 直接实现 Comparable
接口,但父类I1实现了 Comparable
接口,I2 又已经继承了 I1,我们不需要再实现该接口,所以这里变通的作法是让 Comparable<T>
可以接收 T 及 T 的父类类型,所以修改成 Comparable<? super T>
即可,并且这样修改也符合前面的描述。
所以,使用时始终应该是 Comparable<? super T>
优先于 Comparable<T>
,对于 comparator
也一样,使用时始终应该是 Comparator<? super T>
优先于 Comparator<T>
。
类型参数和通配符之间具有双重性
类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或者另一个进行声明。例如下面是可能的两种静态方法声明,来交换列表中的两个元素,第一个使用无限的类型参数,第二个使用的是无限的通配符:
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
一般来说,如果类型参数只在方法声明中出现一次(即只在方法参数声明列表中出现过,而方法体没有出现),就可以用通配符取代它。如果是无限制的类型参数 <E>
,就用无限制的通配符取代它 <?>
;如果是有限制的类型参数 <E extends Number>
,就用有限制的通配符取代它 <? extends Number>
。
第二种实质上会有问题,下面是简单的实现都不能通过编译:
public static void swap(List<?> list, int i, int j) {
list.set(i, list.get(j)); // 不能将 null 之外的任何值放到 List<?>中
}
但可以修改它,编写一个私有的辅助方法来捕捉通配符类型:
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.get(j));
}
总之,在API中使用通配符类型使 API 变得灵活多。如果编写的是一个被广泛使用的类库,则一定要适当地利用通配类型。记住基本原则:producer-extends
,consumer-super
(PECS
)。还要记住所有的 Comparable 和 Comparator 都是消费者,所以适合于 <? super XXX>
。
PECS:如果参数化类型表示一个 T 生产者,就使用 <? extends T>
;如果它表示一个 T 是消费者,就使用 <? super T>
。
第29条:优先考虑类型安全的异构容器
不常用。
通过对泛型的学习我们知道,泛型集合一旦实例化,类型参数就确定下来,只能存入特定类型的元素,比如:
Map<K, V> map = new HashMap<K, V>();
则只能将 K、V 及它们的子类放入 Map 中,就不能将其他类型元素存入。如果使用原生 Map 又会得到类型安全检查,也许你这样定义:
Map<Object,Object> map = new HashMap<Object,Object>();
map.put("Derive", new Derive());
map.put("Sub", new Sub());
这样是可以存入各种类型的对象,虽然逃过了警告,但取出时我们无法知道确切的类型,它们都是 Object,那么有没有一种这样的 Map,即可以存放各种类型的对象,但取出时还是可以知道其确切类型,这是可以的:
public class Favorites {
// 可以存储不同类型元素的类型安全容器,但每种类型只允许一个值,如果存放同一类型多个值是不行的
private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null)
throw new NullPointerException("Type is null");
// favorites.put(type, instance);
/*
* 防止客户端传进原生的 Class 对象,虽然这会警告,但这就不能
* 确保后面 instance 实例为 type 类型了,所以在这种情况下强制检查
*/
favorites.put(type, type.cast(instance));
}
public <T> T getFavorite(Class<T> type) {
//返回的还是存入时真真类型
return type.cast(favorites.get(type));
}
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());
}
}
总之,集合 API 说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这一限制。对于这种类型安全的异构容器,可以用 Class 对象作为键。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com
文章标题:《Effective Java》笔记23-29:泛型
文章字数:5.4k
本文作者:Bin
发布时间:2019-05-27, 20:26:41
最后更新:2019-08-06, 00:43:17
原始链接:http://coolview.github.io/2019/05/27/Effective-Java/%E3%80%8AEffective%20Java%E3%80%8B%E7%AC%94%E8%AE%B023-29%EF%BC%9A%E6%B3%9B%E5%9E%8B/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。