转载

《重学Java系列》之 泛型(下)

不诗意的女程序媛不是好厨师~ 转载请注明出处,From李诗雨--- blog.csdn.net/cjm24848365… 】

《重学Java系列》之 泛型(下)
在 《重学Java系列》之 泛型(上)

中我们已经知道了①什么是泛型,②为什么要有泛型,③泛型类、泛型接口 和泛型方法,④给泛型变量增加约束,⑤泛型类型的继承规则。

今天要讲的就是有一点点难度的东西了,理解一个东西最好的方式就是: 自己找到相似的情景作类比→画图帮助理解→自己敲代码理解→反思回味。 毕竟只有经过自己大脑思考的东西,才能为己所用。

笔记我还是整理的很细,也很好理解。那么今天,让我来继续学习剩下的三点内容吧~♥

6.泛型通配符

《重学Java系列》之 泛型(下)

你见过上面☝这样的代码吗

?和关键字 extends 或者 super 在一起其实就是泛型的高级应用:通配符。

  • ? extends X 表示类型的上界,类型参数是X的子类
  • ? super X 表示类型的下界,类型参数是X的超类

对于它们的使用送大家八字咒语:“上界不存下界不取”

这个咒语什么意思,别急让我来慢慢讲:

(1). 先看看 ? extends X

它表示传递给方法的参数,必须是X的子类(包括X本身), 是类型的 上界

所以 上界 就是 针对 ?extends X 来说的。

我们来让代码自己说它是什么意思~ 有这样的几个类:

public class Animal {
	//...
}

复制代码
public class Pet extends Animal {
	//...
}
复制代码
public class Dog extends Pet {
	//...
}
复制代码

又有这样一个方法:

public static void print(Generic<? extends Animal> a){
        System.out.println(a.getData().getName());
    }
复制代码

我们把这个方法和 ? extends X 对应一下,那此时的Animal类就相当于是X,由于它表示类型的上界,所以,我们可以传Animal及其子类。

再画个图来帮助我们理解一下 :

《重学Java系列》之 泛型(下)

所以如下的操作都是没有问题的:

《重学Java系列》之 泛型(下)

现在, ?extends X 的含义大家应该已经理解,并且知道怎么传递类型参数了吧。

知道了魔咒 “上界不存” 中的 “上界” 的意思了,那 “不存” 又是什么意思呢? 继续往下看: 现在有一个Generic的泛型类,它提供了get和set类型参数变量的方法:

public class Generic<T> {
    //...

    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

复制代码

我想通过set方法往里面放数据,但是,set方法却不允许被调用!编译不通过!

《重学Java系列》之 泛型(下)
这是怎么回事呢??? 其实这个主要是出于安全方面考虑。编译器是一个责任心极强的东西,他 只做它可以 确定 的事情不确定的事情 它是坚决 不做

的。

既然 ? extends X 表示 类型的上界 ,类型参数是X的子类,

  • 那么可以肯定的是:get方法返回的一定是个X(不管是X或者X的子类)编译器是可以确定知道的。所以,get它允许调用。
  • 但是set方法只知道传入的是个X,至于具体是X的那个子类,编译器不知道。所以,出于安全考虑,编译器干脆就不做了。

所以: ? extends X 主要用于安全地访问数据,可以访问X及其子类型,但是不能写入非null的数据。

这就是 “不存” 的意思了,现在, “上界不存” 的咒语你理解了吗?

(2). 再看看 ? super X

它表示传递给方法的参数,必须是X的超类(包括X本身), 与 ? extends X相反,它表示类型 下界

所以 下界 就是 针对 ?superX 来说的l了。

我们用类似的方法来理解一下 “下界不取” 的意思。

还是上面的 Animal、Pet 和Dog类。 我们又有一个方法:

public static void printSuper(Generic<? super Pet> a){
        System.out.println(a.getData());
    }
复制代码

那这个方法中的 Pet 就相当于 ? super X 里的X,由于它表示类型的 下界 ,所以,我们可以传Pet及其超类。 同样的,我们也来画个图:

《重学Java系列》之 泛型(下)

我们再在代码中验证一下:

《重学Java系列》之 泛型(下)
如此看来, ?super X下界

的含义就是: X说:“我就是底线,不能再比我低了,否则我会生气的,后果很严重!哼!☹”

“下界不取” 中的 不取 又是什么道理呢? 我们把之前的代码稍作改变:

《重学Java系列》之 泛型(下)

我们发现 此时 set方法可以被调用的,且能传入的参数只能是X或者X的子类。 但是get方法只会返回一个Object类型的值,并没有什么实际意义。

对此现象我们 可以这样来理解: ? super X 表示类型的 下界 ,类型参数是X的超类(包括X本身)。

  • 那么就可以肯定的说,get方法返回的一定是个X的超类,但是到底是哪个超类?不知道。但是可以肯定的说,Object一定是它的超类,因为Object是所有类的超类,所以get方法返回Object。但是这样取出的全是Object其实并没有什么意义。
  • 对于set方法来说,编译器不知道它需要的确切类型,但是X和X的子类可以安全的转型为X。所以,此时set是被允许的。

所以, ? super X 主要用于安全地写入数据,可以写入X及其子类型。但是读取只能得到Object,并没有什么实际意义,所以我们就没有太大必要去get了。

所以 “下界不取”“不取” 含义也就在于此了。

综上,我们就知道 “上界不存,下界不取” 的真实含义其实就是: 都是出于 安全考虑,

