Java-泛型详解

476 阅读4分钟

语法糖

在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。

为什么需要泛型

List list = new ArrayList();
list.add(100);
list.add("100");

// 第一个元素就是int类型,OK
System.out.println((int)list.get(0) + 1);
// 第二个元素实际为String,因此会引发ClassCastException
System.out.println((int)list.get(1) + 1);

泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。java的泛型是一种语法糖,其采用的方式是类型擦除,所以java泛型是一种伪泛型,这么做也是为了兼容旧版本。

类型通配符

存在问题

public static Map<Number, Long> count(List<Number> list) {
    return list.stream()
            .filter(n -> n.intValue() < 100)
            .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}

List<Integer> numsA = Arrays.asList(1, 2, 3, 100, 200, 300);
// 错误
Map<Number, Long> countA = count(numsA);
       
List<Double> numsB = Arrays.asList(1D, 2D, 3.55D, 100D, 200D, 330D);
// 错误
Map<Number, Long> countB = count(numsB);

上面代码会报错,List,List不是List的子类型。把方法参数改成count(List list)也不行,它们也不是List的子类型,就算运行时传进去的都是Object的List。因为如果这样的话,传人一个子类的List,但是试图把它的元素转成另一个子类时就会有问题。 这种编译时检查虽然增加的程序的安全性,但降低了编码的灵活性,如果有多种类型需要统计,我们不得不为每一种类型编写一份count方法,还有就是count方法不能重载,在一个类中可能写出countInt,countDouble...这样的代码。

无界

< ? extends E>通配符

// list的元素可以是任意类型
public static Map<Number, Long> count(List<?> list) {
    return list.stream()
        .map(n -> (Number)n)
        .filter(n -> n.intValue() < 100)
        .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}

为了解决上述问题,我们可以使用通配符:

就是通配符,代表任意类型。这样就可以接收任何类型的List了,大大提高了灵活性,代码也很简洁,但安全性缺又降低了,试想有人传了一个List s = Arrays.asList("1", "2", "3", "4", "5");进去会发生什么?

通配符上界< ? extends E>

  • 如果传入的类型不是 E 或者 E 的子类,编译不成功
  • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用

通配符下界 < ? super E>

  • 在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。直至 Object

逆变与协变

  • 逆变: 当某个类型A可以由其基类B替换,则A是支持逆变的
  • 协变: 当某个类型A可以由其子类B替换,则A是支持协变的。

PECS

应该在什么时候用通配符上界,什么时候用通配符下界呢?《Effective Java》提出了PECS(producer-extends, consumer-super),即一个对象产生泛型数据时用extends,一个对象接收(消费)泛型数据时,用super。

/** 
 * Collections #copy方法
 * src产生了copy需要的泛型数据,用extens
 * dest消费了copy产生的泛型数据,用super
 */
public static <T> void copy(List<? super T> dest, List<? extends T> src)

? 和 T 的区别

T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。

  1. 区别1:通过 T 来 确保 泛型参数的一致性
// 通过 T 来 确保 泛型参数的一致性
public <T extends Number> void
test(List<T> dest, List<T> src)

//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
public void
test(List<? extends Number> dest, List<? extends Number> src)
  1. 类型参数可以多重限定而通配符不行
  2. 通配符可以使用超类限定而类型参数不行 类型参数 T 只具有 一种 类型限定方式: T extends A 但是通配符 ? 可以进行 两种限定: ? extends A ? super A

类型擦除(type erasure)

当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这在就叫做 类型擦除(type erasure)。

ArrayList<Integer> listA = new ArrayList<>();
ArrayList<String> listB = new ArrayList<>();

// listA和listB运行时的类型都是java.util.ArrayList.class, 返回true
System.out.println(listA.getClass() == listB.getClass());

由于类型擦除的原因,不能在静态变量,静态方法,静态初始化块中使用泛型,也不能使用obj instanceof java.util.ArrayList判断泛型类,接口中定义的泛型。

参考

聊一聊-JAVA 泛型中的通配符 T,E,K,V,?

细说JAVA泛型