Effective Java:04 泛型

Demon.Lee 2024年07月27日 301次浏览

本文代码实践基于 JDK-21:java 21.0.4 2024-07-16 LTS

Java 语言在第 5 个版本才开始引入泛型,此时距离第一个版本已经过去了 8 年。而 Go 语言则是在 Go 1.18 才支持泛型,此时距离 Go 1.0 也已过去了 10 年,为何泛型如此复杂?因为它要解决抽象的问题,用一套算法来满足不同数据或类型的需求。不过这篇文章暂时不讲这些,我们先来看看 Java 中使用泛型时的一些优秀实践。

26 请不要使用原生态类型

代码示例 名称
List<String> 参数化的类型
String 实际类型参数
List<E> 泛型
E 形式类型参数
List<?> 无限制通配符类型
List 原生态类型
<E extends Number> 有限制类型参数
<T extends Comparable<T>> 递归类型限制
List<? extends Number> 有限制通配符类型
static <E> List<E> asList(E[] a) 泛型方法
String.class 类型令牌

为了方便讨论,笔者将书中的术语表摘抄在上面。这一条说的是:不要使用原生态类型,也就说不要使用 List 之类的形式,而要使用 List<E>List<?> 。为什么?因为有安全风险,并且丢失了自描述性。

List list = new ArrayList<String>();
list.add("abc");
list.add(123);
System.out.println(list);
for (Object o : list) {
  String s = (String) o;
  System.out.println(s);
}

上面的代码,我们一看就知道存在问题,但在编译时只会有一个警告,并不会报错:

Unchecked call to 'add(E)' as a member of raw type 'java.util.List' 

只有当我们运行这段程序时,错误才会浮出水面:

[abc, 123]
abc
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String

即使我们使用了 new ArrayList<String>(),但没用,因为虚拟机将添加的元素都当成 Object 类型对待了,即我们所熟知的类型擦除。通过 javap -c -p StringTests.class 反汇编查看字节码(P.S. 可以看到,forEach 被转换成了 iterator ):

Compiled from "StringTests.java"
public class StringTests {
  public StringTests();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class java/util/ArrayList
       3: dup
       4: invokespecial #9                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #10                 // String abc
      11: invokeinterface #12,  2           // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: aload_1
      18: bipush        123
      20: invokestatic  #18                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      23: invokeinterface #12,  2           // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      28: pop
      29: getstatic     #24                 // Field java/lang/System.out:Ljava/io/PrintStream;
      32: aload_1
      33: invokevirtual #30                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      36: aload_1
      37: invokeinterface #36,  1           // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      42: astore_2
      43: aload_2
      44: invokeinterface #40,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
      49: ifeq          76
      52: aload_2
      53: invokeinterface #46,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
      58: astore_3
      59: aload_3
      60: checkcast     #50                 // class java/lang/String
      63: astore        4
      65: getstatic     #24                 // Field java/lang/System.out:Ljava/io/PrintStream;
      68: aload         4
      70: invokevirtual #52                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      73: goto          43
      76: return
}

而只要我们将 List list 改成 List<String> list,编译器就会报错:

StringTests.java:9: Error: Incompatible type: int cannot be converted to String
list.add(123);

为什么要使用类型擦除?这是 Java 为了向后兼容,支持 JDK 5 以前的代码而作出的妥协。这种做法又被称为移植兼容性,即 Migration Compatibility

除了安全风险,另一个自描述性也很重要。因为通过 List<String> 你立刻能明白它想表达什么:一个字符串类型的列表。但如果换成 List 呢?我们不得而知,也因为不可知,从而让它逃过了泛型检查。那就让我们来看看 ListList<?>List<E> 之间的区别:

1)泛型有子类型化的规则,List<E> 是原生态类型 List 的一个子类型,但不是 List<Object> 的子类型。所以,将 List<String> 传给 List 是合法的,但将 List<String> 传给 List<Object> 则不合法;

