语法糖
在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。
为什么需要泛型
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:通过 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)
- 类型参数可以多重限定而通配符不行
- 通配符可以使用超类限定而类型参数不行 类型参数 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判断泛型类,接口中定义的泛型。