《Effective Java》- 泛型 8月 21 2018 14 分钟 读完 (约 2113 字) 不要在新代码中使用原生态类型 首先看一个反例,如何往 String 类型的集中中添加 int 类型的数据: 12345678List<String> str = new ArrayList<>();str.add("1");str.add("2");add(str, 3);private void add(List list, Object o) { list.add(o);}上面的代码在编译器并不会抛出异常,而是会有个一个 Unchecked 的警告。但是当获取到 int 类型的时就会抛出 ClassCastException 异常 如果使用像 List这样的原生态类型,就会失掉类型安全性,但是如果使用List这样的参数化类型, 则不会 不确定或者不在乎集合中元素类型的情况下,可以使用无限制的通配符类型:Set<?> 通配符类型是安全的,也可以限制了添加到集合中元素类型 List 参数化类型,可以包含任何对象类型的一个集合 List<?> 通配符类型,只能包含某种未知对象类型的集合 List 原生态类型,不安全 术语 示例 参数化的类型 List 实际类型参数 String 泛型 List 形式类型参数 E 无限制通配符类型 List<?> 原生态类型 List 有限制类型参数 递归类型限制 <T extends Comparable> 有限制通配符类型 List<? extends Number> 泛型方法 static List asList(E[] a) 类型令牌 String.class 消除非受检警告 Unchecked Cast Warning Unchecked Conversion Warning 非受检方法调用警告 非受检普通数组创建警告 非受检转换警告尽可能消除每一个非受检警告,如果无法消除警告,同时可以证明引起警告的代码是类型安全的,可以用一个注解来禁止这条警告。由于这个注解可以用在任何粒度的级别中,比如说类,方法以及一个变量。所以应该注意尽可能的缩小注解的范围。 永远不要在类上用这个注解。尽量用注释把禁止这个警告的原因记录下来 列表优先于数组数组与泛型相比,有两个不同点: 数组是协变的,如果A是B的子类型,那么数组类型A[]就是B[]的子类型。泛型是不可变的,List不是List的子类,也不是它的父类 数组是具体化的,数组在运行时检查它的元素类型约束;相对来说,泛型是不可具体化的,因为泛型是通过在编译期擦除类型来实现的 1234567// 由于数组是协变的,所以这种写法在编译期并不能检测出来问题Object[] objectArray = new Long[1]; objectArray[0] = "I don't fit in"; // 由于泛型是不可变的,所以在编译期就会报错List<Object> ol = new ArrayList<Long>(); // Incompatible types ol.add("I don't fit in");相对于运行期报错,最好是在编译期就能检测出来 数组和泛型不能很好的混合使用,如果在混合使用的过程中。发现了编译期错误或者警告。应该在第一时间用列表代替数组 优先使用泛型使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。只要时间允许,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端 优先考虑泛型方法下面是一个带有返回值的泛型方法: 123456789101112public <E extends Comparable<E>> E max(List<E> list) { Iterator<E> iterator = list.iterator(); E max = iterator.next(); while (iterator.hasNext()) { E next = iterator.next(); if (next.compareTo(max) > 0) { max = next; } } return max;} 泛型方法使用起来比让客户端自行装换输入参数和返回值要安全 利用有限通配符来提升 API 的灵活性关键思想就是怎么使用有限通配符,使用有限通配符的原则:Producer-Extends,Consumer-Super 如果只从 List<String> producer 中读取数据,那么这个 List 就叫做 Producer如果只从 List<String> consumer 中添加数据,那么这个 List 就交错 Consumer 生产者使用 extends,消费者使用 super 的意思是,当使用 ? 定义一个通用类型的通配符时,如果该类是 Producer,那么应该使用 extends 关键字为此类型指定一个最高父类,如果该类是 Consumer,那么应该使用 super 关键字为此类型指定一个最低子类 extends 用法首先看一下 extends 的用法: 123List<? extends Number> producer1 = new ArrayList<Integer>();List<? extends Number> producer2 = new ArrayList<Float>();List<? extends Number> producer3 = new ArrayList<Double>(); <? extends Number> 指定 Number 为最高父类,所以所有的 Number 子类包括它自身都可以实例化这样的 List,但是不能用跟 Number 没有继承关系或者父类进行初始化: 12List<? extends Number> producer4 = new ArrayList<String>();List<? extends Number> producer5 = new ArrayList<Object>(); List<? extends Number> 的写操作是不允许的,如果允许向 List<? extends Number> 中写入数据,那么所有 Number 的子类都可以写入到该 List 中去,此时从该 List 中取数据时就不能保障拿出数据的具体类型 super 用法super 的用法刚好和 extends 的用法相反: 12List<? super Number> consumer1 = new ArrayList<Object>();List<? super Number> consumer2 = new ArrayList<Number>(); <? super Number> 指定 Number 为最低子类,所以所有的 Number 的父类包括自身都可以实例化这样的 List,但是不能用 Number 的子类进行实例化: 1List<? super Number> consumer3 = new ArrayList<Integer>(); List<? super Number> 允许写操作,但是写入的对象必须是 Number 的子类对象,但是对于读操作,由于没有限制最大父类,所以从 List<? super Number> 中读出来的对象只能是 Object,确定不了数据的类型 关于 PECS 的作用,下面是 Collections#copy 的源码: 123456789101112131415161718public static <T> void copy(List<? super T> dest, List<? extends T> src) { int srcSize = src.size(); if (srcSize > dest.size()) throw new IndexOutOfBoundsException("Source does not fit in dest"); if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) { for (int i=0; i<srcSize; i++) dest.set(i, src.get(i)); } else { ListIterator<? super T> di=dest.listIterator(); ListIterator<? extends T> si=src.listIterator(); for (int i=0; i<srcSize; i++) { di.next(); di.set(si.next()); } }} Collections#copy 的用法: 123List<Number> numberList = new ArrayList<Number>();List<Integer> integerList = new ArrayList<Integer>();Collections.copy(numberList, integerList); 利用 PECS,该函数对集合的 copy 操作通用性非常强 优先考虑类型安全的异构容器Set、Map 的原生类型就是一个类型不安全的异构容器,想要做到类型安全的异构容器,需要将键(key)进行参数化而不是将容器(container)参数化,然后将参数化的键提交给容器,来插入或者获取值。用泛型系统来确保值的类型与它的键相符: 1234567891011121314public 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, type.cast(instance)); } public <T> getFavorite(Class<T> type) { return type.cast(favorites.get(type)); }}其中 Class#cast 保证键和值之间的类型关系,每个值的类型都与键的类型相同,Favorites 类有一个局限性,它不能用在不可具体化的(non-reifiable)类型中:如果试图保存最喜爱的 List<String>,程序就不能进行编译.原因在于你无法为 List<String> 获得一个 Class 对象:List<String>.class 是个语法错误,这也是件好事。 List<String> 和 List<Integer> 共用一个 Class 对象,即 List.class。如果从“字面(type literal)”上来看,List<String>.class 和 List<Integer>.class 是合法的,并返回了相同的对象引用,就会破坏 Favorites 对象的内部结构 至今还没有完全令人满意的解决办法。有一种方法称作 super type token,它在解决这一局限性方面做了很多努力,但是这种方法仍然有它自身的局限性。集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上类避开这一限制 #Java 《Effective Java》- 枚举和注解 《Effective Java》- 类和接口
评论