泛型的意义
范型——类型参数化
目的是安全
多态提供灵活性,范型提供安全性(泛型提供了安全的向下转型)
泛型语法可以让编译器强制执行额外的类型检查,如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
或其子类的实例
通配符<?>
和类型参数<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>
在运行时事实上是相同的类型:List
,Class<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泛型搞成这个样子,就是因为,工期不够。。。。然后后来也没法改了