Effective Java (3rd Editin) 读书笔记:4 泛型

4 泛型

泛型中的术语:

术语 例子
参数化类型(Parameterized type) List<String>
实际类型参数(Actual type parameter) String
泛型(Generic type) List<E>
形式类型参数(Formal type parameter) E
无界通配符类型(Unbounded wildcard type) List<?>
原类型(Raw type) List
有界类型参数(Bounded type parameter) <E extends Number>
递归类型限制(Recursive type bound) <T extends Comparable<T>>
有界通配符类型(Bounded wildcard type) List<? extends Number>
泛型方法(Generic method) static <E> List<E> asList(E[] a)
类型标记(Type token) String.class

Item 26:不要使用泛型的原类型

总结:使用原类型会失去泛型的安全性和表达性,可能在运行时报错,还需要自己转型。

声明中有一个或多个类型参数(type parameter)的类或接口,被称为泛型类和泛型接口,统称为泛型(generic types)。List<String> 读作“List of String”,是一个参数化类型,代表一个元素类型为 String 的 List。

每个泛型都定义了一个原类型(raw type),就是这个泛型没有伴随的类型参数时的名字。比如,List<String> 的原类型为 List。原类型主要是为了兼容引入泛型之前的代码。

不指定类型参数,只使用原类型时,如 List list = new ArrayList();,编译器会给出警告,但是能编译通过。但是这种方式失去了泛型的优点:

  1. 编译时类型检查(插入操作)
  2. 自动转型(查询操作)

List<String> 是 List 的子类型,但不是 List<Object> 的子类型。虽然都可以存放 Object 对象,但是 List<Object> 比 List 要好得多,它表示可以容纳任何类型的对象。下面的代码,会在编译时给出警告,虽然编译成功,但运行时会在自动转型时出错:

// 运行时出错 —— 使用了原类型
public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    unsafeAdd(strings, Integer.valueOf(42));
    String s = strings.get(0); // 编译器自动生成自动转型
}

private static void unsafeAdd(List list, Object o) {
	list.add(o);
}

如果真的不确定集合中会存放什么类型的话,你应该使用无界通配符类型(unbounded wildcard type),如 Set<?>,它代表一个 Set 仅仅可以容纳某种未知类型的对象。Set<?> 和 Set 的区别在于,前者是类型安全且灵活的,后者不安全;前者不能插入任何非 null 对象,后者能插入任何对象。

注意,有一些需要使用原类型的场景:

  • 在类的字面量中必须使用原类型,List.classString[].classint.class 是正确的,但 List<?>.class List<String>.class 是错误的。

  • instanceof 操作符中使用原类型就够了,因为泛型信息会在运行时擦除,在 instanceof 操作符中只能使用无界通配符类型或原类型,而且两者并没有区别,推荐后者。

    // instanceof 操作符的推荐使用方式
    if (o instanceof Set) {
        Set<?> s = (Set<?>) o; // 这种转型时有检查的,不会有编译时警告
    }
    

Item 27:消除未检查警告

总结:未检查警告很重要,不要忽略,每个未检查警告都意味着可能在运行时抛出 ClassCastException 异常。尽最大的能力消除它们。当不能消除且确信代码是类型安全时,在最小的作用域中使用 SuppressWarnings 注解,并在注释中记录你抑制此警告的理由。

使用泛型时,编译器可能给出很多种未检查警告:

  • unchecked cast warning
  • unchecked method invocation warning
  • unchecked parameterized vararg type warning
  • unchecked conversion warning

如果你已经尽可能消除警告,并确信自己的代码没有错误,可以使用 SuppressWarnings 注解(可以作用于类,方法,变量声明),但是注意,一定要最小化注解的作用范围,而且要添加注释说明为什么这是安全的

@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;
}

ArrayList 中的 toArray 方法的注解可以修改为:

public <T> T[] toArray(T[] a) {
    if (a.length < size) {
        // This cast is correct because the array we're creating
        // is of the same type as the one passed in, which is T[].
        @SuppressWarnings("unchecked") T[] result = 
        	(T[]) Arrays.copyOf(elementData, size, a.getClass());
    	return result;
    }
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

Item 28:用 List 代替数组

综述:数组是协变的、reified,泛型是不变的、擦除式的。因此数组不能提供编译时安全。数组和泛型不能混和定义,如果因为两者的转换而有编译时错误或者警告,你的第一反应应该是用 list 代替数组。

数组和泛型类型有两个不同:

  • 数组是协变的(covariant),如果 Sub 是 Super 的子类,那么 Sub[] 是 Super[] 的子类;泛型是不变的(invariant),任意两个类型 T1 和 T2,List<T1>List<T2> 不存在父子关系。因此,数组有更多的风险:

    Object[] objectArray = new Long[1];
    objectArray[0] = "I don't fit in"; // 运行时抛异常 ArrayStoreException,编译时有警告但能通过
    
    List<Object> ol = new ArrayList<Long>(); // 编译失败,Incompatible types
    
  • 数组是 reified,它们会在运行时知道并强制元素类型;泛型是 erasure,它们会在编译时强制类型约束,并在运行时擦除元素类型。

泛型和数组不能混合使用,new List<E>[]new List<String>[]new E[] 都是非法的语句。

为什么泛型数组是非法的?因为它不是类型安全的,如果它是合法的,那么编译时生成的自动类型转换会在运行时抛出 ClassCastException,这违反了泛型类型体系对类型安全的基本保证。

举个例子:

List[] lists = new List[1];
List<Integer> intList = new ArrayList<>();
intList.add(43);
Object[] objects = lists;
objects[0] = intList;
System.out.println(lists[0].get(0));

上面的代码使用了原类型数组,这是合法的,因为 List<Integer> 是 List 的子类,原类型数组的安全隐患是出于原类型本身就存在类型安全隐患(Item 26),虽有隐患但合法。但如果改写成下面的代码片段:

List<String>[] lists = new List<>[1]; // 假设是合法的
List<Integer> intList = new ArrayList<>();
intList.add(43);
Object[] objects = lists;
objects[0] = intList;
System.out.println(lists[0].get(0));

除了第 1 行外,其它行的代码都是合法的,假设现在允许泛型数组,那么,在上面的第 5 行代码处,将 List<Integer> 实例存储在一个声明为只存储 List<String> 实例元素的数组中,在第 6 行取元素时,编译器会自动将 Integer 元素类型转换为 String,因此在运行时抛出 ClassCastException,这违反了泛型类型体系对类型安全的基本保证。

使用 List<E> 代替数组,可以免除很多集合转数组的限制,虽然牺牲简洁性和性能,但得到类型安全和兼容性。比如要实现一个返回集合中一个随机元素的类,有如下三种方案。

方案一:对象数组

    static class Chooser {
        private final Object[] choiceArray;

        public Chooser(Collection choices) {
            choiceArray = choices.toArray();
        }
        
        public Object choose() {
            Random rnd = ThreadLocalRandom.current();
            return choiceArray[rnd.nextInt(choiceArray.length)];
        }
    }

缺点:获取元素需要自动转型,万一转错了呢?

方案二:泛型转数组

    static class Chooser<T> {
        private final T[] choiceArray;
        // The type-safe is guaranteed by generic type.
        @SuppressWarnings("unchecked")
        public Chooser(Collection<T> choices) {
            choiceArray = (T[]) choices.toArray();
        }
        
        public T choose() {
            Random rnd = ThreadLocalRandom.current();
            return choiceArray[rnd.nextInt(choiceArray.length)];
        }
    }

这是类似 ArrayList 中 toArray 方法。缺点是,有未检查警告。

方案三:全部用泛型,不用数组

    static class Chooser<T> {
        private final List<T> choiceList;
        // The type-safe is guaranteed by generic type.
        @SuppressWarnings("unchecked")
        public Chooser(Collection<T> choices) {
            choiceList = new ArrayList<>(choices);
        }
        
        public T choose() {
            Random rnd = ThreadLocalRandom.current();
            return choiceList.get(rnd.nextInt(choiceArray.size()));
        }
    }

缺点:慢一些

需要补充的是:

  1. Java 中的 List 并不是原生类型,很多集合类(如 ArrayList)本身就是用数组实现,而一些泛型类(如 HashMap)基于性能的考虑也使用数组来实现。
  2. 另外由于泛型不支持原始类型,所以要么使用对应装箱类的泛型,要么使用原始类型数组。

Item 29:使用泛型类

综述:由于不需要在客户代码中进行类型转换,泛型类安全和简洁。因此,在设计泛型类时,也要确保在使用时不需要类型转换。

自定义泛型类:

方案一:创建数组时类型转换

static class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    // The elements array will contain only E instances from push(E).
    // This is sufficient to ensure type safety, but the runtime
    // type of the array won't be E[]; it will always be Object[].
    @SuppressWarnings("unchecked")
    public Stack() {
        elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    }

    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; // Eliminate obsolete reference
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, size * 2 + 1);
        }
    }
}