public static void main(String[] args) {
  List<String> list = new ArrayList<>();
  unsafeAdd(list); // Error: Incompatible type: List<String> cannot be converted to List<Object>
  System.out.println(list);
  for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
    System.out.println(s);
  }
}

private static void unsafeAdd(List<Object> list) {
  list.add("abc");
  list.add(12333);
}

2)如果将上面的 unsafeAdd 方法进行如下调整,那编译错误则会不同:

private static void unsafeAdd(List<?> list) {
  for (Object o : list) {
    System.out.println(o);
  }
  list.add(null);
  list.add("abc"); // Error: Incompatible type: String cannot be converted to CAP#1
  list.add(12333);
}

所以,List<String> 也可以传给无限制通配符类型 List<?>List<?> 不能添加元素(null 除外),一般主要用于读操作,比如需要统计各种类型集合的数据量。可以想到,之所以不允许添加元素,就是为了防止新元素破坏原有集合的元素类型约束条件。不过,不能加,不代表不能删:

public interface List<E> extends SequencedCollection<E> {
  // ...

  boolean add(E e);

  boolean remove(Object o);

  // ...
}

3)由于泛型擦除,所以在对象实例上使用 instanceof 时,需要用 ListList<?>

private static void unsafeAdd(Object obj) {
  if (!(obj instanceof List<String>)) { // Error: Object cannot be safely converted to List<String>
    return;
  }
  List<?> list = (List<?>) obj;
  //...
}

这算是泛型使用的一个例外,与此对应的还有它们 class 对象的表示,List.class 是合法的,但 List<String>.classList<?>.class 都不合法。

List<Obiect> 是个参数化类型,表示可以包含任何对象类型的一个集合;

List<?> 则是一个通配符类型,表示只能包含某种未知对象类型的一个集合;

Set 是一个原生态类型,它脱离了泛型系统。

前两种是安全的,最后一种不安全。


27 消除非受检的警告

这一条比较简单,就是针对泛型的使用,如果出现了编译警告,我们需要尽力消除它们:

1)Unchecked Cast Warning

List list = new ArrayList<>();
List<String> list2 = (List<String>)list; // Unchecked cast from List to List<String>
System.out.println(list2);

2)Unchecked Method Invocation Warning

void unchecked_warning() {
  List list = new ArrayList<>();
  process(list); // Unchecked method invocation
}

private void process(List<String> list) {
    System.out.println(list);
}

3)Unchecked Parameterized Vararg Type Warning

void unchecked_warning() {
  List<String> list = new ArrayList<>();
  process(list, list); // Unchecked generics array creation for varargs parameter
}

private void process(List<String>... lists ) {
}

4)Unchecked Conversion Warning

void unchecked_warning() {
  List list = new ArrayList<>();
  List<String> list1 = list; // Unchecked conversion
  list1.add("1");
  System.out.println(list1.getFirst());
}

从上面的例子看,Unchecked Cast WarningUnchecked Conversion Warning 两种类型有些雷同,其实它们生成的字节码没啥差异,只是使用场景不同,前一种主要用于类型转换,后一种则是赋值。针对这些警告,我们需要做的便是消除它们,如果确实消除不掉,并且代码安全,那就要使用 @SuppressWarnings("unchecked") 注解。需要说明的是,@SuppressWarnings("unchecked") 使用的范围要尽量小,能用在一行代码上,就不要用在方法上,能用在方法上,就不要用在类上,然后再加上一条注释进行补充说明,比如:

// This cast is corrent because ...
@SuppressWarnings("unchecked")
List<String> list1 = (List<String>)list;

28 列表优于数组

列表优于数组,顾名思义,尽量用列表替代数组。为什么?还是因为代码安全风险。数组是协变(covariant)的,又是具体化(reified)的:

  • 协变:如果 SubMaster 的子类,那么 Sub[] 也是 Master[] 的子类型;

  • 具体:数组在运行时知道并强化其实际类型。

Number[] nums = new Integer[10];
nums[0] = 1000L; // 编译成功,但运行时报错:java.lang.ArrayStoreException: java.lang.Long...