  • “?extends X” 主要用于安全地访问数据。它只用来读数据,不用来存放数据。
  • ? super X 主要用于安全地写入数据。它只用来写入数据,不用来取出数据。

说的官方一点,这里其实就涉及到 PESC 原则了。 即 Producer Extends Consumer Super 如果参数化类型表示一个生产者,就使用<? extends T>;如果它表示一个消费者,就使用<? super T>

即: 只读不可写时,使用Generic<? extends Animal>:Producer 只写不可读时,使用Generic<? super Pet>:Consumer

7.泛型使用的局限性

无规矩不成方圆,其实泛型的使用也是有局限性和约束的。 下面就让我们来一一看一下,哪些地方是使用泛型时需要格外注意的~

我们都以 Generic 这个泛型类来讲:

《重学Java系列》之 泛型(下)

1. 泛型中不能用 基本数据类型 来实例化类型参数,

而要用 基本数据类型对应的包装类才可以。 代码为证:

《重学Java系列》之 泛型(下)

2. 运行时 只看 原始类型

这个要稍微做个解释,先问大家一个问题 : (PS:其实这一点也可以从类型擦除角度来理解)

Generic<String> stringG=new Generic<>();
        Generic<Boolean> booleanG=new Generic<>();
        //输出会是什么?
        System.out.println(stringG.getClass()==booleanG.getClass());
        //输出又会是什么
        System.out.println("stringG.getClass()的name是:"+stringG.getClass().getName());
        System.out.println("booleanG.getClass()的name是:"+booleanG.getClass().getName());

复制代码

答案是:

《重学Java系列》之 泛型(下)

哇,stringG.getClass() 和 booleanG.getClass() 竟然相等,他们对应的是同一个类型:Generic ,而不是 Generic< String> ,Generic< Boolean>.

这也就说明 运行时的类型 检测,只适用于 原始类型 Generic ,而不是Generic< String> 、Generic< Boolean>.。

所以下面这个代码就是不正确的, instanceof 在 泛型上 是不能使用的,只能用于原始类型。

《重学Java系列》之 泛型(下)

3. 不能在静态域或方法中引用类型变量

《重学Java系列》之 泛型(下)

这是为什么呢: 因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行顺序是,先static的部分,然后才是构造函数。 所以在对象初始化之前static的部分就已经执行了,如果你在静态部分引用了泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。

4. 不能实例化类型变量,

也就是:

《重学Java系列》之 泛型(下)

这个T本来就是虚的,都没有摸清底细,你就乱new 肯定是不行的。

5. 不能捕获泛型类的实例

《重学Java系列》之 泛型(下)

所以,要注意一点:泛型类是不能extends Exception/Throwable的。

再来:

《重学Java系列》之 泛型(下)

我们是不能直接捕获泛型类的对象的。那如果我们想捕获 泛型 的异常就没有办法了吗?我们可是伟大的码农,所以方法还是有的:

public <T extends Throwable> void doSth(T x) throws T{
        try{

        }catch(Throwable e){
            throw x;
        }
    }
复制代码

虽然不允许我们直接捕获泛型类的对象,但是我们可把 泛型类的对象 throw 出去呀,就是这么机智✌~

7.浅谈类型擦除

最后我们再来提一提泛型中的类型擦除吧。

泛型思想早在C++语言的模板(Template)中就开始生根发芽,在Java语言还没有出现泛型的版本时,只能通过 Object是所有类型的父类类型强制转换 两个特点的配合来实现类型泛化。 由于Java语言里面所有的类型都继承于java.lang.Object,所以Object转型成任何对象都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会转嫁到程序运行期之中。

泛型技术在C和Java之中的使用方式看似相同,但实现上却有着根本性的分歧。C里面的泛型是真实泛型,而Java中的泛型其实是伪泛型。 这点怎么说呢?

  • 是这样的,在 C里面 泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符),或是运行期的CLR中, 都是切实存在的 ,List<Integer>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为 真实泛型
  • Java中 的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<Integer>与ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为 类型擦除 ,基于这种方法实现的泛型称为 伪泛型

至于为什么java为什么采用 类型擦除 来实现 泛型? 这是因为java 1.5之前并没有泛型这个概念,泛型出现后,为了兼容之前的版本,就采用了类型擦除折中的策略:编译时对泛型要求严格,运行时却把泛型擦除了。

看下面的代码:

《重学Java系列》之 泛型(下)

编译不通过:

《重学Java系列》之 泛型(下)

按理说一个方法,我传入的参数类型不一样的话,是可以重载的呀?那这里为什么会呢? 上面这段代码之所以不能编译,是因为参数List<Integer>和List<String>编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两种方法的特征签名变得一模一样。

这是因为参数List<Integer>和List<String>编译之后都被擦除了,变成了一样的原生类型List。所以此处传入的其实是同一个类型。不是重载。

【后记】以初学者的心态去重学java,我突然发现自己不懂的越来越多了,问号一个接一个的在脑子里跳舞。就拿泛型来说,我还是有很多要去学习和探究的地方,这里也只是做个学习后的整理,等下一次我有了新的收获后再继续分享,希望可以和大家一起,进步进步再进一步~

积累点滴,做好自己~

原文  https://juejin.im/post/5de1fba56fb9a071ad4bb07b
正文到此结束
Loading...