泛型

  2020-4-17 


JAVA泛型

泛型的意义

范型——类型参数化

目的是安全

多态提供灵活性,范型提供安全性(泛型提供了安全的向下转型)

泛型语法可以让编译器强制执行额外的类型检查,如ArrayList<Integer> list;,会强制检查内部元素是否为Integer类型

在为了泛化而使用多态特性的情景中,因为在使用泛型类时指明了数据类型,赋给其他类型的值会抛出异常,于是向下转型的时候没有潜在的风险。否则使用Object作为默认参数类型,不进行类型检查,取出再向下转型很危险;我们通过把类型也作为参数,解决了这个问题

泛型语法

其实就是把类型当成一个参数进行传递,不过写法比较特别

与普通类的定义相比,泛型的代码在类名后面多出了<T1, T2>T1, T2是自定义的标识符,也是参数,用来传递数据的类型,而不是数据的值,我们称之为类型参数。在泛型中,不但数据的值可以通过参数传递,数据的类型也可以通过参数传递。T1, T2只是数据类型的占位符,运行时会被替换为真正的数据类型。

传值参数(我们通常所说的参数)由小括号包围,如(int x, double y),类型参数(泛型参数)由尖括号包围,多个参数由逗号分隔,如<T><T, E>
也可以定义范型方法、泛型接口,总之只要是(引用)类型,都可以作为参数了,使用也是按原来那样使用

举例:

//使用泛型的类
ClassName<T1,T2> //这里<T1,T2>就是泛型参数表了,这是一种专门传递类型参数的特殊写法
{
    T1 x;  //这里就使用了T1,T2类型参数
    T2 y;
}

//主函数使用
objectName<T1,T2> balabala....  //<T1,T2>就是实际使用的时候,传递进去的参数

类型参数需要在类名后面给出。一旦给出了类型参数,就可以在类中使用了。类型参数必须是一个合法的标识符,习惯上使用单个大写字母,通常情况下,K 表示键,V 表示值,E 表示异常或错误,T 表示一般意义上的数据类型。

注意:方法参数列表中的的参数类型是局部参数,形式参数!这点和普通参数一样

泛型边界限制:(关于边界的意义见下面第4条)

通过 extends关键字可以限制泛型的类型上边界<T extends Number> 表示 T 只接受 Number 及其子类,传入其他类型的数据会报错。这里的限定使用关键字 extends,后面可以是类也可以是接口。但这里的extends已经不是继承的含义了,应该理解为 T 是继承自 Number 类的类型,或者 T 是实现了 XX 接口的类型,它申明了边界。

可以一个class边界+多个interface边界<T extends Artist & CanPlay & CanSing>

特殊用法:使用通配符(通配符?,如Class<?> intClass = int.class

通过<? extends T>限制泛型的类型下边界,通过<? super T>限制泛型的类型下边界,单边界

为什么要用通配符:

如下常用用法:可以扩大泛型表示范围。如果左边写Plate<Fruit>就会出错,因为Apple虽然是Fruit的子类,但Plate<Apple>不是Plate<Fruit>的子类(前面Class与泛型就是这个原因用通配符!见Ch.14-8)于是我们用通配符,导致往里存的可以是任意Fruit或其子类的实例

困扰多年的Java泛型 extends T>  super T>,终于搞清楚了!

通配符<?>和类型参数<T>的区别就在于,对编译器来说所有的T都代表同一种类型,而<?>不是。所以扩大范围会导致副作用:<? extends T>只能取,不能存?代表可以是不同类型,但容器规定不能向容器中存不同类型的对象),而<? super T>只能存,不能取,要取只能取出Object(泛型范围扩大到Object,取出来也必须设为Object类型才装得下)

所以要遵循PECS(Producer Extends Consumer Super)原则:频繁往外读取内容的,适合用上界Extends,经常往里插入的,适合用下界Super

更多参考:为什么要使用通配符边界表示,区别与作用

特殊用法:无界通配符

<?>

不知道里面是什么,所以不允许向List<?>存数据,也不可取数据,更多作为类型参数检查

捕获转换技术:如果向一个使用<?>的方法传递原生类型,那么对于编辑器来说,可能会推断出实际的类型参数,使得这个方法可以调用另一个使用确切类型的的方法(略)

JAVA泛型与多态

JAVA泛型都是按无类型信息的Object存的(类型信息被擦除),它的实现基础是多态,多态的实现是依靠动态绑定

JAVA泛型深层理解——擦除、边界

