Skip to content

泛型

1. 请不要使用原生态类型

Java 5 引入了 泛型 (generic),编译器会自动根据泛型信息做类型转换。

带有参数化类型的类或接口称为泛型类或泛型类型,它们有对应的原生类型

  • 泛型类型: List<E> (E 的列表)
  • 原生类型: List

除了一种情况下,必须使用原生类型:类文字(class literal)中使用原生类型,如 List.class

其余任何情况都不要使用原生类型,编译器没有强制禁止原生类型仅仅是为了兼容之前的代码。

2. 消除非受检的警告

使用泛型编程时会遇到很多编译器警告,比如:

java
//这里会有一个非受检警告
Set<String> set = new HashSet(); ❌
//消除非受检警告
Set<String> set = new HashSet<>(); ✔

要尽可能的消除每一个受检警告,否则都可能在运行时出现 ClassCastException

如果无法消除警告,又可以证明该警告是安全的,那么可以使用 @SuppressWarnings("unchecked") 注解来消除警告。

3. 列表优于数组

  • 数组协变(covariant)的,也就是 Child[] 也是 Parent[] 的子类型
  • 列表可变的,List<Child>List<Parent> 是两个独立类型

你可能认为列表是有缺陷的,实际上数组才是有缺陷的:

java
//这段代码在编译器是合法的
Object[] objectArray = new Long[1];
objectArray[0] = "haha";

//这段代码不合法
List<Object> list = new ArrayList<Long>();
list.add("haha");

实际上上面两种版本都不能将 String 放进 Long 的容器中,但数组在运行期才发现错误;而列表在编译器就发现了错误。

案例: 利用泛型列表实现一个游戏用的色子 (传入一个集合,返回集合中的随机一个元素)

java
class Chooser <T> {
    private final List<T> choiceList;

    public Chooser(Collection<T> choices) {
        choiceList = new ArrayList<>(choices);
    }

    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
    }
}

4. 优先考虑泛型

在设计新类的时候,优先考虑泛型,它们需要在代码中进行类型转换就可以使用。

5. 优先考虑泛型方法

泛型方法也可以从泛型中获益,尤其是静态工具方法, Collections 中的所有算法方法都泛型化了。

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

  • Stack 类为例子
java
public class Stack<E> {

    public void push(E e);
    
    public E pop();

}
  • 假设我们要增加一个方法,让它按顺序将一系列元素全部放到堆栈中去
java
public void pushAll(Iterable<E> src) {
    for (E e : src) {
        push(e);
    }
}
  • 上面的代码有局限性,Stack<Number> 应该支持 pushAll() 类型为 Iterable<Integer>src
java
// 使用 <? extends E> 有限制通配符来实现这种灵活性
public void pushAll(Iterable<? extends E> src) {
    for (E e : src) {
        push(e);
    }
}

类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或另一个进行声明

java
//类型参数方式声明
public static <E> void swap(List<E> list,int i, int j){}

//通配符方式声明
public static void swap(List<?> list,int i, int j){}

对于公共 API 来说,通配符方式声明更简单好用;一般来说,如果类型参数只在方法声明中出现一次,就可以使用通配符取代它。

不过对于通配符方式有一个问题,下面这个简单的实现不能编译

java
public static void swap(List<?> list,int i, int j){
    list.set(i,list.set(j,list.get(i)));
}

对于这个问题可以编写一个泛型辅助方法来解决,但暴露出去的 API 仍然保留了通配符方式,对客户端更加友好。

java
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)));
}

7. 谨慎并用泛型和可变参数

当调用一个可变参数方法时,会创建一个数组来存放可变参数;这个数组应该是一个实现细节,它是可见的。

因此,当可变参数带有泛型时,就产生了泛型数组的问题。

可为什么显示创建泛型数组是非法的,但用泛型可变参数是合法的呢?

因为带有泛型可变参数的方法在实践中作用很大,Java 设计者选择容忍这一矛盾的存在:

java
// Arrays.asList()
@SafeVarargs
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

// Collections.addAll()
@SafeVarargs
public static <T> boolean addAll(Collection<? super T> c, T... elements) {
    boolean result = false;
    for (T element : elements)
        result |= c.add(element);
    return result;
}

Java 7 引入了 @SafeVarags 注解,方法设计者做出承诺,这个泛型可变参数方法是安全的,忽略警告。

不要随意使用 @SafeVarags 注解,除非它是真正安全的,需要满足两大条件:

  1. 它没有在可变参数数组中保存任何值
  2. 它没有对不被信任的代码开放该数组 (或者其克隆程序)

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

泛型最常用于集合,如 Set<E>Map<K,V> ,以及单个元素的容器,如 ThreadLocal<T>AtomicReference<T>

在这些容器中它们都限制了每种容器都只能存放固定类型的元素,但是有时候你可能需要更多的灵活性,比如数据库的行可以存储任意数量的列。

幸运的是,有一种方法可以很容易地做到这一点,将键(key)进行参数化,而不是将容器参数化

案例: Favorites 类,它允许客户端保存和获取一个任意类型的最喜欢的实例。

java
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 类存在运行时安全问题,那就是值是 Object 类型,恶意的客户端可以为 Integer 类型的 key 设置一个 String 类型的值; 就像客户端可以利用 HashSet 原生态类型,将 String 放进一个 HashSet<Integer> 中。

对此,可以付出一点点代价改进,就可以获得运行时的类型安全:

java
public <T> void putFavorite(Class<T> type,T instance) {
    //检验 instance 是否真的是 type 类型的实例    
    favorites.put(Objects.requireNonNull(type),type.cast(instance));
}

注解 API 广泛应用了这种模式,例如 java.lang.reflect.AnnotatedElement 接口

java
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
  • 硬编码获取注解实例
java
elment.getAnnotation(MyAnnotation.class);
  • 动态获取注解实例
java
static Annotation getAnnotation(AnnotatedElement element,
                                    String annotationTypeName) {
    Class<?> annotationType = null;
    try {
        annotationType = Class.forName(annotationTypeName);
    }catch (Exception e) {
        throw new IllegalArgumentException(e);
    }
    
    return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}

方法中 asSubClass(Anotation.class) 的用途

该方法确保加载的类是一个注解类型,如果传入的是 java.lang.String , 就会抛出 ClassCastException ,这一步是安全校验的关键。