看上面的示例,nums 声明的是 Number[] ,而实际上则是 Integer[] ,这是协变。当我们向数组中存放一个 Long 型数据后,编译代码并不会出错,但在运行时则会抛出 java.lang.ArrayStoreException ,这就是具体化的表现。

而泛型则不同,它往往是在编译期强调其类型,而在运行时丢弃其实际类型,并且 List<Sub> 也不是 List<Master> 的子类型。正因如此,泛型和数组不能很好地混合使用,我们也无法创建泛型相关的数组:

List<Number> nums = new ArrayList<Integer>(); // error: incompatible types: ArrayList<Integer> cannot be converted to List<Number>
List<String>[] lists = new List<String>[2]; // error: generic array creation
List<?>[] lists = new List<?>[2]; // no error

不过,创建无限制通配类型的数组却是合法的,比如上面的 List<?>[] lists = new List<?>[2] (前文提到,List<?> 不能添加元素,所以它是安全的)。正因为泛型和数组不能很好的合作,所以我们要优先使用集合类型 List<E>,而不是数组 E[]。这也许会损失一些性能(毕竟数组是固定大小,不需要扩容或存储其他信息),但换来的是更高的类型安全性和灵活的互用性,这是值得的。


29 优先考虑泛型

泛型是为了屏蔽掉不同的数据类型,让开发者将重心放在算法上,即让一个算法可以支持不同的数据结构。如果是通用类的算法,更要优先使用泛型,这一条想要表达的就是这个。

public class Stack {

    private Object[] elements;

    // ...
}

将上面代码示例调整为泛型:

public class Stack<E> {

    private E[] elements;

    // ...
}

通过这样的改造,原来在客户端各处对 Object 进行强制类型转换的代码就都消失了,何乐而不为?


30 优先考虑泛型方法

上一条说的是将类处理为泛型,但如果一个方法可以使用泛型,那也应该优先考虑。最常见的就是各种静态工具方法,比如:

public class Collections {
  // Suppresses default constructor, ensuring non-instantiability.
  private Collections() {
  }

  // ...

  public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
      return Collections.indexedBinarySearch(list, key);
    else
      return Collections.iteratorBinarySearch(list, key);
  }

  // ...

  @SuppressWarnings("unchecked")
  public static final <T> Set<T> emptySet() {
    return (Set<T>) EMPTY_SET;
  }

  // ...
}

Collections#emptySet() 这类方法,我们经常会使用,这正是泛型方法的强大之处,不管具体类型是什么,都能很好的协作。所以,各类方法,特别是方法内部实现是无状态的,都要优先考虑泛型。此时,我们可以将这个方法当作是一个通用的算法实现。


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

关于这一条有一个规则需要理解,即 PECS

producer-extends, consumer-super。另外,所有的 comparable 和 comparator 都是消费者。

下面我们就来理解一下上面提到的灵活性以及 PECS ,先上代码:

import java.util.List;
import java.util.ArrayList;

public class MyStack<E> {
    private final List<E> numbers = new ArrayList<>();

    void addAll(List<E> list) {
        for (E e : list) {
            numbers.add(e); // error: incompatible types: E cannot be converted to Number
        }
    }

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        MyStack<Number> stack = new MyStack<>();
        stack.addAll(list); // error: incompatible types: List<Integer> cannot be converted to List<Number>
        System.out.println(stack);
    }
}

上述代码在编译时会有两处错误,笔者已将错误信息注释在对应位置。之所以有这类问题,是因为泛型与数组不同,它不是协变的。而解决报错也很简单,调整 addAll 方法声明即可:

void addAll(List<? extends E> list) {
  for (E e : list) {
    numbers.add(e);
  }
}

只是简单调整一下泛型的参数,将其变成有限制类型参数 ,便有了如此的神奇的效果,而 ? extends E 想表达的含义也很好理解:E 的某个子类型的列表。与之相对应,我们可以添加一个 removeAll 方法,并将移除的对象添加到指定的集合中:

import java.util.List;
import java.util.ArrayList;

