JAVA-DIY-6-WEEK

Persistence is not necessarily successful, but not sure will not succeed.

Posted by Hyuga on March 28, 2019

第6周讨论主题:泛型

Java如果没有泛型会有什么灾难

JDK1.5之前就没有泛型,现在去看基于JDK1.5之前开发的代码,依旧能看到大量重复代码,还有无处不在的强制转换语句。

代码不雅观是一回事,大量的强制转换语句,都是一颗会不定时爆炸的闷雷。

例如下面这段代码!

List list = new ArrayList();
list.add(1);
list.add("2");
list.add(3L);
list.add(4D);

编译执行都没问题,什么都可以往list丢,list也不知道丢进来的元素究竟是什么类型,只能全都当做Object存了起来。

所以取元素的时候,拿到的类型都是Object,如果开发者要转换为指定类型,就只能进行强转,而强转,就存在风险。

因为list里面也不知道究竟放入了什么类型的元素,放入无任何限制,那么取出强转就必然会存在风险。

如下代码段!

Object o = list.get(0);
System.out.println("1====>" + o);
Integer o1 = (Integer) list.get(0);
System.out.println("2====>" + o1);
Character o2 = (Character) list.get(0);
System.out.println("3====>" + o2);//执行报错,类型转换异常

取出来都是Object,当我知道list里面只有Integer型数据的时候,我可以进行(Integer)强转。

但是因为List存储无任何限制,所以我没办法确定list里面究竟都存了些什么元素,假如存了个对象?Double?枚举?那么强转必然报(ClassCastException)异常。

而采用了泛型后,有何好处呢?

还是拿上面的例子来改造演示!

List<Integer> list = new ArrayList<>();
list.add(1);
list.add("2");//编译不通过
list.add(3L);//编译不通过
list.add(4D);//编译不通过

Integer integer = list.get(0);

我明确了List所能存储的类型,除了Integer的其他类型数据都不能放入List中。不存在上面所说的风险了,因为list只能存储Integer类型数据。所以取出元素也不需要强转了。

泛型的出现,无论是对于业务代码,还是工具组件等等,都有很大的帮助,规范了操作,也提升了代码的复用性和阅读体验。

List<? extends T>和List<? super T>有哪些区别

为了便于阅读理解,还是建议先从super开始讲解!!!

  • 下限(下边界):List<? super T>
    • 特性1:可用于参数类型限定,不能用于返回类型限定。
    • 特性2:可写不可读【不能取(get),只能添加(add)】!!!
      • 可写:参数类型限定,能放入T或T的子类
      • 不可读:返回类型无限定,返回类型可能是T或T的超类(无法明确,只能是Object,无法读取元素具体类型的内部属性)。
    • 场景:投票环节,类型场景自行脑补!

Demo:

List<? super Fruit> list = new ArrayList<>();	
list.add(new Banana());
list.add(new Apple());
list.add(new Fruit());

Object object2 = list.get(0);
System.out.println(object2);
  • 添加元素的时候,只能添加类型为Fruit或Fruit的子类,限定了放入类型。

  • 取出元素的时候,因为返回类型可能是Fruit或Fruit的超类,所以取出类型默认是Object,无法明确知道取出的元素是哪种类型。


  • 上限(上边界):List<? extends T>
    • 特性1:可用于的返回类型限定,不能用于参数类型限定。
    • 特性2:可读不可写【只能取(get),不能添加(add)】!!!
      • 可读:返回类型参数限定,返回的元素只能是T或T的子类,所以默认返回类型是T。
      • 不可写:参数类型无限定,不明确能放入什么, 参数类型是T或T的子类,返回类型是T或T的子类(无法明确,只能是T)。
    • 场景:抽奖取数环节,类型场景自行脑补!

Demo:

List<? extends Fruit> list = new ArrayList<>();
list.add(new Banana());//编译不通过
list.add(new Apple());//编译不通过

List<? extends Fruit> list1 = ListUtil.NEW(new Banana(), new Apple());
Fruit fruit1 = list1.get(0);//取出元素都是Fruit类型
Fruit fruit = list1.get(1);
  • 添加元素的时候,因为extends是限定返回类型,而不是限定入参类型,所以无法确定入参元素的类型,因而不支持入参。

  • 取出元素的时候,因为extends限定了返回类型,返回类型是T或T的子类,所以默认返回类型是T。因为是父子类,所以可以获取元素的内部属性。

注意: 不是说了上限这种模式只能读,不能写么?为什么ListUtil.NEW()还是能赋值?

实际代码如下:
    List<E> list1 = new ArrayList<>(2);
    Collections.addAll(list1, new Banana(),new Apple(),new Fruit());
    

总结:

  • 下限(下边界):List<? super T> 只能用于参数类型限定。内部元素类型是T或T的子类。取出类型是Object。

  • 上限(上边界):List<? extends T> 只能用于返回类型限定。内部元素类型是T或T的子类。取出类型是T。

    至于上述两者的可读不可写,可写不可读,其实我没搞得很懂。

    因为 List<? extends T> 可读不可写,但其实也是有办法写入的。

    如 List<? super T> 可写不可读,但其实也是可以读取的,只是读取出来是Object类型,如果要指定类型需要强转。

    所以这块搞得我有点迷糊。

其实,我更偏向于理解成这是一种编程规范。

super就是用于参数类型限定,明确告诉你能放入什么类型的元素,但是取出来的元素我只知道是T或T的超类,不确定所以只能是Object。

extends就是用于返回类型限定,没有限定放入元素类型,不确定能放入什么元素,不建议存储元素,取出来的元素明确告诉你是T或T的子类,不确定所以只能是T。

类名<? super T>存在哪些实际应用场景

我参考下List.sort()吧,这个平时也比较常用。

default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
	for (Object e : a) {
	    i.next();
	    i.set((E) e);
	}
} 
Arrays源码:
public static <T> void sort(T[] a, Comparator<? super T> c) {
    if (c == null) {
        sort(a);
    } else {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a, c);
        else
            TimSort.sort(a, 0, a.length, c, null, 0, 0);
    }
}

入参采用了Comparator<? super T> c,限定了入参类型,而且因为super特性,可写不可读。

参数c我传了什么就是什么,sort函数内,它并不知道我传的是什么类型,取出来也是Object对象,get不到c的具体属性。保证了入参的安全。

如下代码编译不通过。super限定了入参类型,未限定返回类型,所以无法保证获取的元素类型,无法获取对象的具体属性。

List<? super Fruit> list = new ArrayList<>();
list.sort((o1, o2) -> o1.xxx【无法获取具体属性】);