《Effective Java》- 泛型

127 阅读9分钟

泛型

泛型可以通知编译器某个集合接受哪种对象类型。编译器自动为元素的插入进行转换,如果插入了错误的类型,在编译时就能够觉察。

Item 26: Don't use raw types

不要使用原生态类型

术语说明

1.泛型类|接口

声明中具有一个或者多个类型参数(type parameter)的类或者接口即称为泛型类或者泛型接口。

2.参数化类型

每种泛型定义一组参数化类型(parameterized type),其格式为:类|接口<实际参数>

3.原生态类型

每种泛型都定义一个原生类型(raw type),例如List<E>之于List,原生类型就是为了兼容泛型出现之前的代码。

但是现在禁止使用这种原型类型的集合。如果使用原生类型,就失去了泛型所在的安全性和描述性方面的所有优势。

有效的方法

对于不确定或者不在乎集合中的类型的情况下,优先使用无限制的类型通配符(unbounded wildcard type)而非使用原生类型。

例如原生类型Set的无限制类型通配类型为Set<?>,后者更加安全和。Set<?>具有严格的约束条件,往集合中插入任意的非null元素都会报错。

两个例外

  • 必须在类文字(class literal)中使用原生类型,规范不允许使用参数化的类型。例如List.class是合法的但是List<String>.class不合法。
  • 在参数化类型上使用instanceof操作符是非法的
if(o instanceof Set) { // raw type
    Set<?> s = (Set<?> o); // wildcard type
}

Item 27: Eliminate unchecked warnings

消除非受检的警告

在实际的泛型编程中,可能遇到各种各样的非受检警告,例如非受检类型转换警告、非受检方法调用警告、非受检参数化可变参数警告、非受检转换警告等等。

尽可能地去消除每一个非受检警告,当你消除了所有的警告,你的代码就能够保证类型安全。然而,当你无法消除所有的警告,但是你能够确保代码中的警告是泛型安全的,可以使用注解@SuppressWarnings("unchecked")来压制警告。

注解@SuppressWarnings("unchecked")可以用在局部变量、方法到类等任何声明中。但是,应当始终在最小范围内使用此注解。 永远不要在类上使用此注解。

ArrayList.toArray方法