public class MyStack<E> {
  private final List<E> numbers = new ArrayList<>();

  void addAll(List<? extends E> list) {
    for (E e : list) {
      numbers.add(e);
    }
  }

  void removeAll(List<E> list) {
    for (E e : numbers) {
      list.add(e);
    }
    numbers.clear();
  }

  public static void main(String[] args) {
    List<Object> list = new ArrayList<>();
    MyStack<Number> stack = new MyStack<>();
    stack.removeAll(list); // error: incompatible types: List<Object> cannot be converted to List<Number>
    System.out.println(stack);
  }
}

编译报错,解决方案也很简单,调整 removeAll 方法的泛型声明:

void removeAll(List<? super E> list) {
  for (E e : numbers) {
    list.add(e);
  }
  numbers.clear();
}

上面是 ? extends E ,此处是 ? super E,没错,这就是 PECS :在表示生产者或者消费者的输入参数上使用通配符类型,如果参数化类型表示一个生产者 T,就使用 <? extends T>;如果它表示个消费者 T ,就使用 <? super T> 。如果同时是消费者和生产者呢?那就使用严格的类型匹配,而不是通配符。

java.lang.Comparablejava.util.Comparator 天生就是消费者,所以我们应优先使用 Comparable<? super T>Comparator<? super T> ,比如调用 java.util.Collections.sort() 方法:

// Collections#sort
public static <T extends Comparable<? super T>> void sort(List<T> list) {
  list.sort(null);
}

// ArrayList#sort
@Override
@SuppressWarnings("unchecked")
public void sort(Comparator<? super E> c) {
  final int expectedModCount = modCount;
  Arrays.sort((E[]) elementData, 0, size, c);
  if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
  modCount++;
}

// Arrays#sort
public static <T> void sort(T[] a, int fromIndex, int toIndex,
                            Comparator<? super T> c) {
  if (c == null) {
    sort(a, fromIndex, toIndex);
  } else {
    rangeCheck(a.length, fromIndex, toIndex);
    if (LegacyMergeSort.userRequested)
      legacyMergeSort(a, fromIndex, toIndex, c);
    else
      TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);
  }
}

public static void sort(Object[] a, int fromIndex, int toIndex) {
  rangeCheck(a.length, fromIndex, toIndex);
  if (LegacyMergeSort.userRequested)
    legacyMergeSort(a, fromIndex, toIndex);
  else
    ComparableTimSort.sort(a, fromIndex, toIndex, null, 0, 0);
}

// ComparableTimSort#sort
static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
  assert a != null && lo >= 0 && lo <= hi && hi <= a.length;

  int nRemaining  = hi - lo;
  if (nRemaining < 2)
    return;  // Arrays of size 0 and 1 are always sorted

  // If array is small, do a "mini-TimSort" with no merges
  if (nRemaining < MIN_MERGE) {
    int initRunLen = countRunAndMakeAscending(a, lo, hi);
    binarySort(a, lo, hi, lo + initRunLen);
    return;
  }
  // ...
  // ...
}

// ComparableTimSort#binarySort
private static void binarySort(Object[] a, int lo, int hi, int start) {
  assert lo <= start && start <= hi;
  if (start == lo)
    start++;
  for ( ; start < hi; start++) {
    Comparable pivot = (Comparable) a[start];

    // Set left (and right) to the index where a[start] (pivot) belongs
    int left = lo;
    int right = start;
    assert left <= right;
    /*
             * Invariants:
             *   pivot >= all in [lo, left).
             *   pivot <  all in [right, start).
             */
    while (left < right) {
      int mid = (left + right) >>> 1;
      if (pivot.compareTo(a[mid]) < 0)
        right = mid;
      else
        left = mid + 1;
    }
    assert left == right;
    // ...
    // ...
}

最后是关于 List<E>List<?> 的选择问题,下面是给列表中两个位置的元素进行交换的方法声明:

// Two possible declarations for the swap method
public static <E> void swap(List<E> list, int i, int j); 
public static void swap(List<?> list, int i, int j);

在公共的 Api 中,哪一个更好?是 List<?>,因为它简单。如果类型参数只在方法声明中出现一次,就可以用通配符取代它。


32 谨慎并用泛型和可变参数

前面我们演示了显示创建泛型数组是不可行的,代码无法编译:

List<String>[] lists = new List<String>[2]; // error: generic array creation

但有一个例外,那就是可变参数:

class GenericTests {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("abc");
        vartypeTest(list, list); // warning: [unchecked] unchecked generic array creation for varargs parameter of type List<String>[]
    }

    static void vartypeTest(List<String>... lists) { // warning: [unchecked] Possible heap pollution from parameterized vararg type List<String>
        List<String>[] listArr = lists;
        System.out.println(listArr.length);
        for (List<String> list : listArr) {
            System.out.println(list);
        }
    }
}