可读性好(E[] 字段),简洁,只有一次类型转换。但是会造成无害的“堆污染”(Item 32):数组的运行时类型和编译时类型不一致。在运行时,E 会被擦除为 Object,elements 数组就是 Object[] 类型,编译时实际上是利用泛型自动进行了类型转换。pop 方法返回 Object 对象会在客户代码中被转换为 E 类型,因为push 方法插入的对象确实是 E 类型,所以这种强转是类型安全的。**注意:**如果我们添加一个获取 elements 数组的 API,就会出现问题,因为 Object[] 不能强转为 E[]

        public E[] getElements(){
            return elements;
        }

方案二:获取元素时类型转换(Java 集合类中的做法)

static 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();
        }
        // push requires elements to be of type E, so cast is correct
        @SuppressWarnings("unchecked") E result = (E) elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, size * 2 + 1);
        }
    }
}

每次获取元素都要类型转换,但没有“堆污染”。


Item 30:使用泛型方法

综述:泛型方法就像泛型类一样,使用安全、容易,不需要对参数和返回值进行类型转换。你可以方便地将需要类型转换的旧方法泛型化,这不会影响旧的客户。

类型参数列表(type parameter list),在方法的修饰符和返回类型之间,声明方法中的所有类型参数,如下面代码块中的 <E>

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

泛型单例工厂:创建一个 immutable 对象,适用于多种不同类型的操作。

   // Collections#reverseOrder()
    @SuppressWarnings("unchecked")
    public static <T> Comparator<T> reverseOrder() {
        return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
    }

ReverseComparator.REVERSE_ORDER 实际上是 Comparator<Comparable<Objec>> 类型,虽然不是对于每个 T,Comparator<Comparable<Object>> 都是 Comparator<T> 类型,但是由于它返回的是一个 immutable 对象,此对象调用 Comparable<Object> 的 comparaTo 方法来进行排序,只要 T 实现了 Comparable 接口,这样就是类型安全的。

public int compare(Comparable<Object> c1, Comparable<Object> c2) {
    return c2.compareTo(c1); 
}

递归类型限制(recursive type bound):类型参数被包含类型参数自身的表达式限制。一个常见的使用与 Comparable 接口有关。

public interface Comparable<T> {
    public int compareTo(T o);
}

在实际使用中,几乎每个类型只和自身类型进行比较,所以 String 实现 Comparable<String> 接口,Integer 实现 Comparable<Integer> 接口。让集合中的元素实现 Comparable 接口,就可以对集合进行排序、搜索、找出最值等。这需要集合中的每个元素可以和集合中的其它元素进行比较,即 mutually comparable。在方法中描述为:

    // 使用递归类型限制来表达相互可比性
    public static <T extends Comparable<T>> T max(Collection<T> c);

<T extends Comparable<T>> 可读作“可以和自身比较的任何类型 T”。


Item 31:使用有限制通配符来增加API灵活性

综述:在 API 中使用通配符,虽然稍显麻烦,但增加灵活性。记住基本规则:PECS。记住 comparable 和 comparator 都是消费者。

Item 28 中我们说过,参数化类型是不变的,虽然 Integer 是 Number 的子类,但 List<Integer> 不是 List<Number> 的子类。

现在有一个如下的栈的 API:

public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

下列操作是有效的:

Stack<Number> stack = new Stack<>();
stack.push(new Integer(1)); // Integer 是 Number 的子类,可以向上转型为 Number

但是现在,我们先添加一个新的 API:

// 不灵活
public void pushAll(Iterable<E> src) {
    for (E e : src)
        push(e);
}

下列操作会报错:

Stack<Number> stack = new Stack<>();
Iterable<Integer> integers = ...;
stack.pushAll(integers); // error: incompatible types

因为Iterable<Integer> 不是Iterable<Number> 的子类,所以不能向上转型,类型不兼容。

幸运的是,我们可以使用有限制通配符类型(bounded wildcard type)来解决这种问题。Iterable<? extends E> 读作“Iterable of some subtype of E”,这里的子类(subtype)一词可能有些误导,但是注意,每一个类型也是自身的子类。

// 通配符作为 E 的生产者
public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}

现在我们又想要加入一个 API:

// 不灵活
public void popAll(Collection<E> dst) {
    while(!isEmpty())
        dst.add(pop());
}

但是下列操作会出现类似的问题:

Stack<Number> stack = new Stack<>();
Collection<Object> objects = ...;
stack.popAll(objects); // error: incompatible types

因为 Collection<Object> 不是 Collection<Number> 的父类,所以类型不兼容。

这里使用另一种有限制通配符类型 Collection<? super E>,读作“collection of some supertype of E”,注意,一个类型也是自身的父类。

// 通配符作为 E 的消费者
public void popAll(Collection<? super E> dst) {
    while(!isEmpty())
        dst.add(pop());
}

在上面 Stack 的例子中,pushAll 方法的参数提供了 Stack 需要的元素,是生产者,使用 extends;popAll 方法的参数使用 Stack 中的元素,是消费者,使用 super。总结起来就是统配符类型的 PECS(producer-extends, consumer-super)规则。

