一、在说泛型通配符" ?" 之前先讲几个概念

1、里氏替换原则(Liskov Substitution Principle, LSP):

定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。

LSP包含以下四层含义:

子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。

子类中可以增加自己的方法。

当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。

当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

因为继承带来的侵入性,增加了耦合性,也降低了代码的灵活性。父类修改了代码,子类可能也会受到影响,为了减少这些影响,我们就需要里氏替换原则,虽然不遵循里氏替换原则,程序照样可以运行,但是出错的几率会大大增加。

任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,即基类随便怎么改动子类都不受此影响,那么基类才能真正被复用。

2、逆变、协变和不可变:

定义:逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);

当A≤B时,如果有f(A)≤f(B)成立,那么f(⋅)是协变(covariant)的;

当A≤B时,如果有f(B)≤f(A)成立,那么f(⋅)是逆变(contravariant)的;

当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系,那么f(⋅)是不变(invariant)的;

3、数组的协变与逆变:

1、基本类型数组不允许协变和逆变,无法通过编译。

2、引用类型数组允许协变和逆变,逆变时会检查实际类型,如果不符抛出java.lang.ClassCastException。

下面看代码:

//继承关系如下

class Fruit {}

class Orange extends Fruit {}

class Apple extends Fruit {}

class RedApple extends Apple {}

//数组的协变:Apple是Fruit的子类,Apple数组可以赋值给Fruit数组

Fruit[] fruits = new Apple[10];

fruits[0] = new Apple();

fruits[1] = new RedApple();

//下面两个会编译可以通过,但是在运行时会抛出java.lang.ArrayStoreException

//因为虽然定义了Fruit[],但实际上指向的是Apple[]

fruits[2] = new Fruit();

fruits[3] = new Orange();

//数组的逆变:编译时可以通过,但是运行时会抛出java.lang.ClassCastException

Apple[] apples = (Apple[]) new Fruit[10];

java中数组的协变是个坑,需要注意一下,因为在编译期它允许放入Fruit和Orange等非法类型,但是在运行时还是会出现类型错误。

4、先来说一下泛型的不可变:

List fruitList = new ArrayList();//报错

上面的代码编译期会直接报错,类比数组,泛型将这种类型检查移到了编译期,所以说泛型是不可变的,但有时我们需要泛型可以协变和逆变,那么该怎么办呢?

解决的办法就是下面要讲的<? extends T>(协变)和<? super T>(逆变),也就是平常说的泛型的上界和下界。

5、泛型的 PECS (Producer Extends Consumer Super)原则:

生产者(Producer)使用extends,消费者(Consumer)使用super。

经常往外读取内容的,适用于上界Extends

经常往里插入的,适用于下界Super

二、泛型<? extends T>上界与<? super T> 下界

表示参数化类型的上界,表示参数化类型可能是T 或是T 的子类;
表示参数化类型下界(Java Core中叫超类型限定),表示参数化类型是T或T 的超类型(父类型),直至Object;
先来讲一下参数化类型和通配符的区别:

(1)参数化类型T,它指代的是同一个类型,比如下面的三个T都指代用一个类型,要么是String,要么是其他的,但这三个T都必须是同一个类型。

public List fill(T... t );

  (2)而通配符没有这种约束,List单纯的表示,集合里放了一个东西,是什么我不知道。

1、<? extends T>上界:

List<? extends T> list = new ArrayList<可以是T或是T的子类>();

实现了泛型的协变。当A≤B时,有f(A)≤f(B)成立。看下面代码:

List fruitExtends = new ArrayList<>();
List fruits = new ArrayList<>();
List apples = new ArrayList<>();

fruits = apples; //这个是报错的
fruitExtends = apples; //List使其实现了协变,不报错
上面代码可以看出,可以把Fruit及其子类的ArrayList赋值给List fruits。实现了泛型的协变。

在PECS原则中,extends代表生产者,特点是:不能往里存,只能往外取。

