Java泛型06 : 通配符:上边界、下边界与*

时间:2022-12-29 19:25:02

超级通道: Java泛型学习系列-绪论

本章主要对Java泛型的通配符进行说明。

1.概念简介

在Java泛型定义时:

  • <T>等大写字母标识泛型类型,用于表示未知类型。
  • <T extends ClassA & InterfaceB …>等标识有界泛型类型,用于表示有边界的未知类型。

在Java泛型实例化时:

  • <?>标识通配符,用于表示实例化时的未知类型。
  • <? extends 父类型>标识上边界通配符,用于表示实例化时可以确定父类型的未知类型。
  • <? super 子类型>标识下边界通配符,用于表示实例化时可以确定子类型的未知类型。

2.<T>与<?>的区别

<T>:泛型标识符,用于泛型定义(类、接口、方法等)时,可以想象成形参。
<?>:通配符,用于泛型实例化时,可以想象成实参。

<T>示例:

/** * <p>Title: <T>与<?>示例</></></p> * @author 韩超 2018/2/23 16:33 */
static class Demo1<T>{
    private T t;
}

<?>示例:

//<?>示例
//我们可以确定具体类型时,直接使用具体类型
Demo1<Integer> integerList = new Demo1<Integer>();
//我们不能确定具体类型时,可以使用通配符
Demo1<? extends Number> numberList = null;
//我们能够确定具体类型时,再实例化成具体类型
numberList = new Demo1<Integer>();
numberList = new Demo1<Double>();

3.通配符详解

向上转型与向下转型
为了更好的理解通配符的,我们首先需要回顾Java向上转型与向下转型的概念。
向上转型:
向上转型:子类型通过类型转换成为父类型(隐式的)。
示例如下:

//子类Integer转换成父类型Object(向上转型)
Object integer = new Integer(1);

向上转型有很多好处,如减少重复代码、使代码变得简洁,提高系统扩展性,这里就不多讲述了。
向下转型:
向下转型:父类型通过类型转换成为子类型(显式的,有风险需谨慎)。

//向下转型(有风险需谨慎)
Integer integer1 = (Integer) new Object();
integer1.intValue();

运行结果:

Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.Integer
    at pers.hanchao.generics.wildcard.BoundedWildcardsDemo.main(BoundedWildcardsDemo.java:126)

因为子类型存在一些父类型没有的方法,所以这种向下转型存在安全隐患。


类层次结构
为了更好的理解通配符,假设以下类层次关系:

//人员 是最高父类
Person
//工人 继承自 人员
Worker extends Person   
//攻城狮 继承自 工人
Programmer extends Worker
//Java攻城狮 继承自 攻城狮
JavaProgrammer extends Programmer
//Java架构师 继承自 Java攻城狮
JavaArchitectProgrammer extends JavaProgrammer

都是很简单的类,类相关代码不再这里多展示,只展示各类的初始化:

Person person = new Person(1,"张三");
Worker worker = new Worker(1,"张三","攻城狮");
Programmer programmer = new Programmer(1,"张三","攻城狮","Java");
JavaProgrammer javaProgrammer = new JavaProgrammer(1,"张三","攻城狮","Java","架构师");
JavaArchitectProgrammer architectProgrammer = new JavaArchitectProgrammer(1,"张三","攻城狮","Java","架构师");

3.1.上边界通配符(<? extends 父类型>)

示例代码:

LOGGER.info("上边界类型通配符(<? extends >)示例");
//测试上边界类型通配符
//定义一个列表,唯一可以确定的是:此列表的元素类型的父类型是Programmer
List<? extends Programmer> programmerUpperList = null;
//do something...
//某种情形下,此列表被赋值成JavaProgrammer的列表
programmerUpperList = new ArrayList<JavaProgrammer>();
//赋值
programmerUpperList = Arrays.asList(
        new Programmer(1,"张三","攻城狮","Java"),
        new JavaProgrammer(1,"张三","攻城狮","Java","架构师"),
        new JavaArchitectProgrammer(1,"张三","攻城狮","Java","架构师")
);
//因为唯一可以确定此列表的元素类型的父类型是Programmer,所以可以将取得的元素值赋值给Programmer对象
programmer = programmerUpperList.get(0);
//Worker和Person是其父类,可以向上转型
worker = programmerUpperList.get(1);
person = programmerUpperList.get(2);
LOGGER.info(programmer.getClass().toString() + " : " + programmer.toString());
LOGGER.info(worker.getClass().toString() + " : " + worker.toString());
LOGGER.info(person.getClass().toString() + " : " + person.toString());
LOGGER.info("是什么类型,取出来就是什么类型");
//虽然此列表被实例化成JavaProgrammer的列表,但是不能确定其中的数据就是JavaProgrammer类型的,所以不能将取得的值赋值给JavaProgrammer对象
// javaProgrammer = programmerUpperList.get(0);
//不能确定之后实例化的类型是Programmer、JavaProgrammer还是JavaArchitectProgrammer,所以不接受add
// programmerUpperList.add(programmer);
////总结:add方法受限,可以进行取值。
LOGGER.info("上边界类型通配符(<? extends>):因为可以确定最大类型,所以可以以最大类型去获取数据。但是不能写入数据。\n");