C++的泛型通俗点说是通过在编译时的实例化将泛型类(以类为例)实例化为多个实例(保留了类型信息,是实体)。
而Java则是在编译的时候进行泛型的错误检查,然后进行类型擦除,去掉泛型,保留原始类型,共享同一块代码。也就是说:JAVA没有C++那种真正的泛型

JAVA泛型是使用擦除实现的,在JAVA编译期结束后,泛型信息都被擦除了。实际上:你唯一知道的就是你在使用一个对象泛型类型信息只有在静态类型检查期间(编译期的一个步骤)才会出现,它帮助编译器执行类型检查,在此之后,程序中的所有泛型类型信息都被擦除,替换为它们的非泛型上界(边界见下)

于是在编译期结束,java文件编译为class文件后,JVM看到的只有实际对象,而由泛型附加的类型信息对JVM来说已经被擦除,是不可见的

举例:List<Integer>List<String>在运行时事实上是相同的类型:ListClass<Integer>Class<Father>事实上也是相同的类型Class,而普通的类型变量在未指定边界的情况下将被擦除为Object(未指定上界,那么任何object extends Object,其上界就是Object)

在C++中,由于泛型被真正的实现了,所以程序在编译期就能判断该泛型具体是什么类型,如果你调用了该泛型没有的方法或属性,那么程序在编译期就会报错,如果有就不会。然而,JAVA泛型做不到:由于泛型类型被擦除,JAVA编译器无法判断该泛型具体是什么类型,一律无法编译通过。于是在JAVA中,我们必须协助泛型类,人为给出泛型的边界,以告诉编译器只能接受遵循这个边界的类型。(如果不遵守,就会编译报错,这其实就达到了C++一样的目的,不过泛型边界是编码者人为设置的)本段话具体例子见下面的“举例”

于是我们可以得出:一个泛型参数<T>不能直接拿去用,得先规定边界,比如<Integer>、<? extends Father>都是边界,前者限定Integer,后者限定Father及其子类(上界

举例:

class Father
{
    static String f()
        {
            return "i am father";
        }
}

//可行
class FXtest<T1 extends Father>  //规定上边界Father;或者class FXtest<Father>也行
{
    FXtest()
    {
        System.out.println(T1.f());
    }
}

//不可行
class FXtest<T1>  //没有规定边界,一律报错;然而这在C++里是可行的。
{
    FXtest()
    {
        System.out.println(T1.f());
    }
}

JAVA泛型只是提供了一种类型信息,它的底层仍然只是个Object(或其它上界),你只是看起来好像拥有有关参数的类型信息而已,所以涉及具体操作的时候,你要认识清楚,它实际上是什么类(Object或其它上界),只不过我们额外告诉编译器它应该的类型信息,提供一种安全向下转型的保证。(不过像容器类,你get()将对象取出容器的时候就顺带给你转型了(毕竟泛型已经提供安全了),而不是让你取出来的还是个Object或其它上界然后自己转型)

所以“擦除”擦的是:真实类型(上界)以下的类型信息。真实对象类型是默认情况的最高上界(无人为规定上界的话)

一些细节问题

①任何基本类型都不能作为类型参数(声明泛型类型引用的时候必须写非基本类型,但向里存的时候容器类会自动装箱,但底层依然不是基本类型)

②一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口

③使用带有泛型类型参数的转型或instanceof不会有任何效果

④不能重载:由于擦除的原因,重载方法将产生相同的类型签名

自限定类型【略】

自限定类型强制泛型当做其自己的边界参数来使用

class SelfBounded<T extends SelfBounded<T>> { }

后记

擦除减少了泛型的泛化型。在JAVA基于擦除的泛型实现中,泛型类型被当作第二类类型处理(如前文所说),即不能在某些重要的上下文环境中使用的类型。JAVA使用擦除实现泛型的主要原因是JAVA是后来才加入泛型的,为了兼容性。JAVA泛型不是真正的泛型,像是在原先不安全的多态使用上加了一层限制,也就是刚开始说的:“泛型提供了安全的向下转型”

泛型的所有动作都发生在边界处

容器类就用到了指定类型的泛型,比如List<Integer>,它没有用到泛型基于多态的泛化作用,只顺带用到了泛型的类型检查作用。指定类型实际上就是界内容量为1。

当JAVA最初被创建时,它的设计者们当然了解C++的模板,他们甚至考虑将其囊括到JAVA语言中,但是出于这样或那样的原因,他们决定将模板排除在外(其迹象就是他们过于匆忙)

所以说,JAVA泛型搞成这个样子,就是因为,工期不够。。。。然后后来也没法改了


且听风吟