另外有一个关于通配符和类型参数的选择的原则:若类型参数仅在方法声明中出现一次,就改用通配符

我们来看一下交换 List 中元素的两种实现 API:

修改前:类型参数仅在方法声明中出现一次

public static <E> void swap(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);
}

private static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

虽然我们使用更曲折的方法来实现了 swap,但是这样做是值得的,因为它提供了一个更简洁的 API。


Item 32:不定参数中谨慎使用泛型

综述:不定参数不能与泛型很好地合作,是因为不定参数机制是 leaky abstraction 的数组,它是合法的,但有类型安全问题。如果你要定义泛型不定参数方法,要保证方法是类型安全的,并添加 SafeVarargs 注解。

Item 28 中提过,non-reifiable 类型是指运行时表示提供的信息少于编译时表示的类型,几乎所有泛型和参数化类型都是 non-reifiable。

不定参数(Item 53)的特点是允许客户提供长度不定的同类参数给一个方法,实际上,在方法中,创建了一个数组来存储这些参数,考虑到数组和泛型的混合定义问题,有类型安全风险。

// 危险案例一:修改泛型参数数组的值
static void dangerous(List<String>... stringLists) {
    List<Integer> intList = new ArrayList<>();
    intList.add(43);
    Object[] objects = lists;
    objects[0] = intList; // Heap pollution
    System.out.println(lists[0].get(0)); // ClassCastException
}

lists 引用的是一个 List<String>[] 对象,由泛型的特点,调用 lists[0].get(0) 时,会把实际类型 Integer 类型转换为 String。

// 危险案例二:暴露了不定参数生成的泛型参数数组
static <T> T[] toArray(T... args) {
    return args;    
}

static <T> T[] pickTwo(T a, T b, T c) {
    switch (ThreadLocalRandom.current().nextInt(3)) {
        case 0:
            return toArray(a, b);
        case 1:
            return toArray(a, c);
        case 2:
            return toArray(b, c);
        default:
            throw new AssertionError();
    }
}

指定具体类型再使用 toArray 方法时,不定参数生成指定类型的参数数组,如 String[]。在泛型方法 pickTwo 中使用 toArray 时,不定参数不确定泛型的具体类型,生成的是 Object[]。因此最终 Object[] 转换为 T[] 时会抛“ [Ljava.lang.Object; cannot be cast to [Ljava.lang.String; ”异常。

为了让用户知道不定参数方法的安全性,对于每一个使用泛型类型或参数化类型的不定参数方法使用 SageVarargs 注解。但是你要确保此方法确实在使用中是安全的:

  1. 不往不定参数数组中存储元素
  2. 不让不定参数数组及其克隆对不信任代码可见

考虑安全性,SafeVarargs 注解的方法不能被重写,Java 8 中只能用于 static 和 final 方法,Java 9 中还可以用于 private 非静态方法。

// 类型安全的泛型不定参数
@SafeVarargs
static <T> List<T> flatten(List<? extends T>.. list) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists)
        result.addAll(list);
    return result;
}

Item 33:类型标记和异构容器

综述:泛型的一般使用中,如集合类 API 中示范的那样,使每个容器中类型参数的数量是固定的。要避开这个限制,你可以在容器的键中使用类型参数,而不是在容器中使用类型参数。可以使用 Class 对象作为这种类型安全的异构容器的键,这样的 Class 对象被称为类型标记(type token)。比如,你可以用 DatabaseRow 类型代表数据库的一行(作为异构容器),使用泛型类 Column<T> 作为异构容器的键。

一个异构容器的例子:

static class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();

    public <T> void putFavorite(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), instance);
    }

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type)); // 无未检查警告的转型
    }
}

上述例子中类型标记使用了无限制通配符类型,Java 注解类 API 中大量使用了有限制的类型标记:

public <T extends Annotation> T getAnnotation(Class<T> annotationType);

// 使用 asSubclass 方法,安全地由无限制转化为有限制类型标记
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
    Class<?> annotationType = null; // 无限制类型标记
    try {
        annotationType = Class.forName(annotationTypeName);
    } catch (Exception ex) {
        thorow new IllegalArgumentException(ex);
    }
    return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}

// Class#asSubclass()
@SuppressWarnings("unchecked")
public <U> Class<? extends U> asSubclass(Class<U> clazz) {
    if (clazz.isAssignableFrom(this))
        return (Class<? extends U>) this;
    else
        throw new ClassCastException(this.toString());
}

猜你喜欢

转载自blog.csdn.net/weixin_40255793/article/details/82931228
今日推荐