《Effective Java》- 泛型

不要在新代码中使用原生态类型

首先看一个反例,如何往 String 类型的集中中添加 int 类型的数据:

1
2
3
4
5
6
7
8
List<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的子类,也不是它的父类
    • 数组是具体化的,数组在运行时检查它的元素类型约束;相对来说,泛型是不可具体化的,因为泛型是通过在编译期擦除类型来实现的

    1
    2
    3
    4
    5
    6
    7
    // 由于数组是协变的,所以这种写法在编译期并不能检测出来问题
    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");

    相对于运行期报错,最好是在编译期就能检测出来

    数组和泛型不能很好的混合使用,如果在混合使用的过程中。发现了编译期错误或者警告。应该在第一时间用列表代替数组

    优先使用泛型

    使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。只要时间允许,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端

    优先考虑泛型方法

    下面是一个带有返回值的泛型方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public <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 的用法:

    1
    2
    3
    List<? 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 没有继承关系或者父类进行初始化:

    1
    2
    List<? 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 的用法相反:

    1
    2
    List<? super Number> consumer1 = new ArrayList<Object>();
    List<? super Number> consumer2 = new ArrayList<Number>();

    <? super Number> 指定 Number 为最低子类,所以所有的 Number 的父类包括自身都可以实例化这样的 List,但是不能用 Number 的子类进行实例化:

    1
    List<? super Number> consumer3 = new ArrayList<Integer>();

    List<? super Number> 允许写操作,但是写入的对象必须是 Number 的子类对象,但是对于读操作,由于没有限制最大父类,所以从 List<? super Number> 中读出来的对象只能是 Object,确定不了数据的类型

    关于 PECS 的作用,下面是 Collections#copy 的源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public 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 的用法:

    1
    2
    3
    List<Number> numberList = new ArrayList<Number>();
    List<Integer> integerList = new ArrayList<Integer>();
    Collections.copy(numberList, integerList);

    利用 PECS,该函数对集合的 copy 操作通用性非常强

    优先考虑类型安全的异构容器

    SetMap 的原生类型就是一个类型不安全的异构容器,想要做到类型安全的异构容器,需要将键(key)进行参数化而不是将容器(container)参数化,然后将参数化的键提交给容器,来插入或者获取值。用泛型系统来确保值的类型与它的键相符:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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, 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>.classList<Integer>.class 是合法的,并返回了相同的对象引用,就会破坏 Favorites 对象的内部结构

    至今还没有完全令人满意的解决办法。有一种方法称作 super type token,它在解决这一局限性方面做了很多努力,但是这种方法仍然有它自身的局限性。集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上类避开这一限制

《Effective Java》- 枚举和注解 《Effective Java》- 类和接口

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×