List fruits = new ArrayList<>();
fruits.add(new Apple());//报错
fruits.add(new RedApple());//报错
fruits.add(null);//不报错,虽然不知道fruits所持有的具体元素,但null代表任何类型
(1)不能往里存的原因,如上代码表示list里的元素只能是Fruit或Fruit的子类,因为无法确定List所持有的具体的类型是什么,只知道里面是任何一个继承了Fruit 的子类,所以无法向其中添加元素。编译器在看到apples的赋值,但是List并没有标明其中就是Apple,而是用上了一个占位符capture#1来表示捕获了一个Fruit或Fruit的子类,其实它并不知道是什么,你的插入都和capture#1不匹配。如果可以往里存,那么对于fruits来说,你可以往里放一个Apple,也可以放一个Orange,但这显然是不对的。

需要注意的是:可以添加null,因为null可以表示任何类型。

fruit.add(null);

(2)只能往外取的原因,指定了T为所有元素的“根”,所以可以用T来使用容器里的元素。也就是说不管是什么子类,不管追溯多少辈,肯定有个父类T,所以,对于get方法,我们可以用最大的父类T来接着,也就是把所有的子类向上转型为T。

2、下界:
List list = new ArrayList<可以是T或是T的父类>();
实现了泛型的逆变。当A≤B时,有f(B)≤f(A)成立。看下面代码:

List appleSuper = new ArrayList<>();
List fruits = new ArrayList<>();
List apples = new ArrayList<>();

apples = fruits; //这个是报错的
appleSuper = fruits; //List使其实现了逆变,不报错
上面的代码可以看出,可以把Apple的父类赋值给List appleSuper。实现了泛型的逆变。

在PECS原则中,super代表消费者,特点是:只能往里存,不能往外取。

(1)不能往外取的原因,和extends一样,list里存的只能是T或T的父类,因为无法确定具体的类型是什么,所以往外取时并不知道取出来的是什么,所以无法从list中取元素。

需要注意的是:因为所以的对象都有个必然的父类Object,所以可以读取到Object对象。和extends的null一样。

Object obj = list.get(n);

appleSuper.add(new Apple());
appleSuper.add(new RedApple());
appleSuper.add(new Fruit()); //报错
(2)只能往里存的原因,指明了集合中存放的只能是T或T的父类,所以我们可以把fruits赋值给appleSuper,往里面添加元素时,上面的代码可以看出,往集合里添加Apple或Apple的子类时是可以的,但是存Apple的父类时就会报错。

这里可能会有一个疑问,集合中存放的是Apple或Apple的父类,我们将其父类添加进去的时候为什么会报错呢?

虽然这是一个Apple或Apple的父类的容器,但是具体的是什么类型并不知道,而往里存Apple的父类时,可能往里存的是一个Friut或是一个和Fruit无关的接口,那么就出现了两个不想关的对象。往里添加Apple或Apple的子类时,不关这个子类是什么,它都有一个共同的父,所以是可以的。

三、泛型通配符该什么时候使用?
在Effective Jave一书中总结:为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果每个输入参数既是生产者,又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型比配,这是不用任何通配符而得到的。

简单来说就是PECS原则。

一个经典的例子是java.uitl.Collections中的copy()方法:

public static void copy(List dest, List src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");

if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i di=dest.listIterator();
ListIterator si=src.listIterator();
for (int i=0; i

java泛型之通配符?的更多相关文章

  1. Java泛型和通配符那点事

    泛型(Generic type 或者generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法 ...

  2. java 泛型的通配符和限定

    package cn.sasa.demo1; import java.util.ArrayList; import java.util.Collection; import java.util.Ite ...

  3. Java泛型之通配符

    原文点此链接 使用通配符的原因:Java中的数组是协变的,但是泛型不支持协变. 数组的协变 首先了解下什么是数组的协变,看下面的例子: Number[] nums = new Integer[10]; ...

  4. Java 泛型、通配符? 解惑

    Java 泛型通配符?解惑 分类: JAVA 2014-05-05 15:53 2799人阅读 评论(4) 收藏 举报 泛型通配符上界下界无界 目录(?)[+] 转自:http://www.linux ...

  5. JAVA 泛型与通配符的使用

    泛型的本质是参数化类型.即所操作的数据类型被指定为一个参数. 1.jdk 1.5/1.6 必须显式的写出泛型的类型. 2.jdk 1.7/1.8 不必显式的写出泛型的类型. 一.泛型声明 可以用< ...

  6. Java 泛型和通配符解惑

    转自:http://www.linuxidc.com/Linux/2013-10/90928.htm T  有类型 ?  未知类型 一.通配符的上界 既然知道List<Cat>并不是Lis ...

  7. java 泛型与通配符(?)

    泛型应用于泛型类或泛型方法的声明. 如类GenericTest public class GenericTest<T> { private T item; public void set( ...

  8. java 泛型: 通配符? 和 指定类型 T

    1. T通常用于类后面和 方法修饰符(返回值前面)后面 ,所以在使用之前必须确定类型,即新建实例时要制定具体类型, 而?通配符通常用于变量 ,在使用时给定即可 ? extends A  :  通配符上 ...

  9. [转]JAVA泛型通配符T,E,K,V区别,T以及Class<T>,Class<?>的区别

    原文地址:https://www.jianshu.com/p/95f349258afb 1. 先解释下泛型概念 泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被 ...

随机推荐

  1. json对象遍历顺序问题

    对json对象遍历我们一般使用for in循环,或者Object.keys + 数组方法.在接触js以来听到过一种说法: for in 遍历顺序是不可靠的 但是在实际开发中for in 循环也是按照其 ...

  2. react 有没有类似vue中watch这样的api?

    就是 当组件里state 里的数据发生变化可以监听到这个数据的变化 当数据发生变化的时候做一些事情 比如ajax请求 ?初学react 用vue的时候会用watch 和computed 去监听数据发生 ...

  3. 8个必备的Python GUI库

    Python GUI 库有很多,下面给大家罗列常用的几种 GUI 库.下面介绍的这些GUI框架,能满足大部分开发人员的需要,你可以根据自己的需求,选择合适的GUI库. 很多人学习python,不知道从 ...

  4. myblogplus 第三期 如何更改你博客的图标,已实现 - mooling原创

    三言两语 博客的logo可以凸显你的blog的个性 不知道你有没有觉得博客园原始的那个小矿工不好看了呢 fromto 这才是个人博客的style! 为什么要写这篇文章 因为在博客园的“找找看”中,如果 ...

  5. 2020年的UWP——通过Radio类控制Cellular(1)

    最近在做UWP的项目,在2020年相信这已经是相对小众的技术了,但是在学习的过程中,发现某软这么几年仍然添加了不少的API,开放了相当多的权限.所以打算总结一下最近的一些经验和收获,介绍一下2020年 ...

  6. three.js尝试(一)模拟演唱会效果

    工作闲暇之余,偶然翻到了Three.js的官网,立刻被它酷炫的案例给惊艳到了,当即下定决心要试验摸索一番,于是看demo,尝试,踩坑,解决问题,终于搞定了,一个模拟演唱会场景. 主角围绕一个钢管在舞动 ...

  7. nginx模块化结构

    NGINX是一个免费.开源.高性能.轻量级的HTTP和反向代理服务器,也是一个电子邮件(IMAP/POP3)代理服务器 特点: 占有内存少,并发能力强 Nginx的优点: 模块化.事件驱动.异步.非阻 ...

  8. yum wget rpm

    wget 类似于迅雷,是一种下载工具          下载安装包 yum: 是redhat, centos 系统下的软件安装方式 下载安装包并自动安装 以及一些依赖关系 基于RPM包管理,能够从指定 ...

  9. H5游戏定制,4大优势助力企业曝光10W+

    H5游戏定制,4大优势助力企业曝光10W+ 移动互联网已成为了人们生活的一部分,普通广告形式已很难吸引用户的眼球,企业要怎样才能将广告更广泛的传播给更多用户呢?根据TOM游戏多年从业经验,为大家分享以 ...

  10. touchstart 事件与 click 事件的冲突

    const clickEvent = (function() {   if ('ontouchstart' in document.documentElement === true)     retu ...