// Adding local variable to reduce scope of @SuppressWarnings
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(elements, size, a.getClass());
        return result;
    }
    System.arraycopy(elements, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

在使用注解@SuppressWarnings("unchecked")的时候,最好给出注释,解释为什么压制警告是安全的。

Item 28: Prefer lists to arrays

List优先于静态数组

数组相较于泛型集合而言,有两个主要的不同:

  1. 数组是协变的(covariant),但是泛型集合是可变的(variable)

对于数组,如果SubSuper的子类型,那么Sub[]便是Super[]的子类型。但是这个准则在泛型集合中确是行不通的,List<Sub>不为List<Super>的子类型。

思考一下,List<E>集合这么做有什么好处?

// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException

// Won't compile!
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in")

第一个例子在运行的时候会抛出异常,但是第二个例子在编译的时候就会报错,将错误检测提前,对我们来说当然是一桩美事。

  1. 数组是具体化的(reified)

数组在运行的时候知道和强化他们的元素类型。而泛型则是通过擦除(erasure)来实现的,所谓泛型擦除,即使在运行期间丢弃元素的类型信息,使泛型可以和没有使用泛型的代码随意互用。

从技术角度上来讲,E | List<E> | List<String>等类型应该被称为不可具体化的(non-reifiable)类型。其在运行的时候包含的信息要比其在编译的时候包含的信息更少。

当我们遇到泛型数组创建错误的时候,最好的解决办法就是使用集合类型List<E>而非数组类型E[]。这样可以得到更高的类型安全性以及互用性。

Item 29: Favor generic types

优先泛型类型

如何写好属于自己的泛型呢?想要写好一个泛型,需要遵循以下步骤:

第一步,在一个类的声明中添加一个或者数个参数类型。

第二步,将所有Object类型替换为泛型参数类型。

第三步,将Object[]数组转换为E[]或者数组中的元素进行强制类型转换为E类型。

第四步,使用注解@SuppressWarnings("unchecked")压制警告

在使用JDK或者编写泛型的时候一定要注意,泛型只能为引用数据类型,而不能是基本数据类型。对于基本数据类型,一定要使用其包装类。

还有一些情况,对泛型的类型有着比较严格的限制。例如java.util.concurrent.DelayQueue,它在声明中就严格限制了泛型的类型,DelayQueue<E extends Delayed>,确保队列中的每个元素都是Delayed的子类型,避免了强制类型转换是被的问题。需要注意的是,根据extends的定义,一个类也是自身的子类型。因此,创建一个DelayQueue<Delayed> dq也是合法的。

Item 30: Favor generic methods

优先泛型方法

像类可以是泛型的一样,方法也可以是泛型的。进行参数化类型操作的静态工具方法常常也是泛型的。例如,Collections工具类中的所有算法都是泛型的。

对于一个使用原生类型的方法,将其改造为泛型方法的途径为:

在方法的声明中添加一个类型参数(type parameter),并且在方法中从始至终使用此类型参数。

一般情况下,通过上面的步骤就可以将一个方法变为泛型方法。但是某些情况下,我们想要泛型方法更加灵活,这就需要使用约束的通配类型(bounded wildcard types)了。

尽管这种情况不是很常见,通过某个包含该类型参数本身的表达式来限制类型参数是允许的,即为递归类型限制(recursive type bound)。

递归类型限制的一个典型应用场景为对实现了Comparable<T>接口的对象进行的排序、搜索、求最值等算法。例如,

public  static <E extends Comparable<E>> E max(Collection<E> c);

Item 31: Use bounded wildcards to increase API flexibility

利用有限制的通配符提升API的灵活性

上文讲到,List<String>并不是List<Object>的子类型,这是因为前者只能插入String类型的对象,但是后者却能够插入任意对象,如果满足父子类型的话,有悖设计模式的里氏代换原则。

如果我们想要往List<Number>中插入其子类型Integer类型的数据,应该如何做呢?

这个时候,可以使用有限制的通配符类型(bounded wildcard type)来实现,即


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

同样地,可以使用限制通配符类型来改造实现popAll()方法,即


public void popAll(Collection<? super E> dst) {
    while(!isEmpty()){
        dst.add(pop());
    }
}

为了最大化灵活度,在表示生产者或者消费者的参数类型上使用通配符类型。

为了便于记忆,总结下面两种情况:

  • Producer: extends
  • Consumer: super

但是请注意,一定不要在返回值上使用通配符!

当通配符得到良好应用的时候,类的用户对此几乎是无感的,但是如果在使用的类的过程中还需要考虑通配符类型的问题,那么API设计一定出现了问题。

Item 32: Combine generics and varargs judiciously

谨慎地使用泛型和可变参数

当一个参数化类型的变量指向一个不是该类型的对象的时候,就会出现堆污染(heap pollution)。堆污染会造成编译器的自动类型转换失败,破坏泛型系统的最基础的保障。

其实,带有泛型或者参数化的可变参数方法在Java开发中使用广泛,例如Arrays.asList(T... a)等。Java 7 之后引入了专门用于压制可变参数泛型的注解@SafeVarags。这个注解视为方法的作者对方法的类型安全的一个承诺。

那么,如何才能确保一个泛型的可变参数是安全的呢?

如果可变参数数组只是用来将数量可变的参数从调用程序传入到方法中,中间不涉及任何的引用转换,那么他就是安全的。

允许另一个方法去访问一个泛型可变参数数组是不安全的。当然,有两个例外:

  • 将变参数组传递给另一个带有注解SafeVarags的可变参数方法
  • 将数组传递给只计算数组内容部分函数的可变参数方法

下面一个例子就是最佳实践:

@SafeVarags

static <T> List<T> flatten(List<? extends T>... lists) {
    List<T> result = new ArrayList<>();
    for (List<? extends T> list : lists) {
        result.addAll(list);
    }
    return result
}

上述代码中,方法签名中<T>为泛型声明部分,用于引入一个泛型类型变量T,允许你在方法签名和方法体中使用这个泛型。

如何在正确时间去使用注解@SafeVarags

对于每一个带有泛型的可变参数或者参数化类型的方法,都需要使用@SafeVarargs。需要注意的是,Java 8 中仅能在静态方法或者是final上使用,Java 9 之后也可以在实例化方法中使用了。

泛型可变参数在下列条件下是安全的:

  • 没有在可变参数数组中保存任何值
  • 对非信任的代码不可见

Item 33: Consider typesafe heterogeneous containers

考虑使用类型安全的异构容器

假设想要一个容器,它能够存储各种各样泛型安全的元素,如何去实现呢?

在这种情况下,可以将键(key)进行参数化,而不是将容器(container)进行参数化。然后将这些参数化的键提交到容器用来插入或者取出值。

下面是一个非常简单的异构化容器的实现:


// Typesafe heterogeneous container pattern - implementation 
public 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)); 
    } 
}

Favorites是类型安全的,也是异构的(heterogeneous),它不像普通的KV映射,它的所有键都是不同类型的,我们将此类的容器称为类型安全的异构容器(typesafe heterogeneous container)。

注意:上面的代码中,每个类型仅仅只能存储一个值,如果想要对某个类型存储多个值,则可以将私有的map替换为Map<Class<?>, List<?>>