Effective Java : 泛型

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

简介

  • Java泛型从1.5引入,为了保持兼容性,实现的是伪泛型,类型参数信息在编译完成之后都会被擦除,其在运行时的类型都是raw type,类型参数保存的都是Object类型,List<E>raw type就是List
  • 编译器在编译期通过类型参数,为读操作自动进行了类型强制转换,同时在写操作时自动进行了类型检查
  • 如果使用raw type,那编译器就不会在写操作时进行类型检查了,写入错误的类型也不会报编译错误,那么在后续读操作进行强制类型转换时,将会导致转换失败,抛出异常

ListList<Object>

  • 前者不具备类型安全性,后者具备
  • 可以将List<String>传递给List的参数,但不能传递给List<Object>的参数
  • 泛型有子类化(subtyping)规则,List<String>List的子类型,但不是List<Object>的子类型

Set<?>Set区别

当不确定类型参数,或者说类型参数不重要时,也不应该使用raw type,而应该使用List<?>

  • 任何参数化的List均是List<?>的子类
  • 通配符是类型安全的,原生态是不安全的/
  • 被引用的List<?>,并不能向其中加入任何非null元素,读取出来的元素也是Object类型,而不会被自动强转为任何类型
  • instanceof不支持泛型

术语:

24.消除非受检警告

原则

  • 要尽可能的消除每一个 非受检警告
  • 清除警告能够确保不会出现ClassCastException
  • 如果无法消除警告,且确信是安全的,可添加注解
@SuppressWarnings("unchecked")
  • 应该尽量减小注解作用的范围,通常是应该为一个赋值语句添加注解
  • 非受检警告很重要,不要忽视他们,每一条警告都预示着这里会在运行时出现一些问题.

25.列表优于数组

简介

  • 数则是协变的,而泛型是不可变的,如下示例:
// Fails at runtime
    Object[] objects = new Long[1];
    objects[0] = "Throws ArrayStoreException";

// Won`t compile
    List<Object> objectList = new ArrayList<Long>();
    objectList.add("Incompatible types");
  • 数组是具体化的,只有在运行时才能知道并检查他们的元素类型约束.
  • 泛型则是通过擦除来实现,在编译时就知道元素的类型
  • 不能返回泛型元素的数组,必须是具体化类型(即 E[]是类型不安全的)
  • 在结合使用可变参数方法泛型时会出现令人费解的警告

总结

  • 数组是协变且可以具体化的.泛型是不可变且可以被擦除的.
  • 数组提供了运行时类型安全,泛型提供了编译时类型安全

26.优先考虑泛型

简介

第25 条 说明不能创建 不可具体化类型的数组,如E[].如下面这种情况:

elements = new E[DEFAULT_CAPACITY];

解决这种问题,有两种方式

  1. 创建一个 Object数组,并转换成 泛型数组类型

  2. elements域的 类型 从 E改为 Object[]
    这两种方式编译器都无法再运行时类型检验,因此需要自己添加注解.

@SuppressWarning("unchecked")
elements = (E[])new Object[DEFAULT_CAPACITY];

总结

  • 有时看似与item 25矛盾,实属无奈,Java原生没有List,原生只支持数组,ArrayList不得不基于数组实现,HashMap也是基于数组实现的
  • 泛型不支持原生类型,比如List<int>会报编译时错误,使用包装类型即可解决.List<Integer>
  • 泛型比使用者进行cast更加安全,而且由于Java泛型的擦除实现,也可以和未做泛型的老代码无缝兼容

27.优先考虑泛型方法

简介

静态工具方法尤其适合泛型化,编写泛型方法编写泛型类型类似.

// Generic method
    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
        Set<E> result = new HashSet<E>(s1);
        result.addAll(s2);
        return result;
    }
  • 泛型方法无需指定类型参数的值.利用 有限制的通配符可以使方法变得更加灵活.
  • 类型安全如<T extends Comparable<T>>,可以读作”针对可以与自身进行比较每个类型T”.

总结

  • 泛型方法像泛型类一样,需要传入泛型参数
  • 为了消除泛型声明冗余,可以使用静态工厂方法,如下:
Map<String,List<String>> map = new HashMap<String,List<String>>();

public static <K, V> HashMap<K, V> newHashMap() {
    return new HashMap<K, V>();
}
  • 方法使用者就不用类型转换即可使用

28.利用有限制通配符限制API的灵活性

语义:

使用 <? extends T> : 表示T生产者
或者 <? super E>: 表示E消费者
不要用 通配符 作为返回值 类型.

示例

    public void pushAll(Iterable<? extends E> src) {
        for (E e : src)
            push(e);
    }

    // Wildcard type for parameter that serves as an E consumer
    public void popAll(Collection<? super E> dst) {
        while (!isEmpty())
            dst.add(pop());
    }
  1. 这样的化, 这样就可以将 EE的子类放入 集合中.popAll同理
  2. 因为泛型是不可变的,就好像 List<String> 不是 List<Object>的子类一样.但可以通过这种方式来解决.
  • 下面是一个通过 使用通配符 修正的类型声明
//遵循了开头的三个规则
public static <T extends Comparable<? super T>> T max(List<? extends T> list{}

显示的类型参数

如果编译器不能推断类型.我们也可以显示的指定类型,如:

    Set<Number> numbers = Union.<Number>(integers,doubles);

建议

public static <E> void swap(List<E> list,int i,int j);
public static void swap(List<?> list,int i,int j);//使用通配符
  • 如上所示,如果类型参数只在方法声明中出现一次,应该用通配符替代
  • 但是上面的 通配符 方法会编译错误.不能把null之外的任何值放进List<?>(参考第23条),通常通过helper的方式解决
  • 所有的 comparablecomparator都是消费者.
// 不导出,对用户不可见
private static <E> void swapHelper(List<E> list,int i,int j){
    list.set(i,list.set(j,list.get(i));
}

public static void swap(List<?> list,int i,int j){
    swapHelper(list,i,j);
};

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

介绍

使用泛型限制了 每个容器只能有 固定数目的类型参数,有时候我们需要跟过的灵活性,比如 如下示例:

示例: Favorites.java

public <T> void putFavorite(Class<T> type, T instance) {}

public <T> T getFavorite(Class<T> type) {}

这里,存入和取出的是不同类型 也可以满足,而且保证了类型安全,而不是像 Map<K,V>在声明时就 将类型固定 了,这种 被称为 类型安全的异构容器.

TypeToken

1.5 开始 java支持 Class<T>,即 :

String.class 属于 Class<String> 类型
Integer.class 属于 Class<Integer> 类型

这种 当一个类 的字面文字 被用在方法中,来传达 编译时和运行时的类型信息时.被称作 type token

解惑

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);
}
public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
}

如上,虽然 Class<?>使用了 通配符,却 不涉及到 使用通配符容器不能存入任何非null的元素 [参考 23 条], 因为它属于键 key,而不是 map 容器

局限

  • Favorites类并不是运行时安全的.如原生态类型的类,HashSet等,可如下实现安全的运行时类型.
public <T> void putFavorite(Class<T> type, T instance) {
        if (type == null)
            throw new NullPointerException("Type is null");
        favorites.put(type, type.cast(instance));
}

如上这种技巧,普遍的用在了checkedList,checkedMap中.

  • Favorites类不能用在 不可具体化的类型中.就是说,可以保存String,String[],但是不能保存List<String>,因为 List<String>.class是不合法的.尽管可以有TypeToken

总结

泛型限制了 容器使用固定数目的 类型参数,我们可以将类型参数放在 键上,而不是容器上,来避开这一限制,
这种方式叫做 异构容器

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章