除了编译时会出现两个警告外,上述代码运行时没有任何问题。那为什么会允许这样一个例外出现呢?因为带有泛型可变参数或参数化类型的方法在实践中用处很大,为了实际利益,Java 设计者们便容忍了这个矛盾的存在。像下面这些方法就是:

// Arrays#asList
@SafeVarargs
@SuppressWarnings("varargs")
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;
}

// List#of (Java 9开始支持)
@SafeVarargs
@SuppressWarnings("varargs")
static <E> List<E> of(E... elements) {
  switch (elements.length) { // implicit null check of elements
    case 0:
      @SuppressWarnings("unchecked")
      var list = (List<E>) ImmutableCollections.EMPTY_LIST;
      return list;
    case 1:
      return new ImmutableCollections.List12<>(elements[0]);
    case 2:
      return new ImmutableCollections.List12<>(elements[0], elements[1]);
    default:
      return ImmutableCollections.listFromArray(elements);
  }
}

而那两个警告,只需要增加 @SafeVarargs 注解便能消除:

@SafeVarargs
static void vartypeTest(List<String>... lists) {
  // ...
}

@SafeVarargs 是 Java 7 引入的,专门用于消除泛型可变参数警告,使用这个注解,也就意味着承诺:这里的类型是安全的。那什么时候是不安全的?我们来调整一下代码示例:

@SafeVarargs
static void vartypeTest(List<String>... lists) {
  Object[] listArr = lists;
  System.out.println(listArr.length);
  List<Integer> nums = List.of(123);
  listArr[0] = nums;
  for (List<String> list : lists) {
    String firstStr = list.get(0); // java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
    System.out.println(firstStr);
  }
}

编译上述代码没有问题,但运行时就会出现 java.lang.ClassCastException ,这就是堆污染(heap pollution),即运行时具体类型被擦除后引发的类型安全问题,再看一个示例:

class GenericTests {

  public static void main(String[] args) {
    String[] result1 = toArray("a", "b", "c"); // anewarray  #7 // class java/lang/String
    System.out.println(result1.length); // 3
    String[] result = pickTwo("aa", "bb", "cc"); // java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String;
    System.out.println(result.length);
  }

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

  @SafeVarargs
  static <T> T[] toArray(T... args) {
    return args;
  }
}

这里的问题在于,从一个泛型函数调用另一个泛型函数时,如果使用了泛型可变参数,此时创建的数组不再是实际类型(这里是 String[] ),而是 Object[] 。这是编译时决定的,所以就出现了类型转换异常。使用 javap 命令可以看到创建的实际类型,笔者已对其进行了注释。那如何确认这类代码是类型安全的呢?有两个条件:

  • 没有在可变参数数组中保存任何值;
  • 没有在不被信任的代码中开放该数组(或其克隆程序)。

如果不想使用 @SafeVarargs 注解(该注解在 Java 8 中只能修饰静态方法和 final 实例方法,在 Java 9 中开始支持私有实例方法),我们也可以使用 List 参数来代替可变参数数组:

