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

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. spring-boot如何生成元数据与javaBean进行关联用作配置文件提示

    spring-boot如何生成元数据与javaBean进行关联用作配置文件提示 首先需要引入一个jar依赖包,以及一个maven plugin如下所示 <dependency> <g ...

  2. ARM开发板实现双系统引导的一种方法——基于迅为iTOP-4412开发板

    前言 本文所用的uboot代码为迅为官方提供,开发板是迅为iTOP-4412开发板.本文如有错误,欢迎指正. 首先,我们确定一下系统启动的流程:首先启动uboot,uboot启动内核并挂载rootfs ...

  3. java 区块

    方法区:存放staic变量,方法签名,类信息,字段等 堆:存放对象数据,string常量 栈:存放对象的引用,操作数,没逃逸但是逃逸分析且被编译器产生逃逸优化的对象数据

  4. MySQL数据库中几种数据类型的长度

    在MySQL里新建表自然会涉及设置字段长度,但有时会发现长度限制在一些字段类型中不起作用?字段长度是按字节算还是字符算? 如图中:int看起来只要还在本身类型取值范围内就行,字段长度没有起到作用:而c ...

  5. 基于Celery在多台云服务器上实现分布式

    起源 最近参加公司里的一个比赛,比赛内容里有一项是尽量使用分布式实现项目.因为项目最终会跑在jetsonnano,一个贼卡的开发板,性能及其垃圾.而且要求使用python? 找了很多博客,讲的真的是模 ...

  6. 目标检测中的IOU和CIOU原理讲解以及应用(附测试代码)

    上期讲解了目标检测中的三种数据增强的方法,这期我们讲讲目标检测中用来评估对象检测算法的IOU和CIOU的原理应用以及代码实现. 交并比IOU(Intersection over union) 在目标检 ...

  7. java-介绍函数理解重载

    package day02; public class FunctionOverload { public static void main(String[] args){ int a = add(, ...

  8. 如何编写一个简单的Linux驱动(二)——设备操作集file_operations

    前期知识 如何编写一个简单的Linux驱动(一)--驱动的基本框架 前言 在上一篇文章中,我们学习了驱动的基本框架.这一章,我们会在上一章代码的基础上,继续对驱动的框架进行完善.要下载上一篇文章的全部 ...

  9. 大神Java8写了一段逻辑,我直呼看不懂

    业务背景 首先,业务需求是这样的,从第三方电商平台拉取所有订单,然后保存到公司自己的数据库,需要判断是否有物流信息,如果有物流信息,还需要再进行上传. 而第三方接口返回的数据是 JSON 格式的,其中 ...

  10. [LeetCode]67. 二进制求和(字符串)(数学)

    题目 给你两个二进制字符串,返回它们的和(用二进制表示). 输入为 非空 字符串且只包含数字 1 和 0. 题解 两个字符串从低位开始加,前面位不够补0.维护进位,最后加上最后一个进位,最后反转结果字 ...