通配符
- ? 无界通配符
- ? extends T 上界通配符
- ? super T 下届通配符
通配符主要用于的变量声明及形参列表,作用于对同类型不同泛型类型的对象无法使用统一的泛型类型进行定义或接受。
从侧面来说给通配符增加解析,自然也具有了泛型类型转换的能力。上面的话有点绕,多说无益,看下面的例子就一目了然了。
通配符在普通局部方法中基本没有使用的意义,他的作用一般用于形参列表。
下面对通配符的应用场景进行举例:
//如下方法,我定义了多个ArrayList集合,这多个ArrayList都声明了泛型,什么类型都有。
public void testMethod(){
List<Map> lm = new ArrayList<>(){{
add(new HashMap(){{put("name","赵六");}});
}};
List<String> ls = new ArrayList<>(){{
add("张三");
}};
List<Integer> ln = new ArrayList<>(){{
add(1);
add(2);
}};
//这时我需要调用一个方法对以上集合进行处理,进行遍历操作
List<?> l= processList(lm);
List<?> l= processList(ln);
//上界通配符时看此方法
processListExtends(ls);
//下界通配符看此方法
processList(ln);
}
/**
*对于方法本身来说,传入的List集合什么类型都有,有Map、String、Integer虽然Object是他们的父类,但我也不能将泛型类型一概而论的
*定义为Object类型(List<Obejct> list)。因为泛型本身就是个语法糖,没有继承的概念,给我什么类型我就是什么类型不给我类型我就
*编译为Object类型,所以就只能使用?来代替,这个?就是通配符又称无界通配符。因为没有限制只要是List集合都能传入,所以叫无界操作
*符,如果和extends和super一起使用规定是某个类的子类或父类那叫有界操作符。
*/
public List<?> processList(List<?> list){//如果定义为List<Object> list,上面testMethod方法对该方法调用时,直接受检异常
if (null!=list){
//如下对传入的List集合进行了遍历,遍历的时候就得需要Object类型进行接收遍历
//这时候问题来了,刚才不是说不能用Object类型的泛型来接收吗这时候怎么用上Object了
//因为泛型只能声明为引用类型,不可声明为基本数据类型,那么里面的值也一定是引用类型,
//只要是引用类型那么它的祖类就一定是Object类型,那么自然只能使用Object进行接收处理
//或者不想用Object进行接收,那么可以给通配符加一个界限,详细可以看下面的上界通配符和下界通配符
for (Object obj:list){
System.out.println(obj);
//既然是Object自然就可以进行强转,但并不推荐这样做。
//下面判断了如果obj是Map类型或String就进行打印,这样看确实没问题,写法也确实没问题
//但问题出在通用性上,我调用了一个方法,然后这个方法只要是List类型都能传入,
//结果处理的时候却只能处理Map和String类型,这种踩屎感相信开发人员都有感受。
//因为这写法反人类有违常识。开发人员普遍认为既然能传入就应该能处理,不能处理你让我传它干啥,是不是“司马”了,擦
//-------虽说程序的本质是模拟世界,但是上面这行话可不是指有关事业单位或面向大众的服务部门,请不要过分解读-------
//那么针对于这种问题,我们就可以对形参进行处理,我们给通配符加一个界限,详细可以看下面的上界通配符和下界通配符
if (obj instanceof Map){
System.out.println((Map)obj);
}else if(obj instanceof String){
System.out.println(obj);
}
}
//看完上面的介绍,通配符的弊端就体现出来了,因为不知道具体类型,所以不能新增。看到这可能会说泛型只能为引用类型
//那么传入Obejct不就行了,然而非也,你可以说String和Integer都是Object类型,但不能说Object是String或Integer类型
//注意这里说的是类型,泛型不体现继承关系。
//如果要新增就只能增加为null,因为String不可以赋值为Objct但可以赋值为null,Integer同理,所有引用类型都同理。
//所有null在这里相比于Object更通用。这样就是通配符的弊端只能取不能存,要存就存null,但存null好像又没啥意义只能占个位
list.add(null);
}else {
//list可以定义为一个新的对象,这个对象可以声明为任意的泛型类型,不知道类型可以不写默认为Object类型,
//但不能这样声明list = new ArrayList<?>();想想上面说的应该就理解不能声明为?,
//通配符的作用不是用来定义对象的,而是接受对象的。试问你怎么定义一个不知道类型的对象,不知道类型可以不写默认为Object
//但Object!=? 这俩不等价。
list = new ArrayList<Integer>();
}
return list;
}
/**
* 如下写法通配符和extends搭配使用的方式称之为上界通配符,顾明思议给通配符增加了一个界限,
* 只能传给我一个CharSequence类型或它的子类String类型的集合,传入其他类型时报受检异常,
* 因为最大或最上也只会是CharSequenc类型,所以又称为上界通配符
*
* 这里需要注意一点,就是更改了方法名。没法重载!!形参的参数类型并不受泛型影响。换句话说泛型只是定义我对象内部的数据类型,不会改 * 改对象的元素类型
*/
public void processListExtends(List<? extends CharSequence> list){
if (null!=list && !list.isEmpty()){
//因为增加了界限,最大或最上就是个CharSequence类型,所以可以用CharSequence类型来进行接收,
//当然拿Object来接收也没问题,当然也仅限于最上CharSequence和祖类Object,它的子类自然是不可以
//String和StringBuffer都是CharSequence的子类,但不能说String就是StringBuffer,但可以说这俩都是CharSequence
//这样增加界限的好处自然而然就显现出来了,我对数据进行处理时就能使用CharSequence中的方法来进行处理,
//如果使用Object进行接收,那么就只能使用Object里面的方法了,这自然是不推荐使用Object进行接收的。
for (CharSequence c:list){
System.out.println(c);
}
//无法再添加元素,换句话说Object都不行凭啥你行
list.add(null);
}
}
/**
* 如下写法通配符和super搭配使用的方式称呼为下界通配符,和上界通配符正好相反,下界通配符规定了只能是Integer的父类或者是
* Integer,传入其他类型报受检异常。
*
* 方法名问题在上界通配符时已经说过,在此不再赘述
*/
public void processListSuper(List<? super Integer> list){
if (null!=list && !list.isEmpty()){
//这里又回到了无界通配符的时候,只能通过Object来进行接收。这块稍微难理解一点,因为无界操作符是因为没有界限所以只能通过 //Object来接,这里已经声明界限了,为什么还是只能用Object来接?
//其实还是可以进行强转的,例如for (Integer c:(List<Integer>)list),但还是那句话不推荐,这样做写的时候可能不报错但 //运行时可能报错,也就是存在强转的风险。例如Integer的父类是Number,但Number有很多子类,比如Long,Double等,如果我将 //一个List<Double>类型的集合转换为List<Number>然后再把它传给了当前的方法,那么就可能存在强转的风险,因为Double和 //Integer就不是一个类型,这里使用的是数值类型进行的举例可能不会报异常,但如果是自己定义的对象呢。所以是存在风险的,与其 //在运行时报异常,还不如写的时候就给我报异常呢,对吧。
//明白了转换风险之后,继续分析,既然直接转换成Integer不行,转换成他的直接父类Number总成了吧,其实还是不可以的,强转风 //险依旧存在,因为实际开发中一个类可能有多层父类,直接父类上面还有好几层的父类,那么强转的风险就移到了父类,父类也迷茫了
//不会又是哪个孙子扮我爹来忽悠我来了吧,我也不敢吱声啊,算了我先认了吧毕竟长得像,但实际一做亲子鉴定,擦果然是孙子扮的
//为了这种长得像,误当爹的情况发生,我劝你只认Object
for (Object c:list){
System.out.println(c);
}
}
//下界通配符相比于无界和上界有一个优势,可以新增数据了,但也仅限于Integer类型,list.add(new Number())是不可以的
//其实不难理解,Integer可以转换为Number类型向上转型无需强转。但如果new Number();强转为Integer类型肯定会类型转换异常的
//虽然new Number()这样的写法有问题,实际该使用一下匿名内部类,但这里为了举例就先不纠结语法,能明白意思就好。
list.add(112);
}
/**
* 如果三个通配符,已经搞懂就需要实战一把,请看下面的方法随机找的一个,此方法是Collectors类中的方法平面映射,和stream搭配使用。
* 不得不提stream把泛型用到了极致,如果能把Collectors类全部看明白,那么泛型就学到了极致。
*
* 下面对此方法进行分析
* 1.首先此方法为一个静态泛型方法,定义了会用到的泛型类型<T, U, A, R>, 返回值为Collector类型,并将泛型类型进行了传递
* 2.参列表传入了两个参数,类型分别是Function类型和Collector类型。并且对这两个类型进行了限制,限制如下
* Function类定义了两个泛型类型,第一个泛型类型要求是泛型方法T类型或T类型的父类下界通配符。第二个就难理解点要求必须是Stream * 类或它的子类既上界通配符,同时要求这个stream的泛型类型必须为泛型方法的U类型或它的子类。
* Collector类定义了三个泛型类型,第一个参数限制要求必须为U类型或它的父类,其他两个为泛型方法的A和R
*
* 捋到这里其实已经没有意义在往下捋了,看这个方法的意义是看一下通配符的其他用法,而不是搞明白这个方法是干什么的,
* 如果想搞清楚这个方法具体怎么允许的,就需要debug了干看是非常蒙的,即使debug也很蒙。函数式编程的弊端在这里暴露无遗,各种函数式 * 接口和泛型导致可读性非常差,就像下面解析的这个方法,谁看谁蒙。到底实现怎样的效果可能只有写这个方法的人能搞懂,其他人像看明白具
* 做什么就非常困难。其实对于stream会用就好,不用深究他的具体实现没意义,stream的意义是易用好用,但易用好用的东西底层往往是非常 * 复杂的
*/
public static <T, U, A, R>
Collector<T, ?, R> flatMapping(Function<? super T, ? extends Stream<? extends U>> mapper,
Collector<? super U, A, R> downstream) {
BiConsumer<A, ? super U> downstreamAccumulator = downstream.accumulator();
return new CollectorImpl<>(downstream.supplier(),
(r, t) -> {
try (Stream<? extends U> result = mapper.apply(t)) {
if (result != null)
result.sequential().forEach(u -> downstreamAccumulator.accept(r, u));
}
},
downstream.combiner(), downstream.finisher(),
downstream.characteristics());
}