运行结果:

2018-02-23 16:45:50 INFO  BoundedWildcardsDemo:52 - 上边界类型通配符(<? extends >)示例 2018-02-23 16:45:50 INFO BoundedWildcardsDemo:70 - class pers.hanchao.generics.wildcard.Programmer : Programmer{lang='Java'} 2018-02-23 16:45:50 INFO BoundedWildcardsDemo:71 - class pers.hanchao.generics.wildcard.JavaProgrammer : JavaProgrammer{post='架构师'} 2018-02-23 16:45:50 INFO BoundedWildcardsDemo:72 - class pers.hanchao.generics.wildcard.JavaArchitectProgrammer : JavaArchitectProgrammer{level='架构师'} 2018-02-23 16:45:50 INFO BoundedWildcardsDemo:73 - 是什么类型,取出来就是什么类型 2018-02-23 16:45:50 INFO BoundedWildcardsDemo:79 - 上边界类型通配符(<? extends>):因为可以确定最大类型,所以可以以最大类型去获取数据。但是不能写入数据。

总结:
上边界类型通配符可以确定父类型,回顾向上转型与向下的概念:

在获取数据时 [ 返回类型 get(){ return this.t;} ]:

  • 因为可以确定父类型,所以可以将返回类型设置为父类型。
  • 虽然不能确定返回 this.t的是何种类型,但是this.t的类型肯定是返回类型的子类型,可以通过向上转型成功获取。

在写入数据时 [ void set(参数类型 t){ this.t = t;} ]

  • 因为可以确定父类型,所以可以将参数类型设置为父类型。
  • 虽然不能确定 this.t 字段的具体类型,但是肯定是参数类型的子类型,所以此时set方法进行的是向下转型,存在很大风险。Java泛型为了减低类型转换的安全隐患,不允许这种操作。

上边界类型通配符(<? extends 父类型>):
因为可以确定父类型,所以可以以父类型去获取数据(向上转型)。但是不能写入数据

3.2.无边界通配符(<?>)

通过前面的章节Java泛型04 : 泛型类型擦除中提到的类型擦除,我们可以很容易分析出来下面的结论:

无边界通配符<?> 等同于 上边界通配符<? extends Object>

所以关于无边界通配符(<?>)就很好理解了。

示例代码:

LOGGER.info("无边界类型通配符示例,无边界=上边界Object");
//无边界类型通配符示例
List<?> programmerNoList = null;
//do something ...
//某种情形下,此列表被赋值成Programmer的列表
programmerNoList = new ArrayList<Programmer>();
programmerNoList = Arrays.asList(1,programmer);
//唯一可以确定其最大类是Object ,所以可以根据Object类型取值
Object obj0 = programmerNoList.get(0);
Object obj1 = programmerNoList.get(1);
LOGGER.info(obj0.getClass().toString() + " : " + obj0.toString());
LOGGER.info(obj1.getClass().toString() + " : " + obj1.toString());
LOGGER.info("无边界类型通配符(<?>):等同于(<? extends Object>),因为可以确定最大类型为Object,所以可以以Object类型去获取数据。但是不能写入数据。\n");

运行结果:

2018-02-23 16:45:50 INFO  BoundedWildcardsDemo:99 - 无边界类型通配符示例,无边界=上边界Object
2018-02-23 16:45:50 INFO  BoundedWildcardsDemo:109 - class java.lang.Integer : 1
2018-02-23 16:45:50 INFO  BoundedWildcardsDemo:110 - class pers.hanchao.generics.wildcard.Programmer : Programmer{lang='Java'}
2018-02-23 16:45:50 INFO  BoundedWildcardsDemo:111 - 无边界类型通配符(<?>):等同于(<? extends Object>),因为可以确定最大类型为Object,所以可以以Object类型去获取数据。但是不能写入数据。

总结:

无边界类型通配符(<?>):
无边界类型通配符(<?>) 等同于 上边界通配符<? extends Object>
因为可以确定父类型是Object,所以可以以Object去获取数据(向上转型)。但是不能写入数据

3.3.下边界通配符(<? super 子类型>)

示例代码:

LOGGER.info("下边界类型通配符(<? super >)示例");
//测试下边界类型通配符
//定义一个列表,唯一可以确定的是:此列表的元素类的子类型是Programmer
List<? super Programmer> programmerLowerList = null;
//do something ...
//某种情形下,此列表被赋值成Worker的列表
programmerLowerList = new ArrayList<Worker>();
//因为无法确定对象的实例化类型是Programmer、Worker还是Person,所有不能get
// programmer = programmerLowerList.get(0);
// worker = programmerLowerList.get(0);
//因为唯一可以确定此列表的元素类型的子类是Programmer,所以可以添加Programmer类型的对象及其子类型
programmerLowerList.add(programmer);
programmerLowerList.add(javaProgrammer);
programmerLowerList.add(architectProgrammer);
LOGGER.info(programmerLowerList);
LOGGER.info("存的什么类型,就是什么类型");
LOGGER.info("下边界类型通配符(<? super>):因为可以确定最小类型,所以可以以最小类型去写入数据。但是不能获取数据。\n");

运行结果:

2018-02-23 16:45:50 INFO  BoundedWildcardsDemo:81 - 下边界类型通配符(<? super >)示例
2018-02-23 16:45:50 INFO  BoundedWildcardsDemo:95 - [Programmer{lang='Java'}, JavaProgrammer{post='架构师'}, JavaArchitectProgrammer{level='架构师'}]
2018-02-23 16:45:50 INFO  BoundedWildcardsDemo:96 - 存的什么类型,就是什么类型
2018-02-23 16:45:50 INFO  BoundedWildcardsDemo:97 - 下边界类型通配符(<? super>):因为可以确定最小类型,所以可以以最小类型去写入数据。但是不能获取数据。

下边界类型通配符可以确定子类型,回顾向上转型与向下的概念:

在获取数据时 [ 返回类型 get(){ return this.t;} ]:

  • 因为可以确定子类型,所以可以将返回类型设置为子类型。
  • 虽然不能确定返回的是何种类型,但是返回的对象this.t的类型肯定是返回类型的父类型,此时进行的是向下转型。Java泛型为了减低类型转换的安全隐患,不允许这种操作。

在写入数据时 [ void set(参数类型 t){ this.t = t;} ]

  • 因为可以确定子类型,所以可以将参数类型设置为子类型。
  • 虽然不能确定this.t字段的具体类型,但是肯定是参数类型的父类型,所以此时set方法进行的肯定是向上转型,写入成功。

联系到类型擦除,我们可以继续分析出来下面的结论:

下边界通配符<? super 子类型> 等同于 下边界通配符<? super 子类型> + 上边界通配符<? extends Object>

所有可以进一步进行探索。
示例代码:

LOGGER.info("下边界类型通配符(<? extends >)的继续理解:上边界类型通配符真的无法取值吗?");
//上面已经因为无法确定类型,所以无法通过向上转型取值
// programmer = programmerLowerList.get(0);
//但是可以确定其最大类型必然是Object,是否可以通过Object取值??
Object object =  programmerLowerList.get(0);
LOGGER.info(object.getClass().toString() + ":" + object.toString());
LOGGER.info("下边界类型通配符(<? super >) = (<? super >) + (<? extends Object>)");
LOGGER.info("意义不大");

运行结果:

2018-02-23 16:45:50 INFO  BoundedWildcardsDemo:113 - 下边界类型通配符(<? extends >)的继续理解:上边界类型通配符真的无法取值吗? 2018-02-23 16:45:50 INFO BoundedWildcardsDemo:118 - class pers.hanchao.generics.wildcard.Programmer:Programmer{lang='Java'} 2018-02-23 16:45:50 INFO BoundedWildcardsDemo:119 - 下边界类型通配符(<? super >) = (<? super >) + (<? extends Object>) 2018-02-23 16:45:50 INFO BoundedWildcardsDemo:120 - 意义不大

总结:

下边界类型通配符(<? super 子类型>):
因为可以确定最小类型,所以可以以最小类型去写入数据(向上转型)。
因为可以确定最大类型是Object,所以可以以Object去获取数据(向上转型)。但是这么做意义不大

3.4.总结

  • 上边界类型通配符(<? extends 父类型>):因为可以确定父类型,所以可以以父类型去获取数据(向上转型)。但是不能写入数据。
  • 下边界类型通配符(<? super 子类型>):因为可以确定最小类型,所以可以以最小类型去写入数据(向上转型)。而不能获取数据。
  • 无边界类型通配符(<?>) 等同于 上边界通配符<? extends Object>,所以可以以Object类去获取数据,但意义不大。
  • 下边界类型通配符(<? super 子类型>)下边界通配符<? super 子类型> + 上边界通配符<? extends Object>,所以可以以Object类去获取数据,但意义不大。