class GenericTests {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("abc");
        vartypeTest(List.of(list, list)); // 这里调整为 List
    }

    static void vartypeTest(List<List<String>> lists) { // 这里是 List<List>>
        System.out.println(lists.size());
        for (List<String> list : lists) {
            System.out.println(list);
        }
    }

}

当然,这里的性能肯定要稍微差一点,不过编译器能证明它是安全的,没有类型安全风险。


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

类型安全的异构容器指的是什么?我们先上代码:

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

  public static void main(String[] args) {
    Favorites t3 = new Favorites();
    t3.putFavorite(String.class, "Hello, World!");
    t3.putFavorite(Integer.class, 123);
    System.out.println("String: " + t3.getFavorite(String.class));
    System.out.println("Integer: " + t3.getFavorite(Integer.class));
    System.out.println("Long: " + t3.getFavorite(Long.class));
  }
}

这里的 Favorites 类就是一个类型安全的异构容器:

1)类型安全指的是它的 key 是泛型化的,并以此来保证 value 的值类型与 key 一致;

2)而异构是指它的 key 都是不同的类型,不像 List<T>, Set<E> 中只有一种类型参数。

像这样,将 key(而不是 value) 进行参数化,就给我们带来了业务实现的灵活性。注意代码第 10 行中的 type.cast(favorites.get(type)),这里使用 cast 方法的目的就是确保获得的值一定是 T 类型的实例,否则这行代码将会抛出异常:

public final class Class<T> implements java.io.Serializable,
GenericDeclaration,Type,AnnotatedElement,TypeDescriptor.OfField<Class<?>>,Constable {
  // ...

  @IntrinsicCandidate
  public native boolean isInstance(Object obj);

  //...

  @SuppressWarnings("unchecked")
  @IntrinsicCandidate
  public T cast(Object obj) {
    if (obj != null && !isInstance(obj))
      throw new ClassCastException(cannotCastMsg(obj));
    return (T) obj;
  }  

  // ...
  //...
}

不过,上面的 Favorites 仍然有类型安全风险,因为客户端可能会使用如下的形式:

public static void main(String[] args) {
  Favorites t3 = new Favorites();
  Class clazz = Long.class;
  t3.putFavorite(clazz, "abc");
  System.out.println("Long: " + t3.getFavorite(Long.class));
}

通过使用 Class 对象的原生类型,就能将一个字符串类型存储到 Long 类型的 key 中,当然,编译期会有警告,但不会报错:

$ javac -J-Duser.language=en -J-Duser.country=US -Xlint:unchecked Favorites.java
Favorites.java:22: warning: [unchecked] unchecked method invocation: method putFavorite in class Favorites is applied to given types
    t3.putFavorite(clazz, "abc");
                  ^
  required: Class<T>,T
  found:    Class,String
  where T is a type-variable:
    T extends Object declared in method <T>putFavorite(Class<T>,T)
Favorites.java:22: warning: [unchecked] unchecked conversion
    t3.putFavorite(clazz, "abc");
                   ^
  required: Class<T>
  found:    Class
  where T is a type-variable:
    T extends Object declared in method <T>putFavorite(Class<T>,T)
2 warnings

如果想发现问题,只能等到运行期调用 getFavorite 方法才能发现,如果想早一点发觉呢?也是可以的:

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

同样是使用 Class#cast 方法,只不过我们将其提前到了 putFavorite 方法,这样 Favorites 中存储的就都是类型安全的数据了。

最后是一个小技巧,当需要将一个 Class<?> 传给有限制通配符类型的 Class<? extends Annotation> 时,怎么做才不会弹出警告?答案是使用 Class#asSubclass 方法,比如:

Class<?> clazz = ...;
Class<? extends Annotation> subClazz = clazz.asSubclass(Annotation.class);

同样的,如果 clazz 不是 Annotation 的子类,该转换将会抛出异常:

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

好,本期内容就总结到这里。下图截取自耗子叔的《左耳听风》专栏,他认为泛型的本质是:屏蔽掉数据和操作数据的细节,让算法更为通用,让编程者更多地关注算法的结构,而不是在算法中处理不同的数据类型,供参考。