泛型是 jdk5 引入的类型机制,就是将类型参数化,它是早在 1999 年就制定的 jsr14 的实现。
泛型机制将类型转换时的类型检查从运行时提前到了编译时,使用泛型编写的代码比杂乱的使用 object 并在需要时再强制类型转换的机制具有更好的可读性和安全性。
泛型程序设计意味着程序可以被不同类型的对象重用,类似 c++ 的模版。
泛型对于集合类尤其有用,如
。这里可能有疑问,既然泛型为了适应不同的对象,
- ArrayList
本来就可以操作不同类型的对象呀?那是因为没有泛型之前采用继承机制实现的,实际上它只维护了一个
- ArrayList
对象的数组。结果就是对 List 来说它只操作了一类对象
- Object
,而在用户看来却可以保存不同的对象。
- Object
泛型提供了更好的解决办法——类型参数,如:
- List<String> list =newArrayList<String>();
这样解决了几个问题:
- 1 可读性,从字面上就可以判断集合中的内容类型;
- 2 类型检查,避免插入非法类型。
- 3 获取数据时不在需要强制类型转换。
- public class Pair<T>{
- privateT field1;
- }
其中
是类型参数定义。
- <T>
使用时:
- Pair<String> p = new Pair<String>();
此时类内部的
就是字符串类型了。
- field1
如果引用多个类型,可以使用逗号分隔:
- <S, D>
类型参数名可以使用任意字符串,建议使用有代表意义的单个字符,以便于和普通类型名区分,如:
代表
- T
,有原数据和目的数据就用
- type
,
- S
,子元素类型用
- D
等。当然,你也可以定义为
- E
, 甚至
- XYZ
。
- xyZ
泛型方法定义如下:
- public static T marshalle(T arg){}
与泛型类一样,
是类型参数定义。如:
- <T>
- public class GenericMethod{
- public static T getMiddle(T... a){returna[a.length/2];
- }
- }
严格的调用方式:
- String o=GenericMethod.getMiddle("213","result","12");
一般情况下调用时可以省略,看起来就像定义 String 类型参数的方法:
,这是因为 jdk 会根据参数类型进行推断。看一下下面的例子:
- GenericMethod.getMiddle(String,String,String)
- Object o=GenericMethod.getMiddle("213",0,"12");
- System.out.println(o.getClass());
- System.out.println(o);
输出结果为:
- class java.lang.Integer
- 0
这是因为 jdk 推断三个参数的共同父类,匹配为 Object, 那么相当于:
- Object o=GenericMethod.getMiddle("213",0,"12");
习惯了类型参数放在类的后面,如
,泛型方法为什么不放在后面?看一个例子:
- ArrayList<String>
- public static T f(T t) {
- return t;
- }
- public static class a {}
- public static class b {}
- //尽量恶心一点
- @Test public void test() {
- a c = new a();
- f(c); //OK
- f(c); //error,看起来像是一个逗号运算符连接的两个逻辑表达式,当然目前java中除了for(...)并不支持逗号运算符
- }
因此,为了避免歧义,jdk 采用类型限定符前置。
如果泛型方法定义在泛型类中,而且类型参数一样:
- public class GenericMethod<T> {
- public void sayHi(T t){
- System.out.println("Hi "+t);
- }
- }
是不是说,定义 GenericMethod 时传了 Integer 类型,sayHi() 也就自动变成 Integer 了呢?No。
- String i="abc";newGenericMethod().sayHi(i);
该代码运行一点问题都没有。原因就在于泛型方法中的
, 如果去掉它,就有问题了。
- <T>
- Themethod sayHi(Integer) in the type GenericMethod<Integer>is not applicable for the arguments
- (String)
小结:
泛型方法有自己的类型参数,泛型类的成员方法使用的是当前类的类型参数。
方法中有
是泛型方法;没有的,称为泛型类中的成员方法。
- <T>
如果限制只有特定某些类可以传入
参数,那么可以对
- T
进行限定,如:只有实现了特定接口的类:
- T
,表示的是 Comparable 及其子类型。
- <T extends Comparable>
为什么是
不是
- extends
,或者其他限定符?
- implements
- 严格来讲,该表达式意味着:`TsubtypeOf Comparable`,jdk不希望再引入一个新的关键词;其次,T既可以是类对象也可以是接口,如果是类对象应该是`implements`,而如果是接口,则应该是`extends`;从子类型上来讲,extends更接近要表达的意思。
- 好吧,这是一个约定。
限定符可以指定多个类型参数,分隔符是
,不是逗号,因为在类型参数定义中,逗号已经作为多个类型参数的分隔符了,如:
- &
。
- <T,S extends Comparable & Serializable>
泛型限定的优点:
限制某些类型的子类型可以传入,在一定程度上保证类型安全;
可以使用限定类型的方法。如:
- public class Parent<T>{
- privateT name;publicTgetName() {returnname;
- }public void setName(T name) {//这里只能使用name自object继承的方法
- this.name = name;
- }
- }
加上限定符,就可以访问限定类型的方法了,类型更明确。
- public class Parent<T extends List<T>>{
- privateT name;publicTgetName() {returnname;
- }public void setName(T name) {//这里可以访问List的方法,如name.size()
- this.name = name;
- }
- }
注:
我们知道 final 类不可继承,在继承机制上
是错误的,但泛型限定符使用时是可以的:
- class SomeString extends String
,只是会给一个警告。
- <T extends String>
后面的通配符限定有一个 super 关键字,这里没有。
泛型只在编译阶段有效,编译后类型被擦除了,也就是说 jvm 中没有泛型对象,只有普通对象。所以完全可以把代码编译为 jdk1.0 可以运行的字节码。
定义部分,即尖括号中间的部分直接擦除。
- public class GenericClass<T extends Comparable>{}
擦除后:
- public class GenericClass{}
引用部分如:
- publicT field1;
其中的 T 被替换成对应的限定类型,擦除后:
- publicComparable field1;
如果没有限定类型:
- public class GenericClass<T>{
- publicT field1;
- }
那么的替换为 object, 即:
- public class GenericClass{
- publicObject field1;
- }
有多个限定符的,替换为第一个限定类型名。如果引用了第二个限定符的类对象,编译器会在必要的时候进行强制类型转换。
- public class GenericClass<T extends Comparable&Serializable>{
- publicT field1;
- }
类擦除后变为:
- public class GenericClass{
- publicComparable field1;
- }
而表达式返回值返回时,泛型的编译器自动插入强制类型转换。
反编译 GenericClass:
- Compiled from"GenericClass.java"
- public class com.pollyduan.generic.GenericClass<T> {
- publicT field1;publiccom.pollyduan.generic.GenericClass();
- }
好像前面说的不对啊,这还是 T 啊,没有擦除呀?
这就是擦除的残留。反汇编:
- {publicT field1;
- descriptor: Ljava/lang/Object;
- flags: ACC_PUBLIC
- Signature: #8 // TT;
- publiccom.pollyduan.generic.GenericClass();
- descriptor: ()V
- flags: ACC_PUBLIC
- Code:
- stack=1, locals=1, args_size=1
- 0: aload_01: invokespecial #12 // Method java/lang/Object."<init>":()V
- 4:returnLineNumberTable:
- line2:0LocalVariableTable:
- Start Length Slot Name Signature0 5 0 thisLcom/pollyduan/generic/GenericClass;
- LocalVariableTypeTable:
- Start Length Slot Name Signature0 5 0 thisLcom/pollyduan/generic/GenericClass;
- }
- SourceFile: "GenericClass.java"Signature: #22 // <T:Ljava/lang/Object;>Ljava/lang/Object;
其中:
descriptor:对方法参数和返回值进行描述; signature:泛型类中独有的标记,普通类中没有,JDK5 才加入,标记了定义时的成员签名,包括定义时的泛型参数列表,参数类型,返回值等;
可以看到
是签名,还保留了定义的格式;其对应的参数类型是
- public T field1;
。
- Ljava/lang/Object;
最后一行是类的签名,可以看到 T 后面有跟了擦除后的参数类型:
。
- <T:Ljava/lang/Object;>
这样的机制,对于分析字节码是有意义的。
原因在于类型擦除,Object 不能存储基本类型:
byte,char,short,int,long,float,double,boolean
从包装类角度来看,或者说三个: Number(byte,short,int,long,float,double),char,boolean
- if (aaa instanceof Pair) {} //error
- Pair p = (Pair) a; //warn
- Pair p;
- Pair i;
- i.getClass() == p.getClass(); //true
- GenericMethod[] o = null; //ok
- o = new GenericMethod[10]; //error
可以定义泛型类对象的数组变量,不能创建及初始化。
注,可以创建通配类型数组,然后进行强制类型转换。不过这是类型不安全的。
- o=(GenericMethod[]) newGenericMethod[10];
不可以创建的原因是:因为类型擦除的原因无法在为元素赋值时类型检查,因此 jdk 强制不允许。
有一个特例是方法的可变参数,虽然本质上是数组,却可以使用泛型。
安全的方法是使用 List。
java 不支持泛型类型的对象数组,可变参数是可以的。它也正是利用了强制类型转换,因此同样是类型不安全的。所以这种代码编译器会给一个警告。
- public static T getMiddle(T... a){returna[a.length/2];
- }
去除警告有两种途径:一种是在定义可变参数方法上(本例中的 getMiddle())加上
注解,另一种是在调用该方法时添加
- @SafeVarargs
注解。
- @SuppressWarnings("unchecked")
- T t = new T(); //error
- T.class.newInstance(); //error
- T.class; //error
解决办法是传入
参数,调用
- Class<T> t
。
- t.newInstance()
- public void sayHi(Class c){
- T t=null;try{
- t=c.newInstance();
- }catch(Exception e) {
- e.printStackTrace();
- }
- System.out.println("Hi "+t);
- }
- public class Singleton<T>{
- private staticT singleton;//error
- public staticTgetInstance(){}//error
- public static void print(T t){}//error}
但是,静态的泛型方法可以使用泛型类型:
- public static T getInstance() {
- return null;
- } //ok
- public static void print(T t) {} //ok
这个原因很多资料中都没说的太明白,说一下个人理解,仅供参考:
- 1. 泛型类中,<T>称为类型变量,实际上就相当于在类中隐形的定义了一个不可见的成员变量:`private T t;`,这是对象级别的,对于泛型类型变量来说是在对象初始化时才知道其具体类型的。而在静态域中,不需要对象初始化就可以调用,这是矛盾的。2. 静态的泛型方法,是在方法层面定义的,就是说在调用方法时,T所指的具体类型已经明确了。
Throwable 类不可以被继承,自然也不可能被
。
- catch
- public class GenericThrowable<T>extends Throwable{
- //The generic class GenericThrowable<T> may not subclass java.lang.Throwable}
但由于 Throwable 可以用在泛型类型参数中,因此可以变相的捕获泛型的 Throwable 对象。
- @Test
- public void testGenericThrowable(){
- GenericThrowable obj=newGenericThrowable();
- obj.doWork(newRuntimeException("why?"));
- }public static class GenericThrowable<T extends Throwable>{
- public void doWork(T t)throwsT{try{inti=3/0;
- }catch(Throwable cause){
- t.initCause(cause);throwt;
- }
- }
- }
这个能干什么?
- @Test
- public void testGenericThrowable(){
- GenericThrowable obj=newGenericThrowable();
- obj.doWork(newRuntimeException("What did you do?"));
- }public static class GenericThrowable<T extends Throwable>{
- public void doWork(T t)throwsT{try{
- Reader reader=newFileReader("notfound.txt");//这里应该是checked异常}catch(Throwable cause){
- t.initCause(cause);throwt;
- }
- }
- }
FileReader 实例化可能抛出已检查异常,jdk 中要求必须捕获或者抛出已检查异常。这种模式把它给隐藏了。也就是说可以消除已检查异常,有点不地道,颠覆了 java 异常处理的认知,后果不可预料,慎用。
定义一个普通的父类:
- packagecom.pollyduan.generic;public class Parent{
- public void setName(Object name) {
- System.out.println("Parent:"+ name);
- }
- }
那么继承一个子类,Son.java
- packagecom.pollyduan.generic;public class Son extends Parent{
- public void setName(String name) {
- System.out.println("son:"+ name);
- }public static void main(String[] args) {
- Son son=newSon();
- son.setName("abc");
- son.setName(newObject());
- }
- }
Son 类
了一个 setName(String) 方法,这没问题。输出:
- 重载
- son:abc
- Parent:java.lang.Object@6d06d69c
Parent 修改泛型类:
- packagecom.pollyduan.generic;public class Parent<T>{
- public void setName(T name) {
- System.out.println("Parent:"+ name);
- }
- }
从擦除的机制得知,擦除后的 class 文件为:
- packagecom.pollyduan.generic;public class Parent{
- public void setName(Object name) {
- System.out.println("Parent:"+ name);
- }
- }
这和最初的非泛型类是一样的,那么 Son 类修改为:
- packagecom.pollyduan.generic;public class Son extends Parent<String> {
- public void setName(String name) {
- System.out.println("son:"+ name);
- }public static void main(String[] args) {
- Son son=newSon();
- son.setName("abc");
- son.setName(newObject());//The method setName(String) in the type Son is not applicable for the arguments (Object)}
- }
发现重载无效了。这是泛型擦除造成的,无论是否在
是否标注为
- setName(String)
都将是重写,都不是重载。而且,即便你不写 setName(String) 方法,编译器已经默认重写了这个方法。
- @Override
换一个角度来考虑,定义 Son 时,Parent 已经明确了类型参数为 String,那么再写 setName(Stirng) 是重写,也是合理的。
- packagecom.pollyduan.generic;public class Son extends Parent<String> {
- public static void main(String[] args) {
- Son son=newSon();
- son.setName("abc");//ok}
- }
反编译会发现,编译器在内部编译了两个方法:
- public void setName(java.lang.String);public void setName(java.lang.Object);
虽然是 public 但编码时会发现不可见,它称为 "桥方法",它会重写父类的方法。
- setName(java.lang.Object)
- Son son=newSon();
- Parent p=son;
- p.setName(newObject());
强行调用会转换异常,也就证明了它实际上调用的是 son 的 setName(String)。
我非要重载怎么办?只能曲线救国,改个名字吧。
- public void setName2(String name) {
- System.out.println("son:"+ name);
- }
一个泛型类的类型参数不同,称之为泛型的不同参数化。
泛型有一个原则:一个类或类型变量不可成为两个不同参数化的接口类型的子类型。如:
- packagecom.pollyduan.generic;importjava.util.Comparator;public class Parent implements Comparator{
- @Override
- public int compare(Object o1, Object o2) {return 0;
- }
- }public class Son extends Parent implements Comparator{}
这样是没有问题的。如果增加了泛型参数化:
- packagecom.pollyduan.generic;importjava.util.Comparator;public class Parent implements Comparator<Parent>{
- @Override
- public int compare(Parent o1, Parent o2) {return 0;
- }
- }packagecom.pollyduan.generic;importjava.util.ArrayList;importjava.util.Comparator;public class Son extends Parent implements Comparator<Son> {
- //The interface Comparator cannot be implemented more than once with different arguments}
原因是 Son 实现了两次 Comparator,擦除后均为 Comparator,造成了冲突。
通配符是在泛型类使用时的一种机制,不能用在泛型定义时的泛型表达式中 (这是泛型类型参数限定符)。
如果 P 是 S 的超类,那么
就是
- Pair<S>
的子类型,通配符就是为了解决这个问题的。
- Pair<? extends P>
这称为子类型限定通配符,又称上边界通配符 (upper bound wildcard Generics),代表继承它的所有子类型,通配符匹配的类型不允许作为参数传入,只能作为返回值。
- public static void test1() {
- Parent bean1 = newParent();
- bean1.setName(123);
- Parent bean2 = bean1;
- Integer i =100;
- bean2.setName(i);// 编译错误Number s = bean2.getName();
- System.out.println(s);
- }
getName() 的合理性:
- 无论bean2指向的是任何类型的对象,只要是Number的子类型,都可以用Number类型变量接收。
为什么 setName(str) 会抛出异常呢?
- 1.Number> 表明了入参是Number的子类型;2.那么bean2 可以指向Parent<Integer>,也可以指向Parent,这都是符合规则的;3.再看setName(Number>),逻辑上传入Integer或者Double对象都是符合逻辑的;4.如果bean2指向的是Parent<Integer>,而传入的对象是Double的,两个看似合理的规则到一起就不行了。5.因此,jdk无法保证类型的安全性,干脆不允许这样——不允许泛型的子类型通配类型作为入参。
与之对应的是超类型 Pair
- public static void test2() {
- public static void test2() {
- Parent bean1 = new Parent();
- bean1.setName(123);
- Parentsuper Integer > bean2 = bean1;
- Integer i = 100;
- bean2.setName(i);
- Integer s = bean2.getName(); // 编译错误
- Object o = bean2.getName(); // ok
- System.out.println(o);
- }
- }
setName 的可行性:
- 1.无论bean2指向Parent<Number>,Parent<Integer>还是Parent<Object>都是允许的;2.都可以传入Integer或Integer的子类型。
getName 为毛报错?
- 1.由于限定类型的超类可能有很多,getName返回类型不可预知,如Integer 或其父类型Number/OtherParentClass...都无法保证类型检查的安全。2.但是由于Java的所有对象的顶级祖先类都是Object,因此可以用Object获取getName返回值。
就是
- Pair<?>
- Pair<? extends Object>
因此,无限定通配符可以作为返回值,不可做入参。
返回值只能保存在 Object 中。
和 P
- P<?>
可以调用 setter 方法,这是它和
- Pair
最重要的区别。
- Pair<?>
不等于
- P<?>
- P<Object>
是
- P<Object>
的子类。
- P<?>
- 1. 限定通配符总是包括自己;2. 子类型通配符:set方法受限,只可读,不可写;3. 超类型通配符:get方法受限,不可读(Object除外),只可写;4. 无限定通配符,只可读不可写;5. 如果你既想存,又想取,那就别用通配符;6. 不可同时声明子类型和超类型限定符,即extends和super只能出现一个。
通配符的受限只针对
和
- setter(T)
, 如果定义了一个
- T getter()
这种具体类型参数的方法,无限制。如:如果增加一个方法
- setter(Integer)
,可以任意调用。
- setId(Integer id)
通配符限定类中可以使用 T,编译器适配类型。
有一个键值对的泛型类:
- @Dataclass Pair {
- privateT key;privateT value;
- }
使用通配类型创建一个 swap 方法交换 key-value,交换时需要先使用一个临时变量保存一个字段:
- public static void swap(Pair p){// ? k=p.getKey();//error,?不可作为具体类型限定符Object k=p.getKey();//好吧,换成object,okp.setKey(p.getValue());//but,通配符类型不可做入参p.setValue(k);
- }
这里有一个办法解决它,再封装一个
:
- swapHelper()
- private static void swapHelper(Pair p){
- T k=p.getKey();
- p.setKey(p.getValue());
- p.setValue(k);
- }
- public static void swap(Pair p){
- swapHelper(p);
- }
这种方式,称为:通配符捕获,用一个
来捕获
- Pair<T>
中的类型。
- Pair<?>
注:
- 当然,你完全可以直接使用swapHelper,这里只是为了说明这样一种捕获机制。
- 只允许捕获单个、确定的类型,如:ArrayList<?>> 是无法使用 ArrayList<Pair<T>> 捕获的。
继承泛型类时,必须对父类中的类型参数进行初始化。或者说父类中的泛型参数必须在子类中可以确定具体类型。
例如:有一个泛型类
,那么
- Parent<T>
类定义时有两种方式初始化父类型的类型参数:
- Son
1 用具体类型初始化:
- public class Son extends Parent<String>{}
2 用子类中的泛型类型初始化父类:
- public class Son<T>extends Parent<T>{}
和
- Pair<P>
- Pair<S>
无论 P 和 S 有什么继承关系,一般
和
- Pair<P>
没什么关系。
- Pair<S>
- Pair s = new Pair < >();
- Pair p = s; //error
和
- Parent<T>
- Son<T>
泛型类自身可以继承其他类或实现接口,如 List 实现 ArrayList
泛型类可以扩展泛型类或接口,如 ArrayList 实现了 List,此时 ArrayList 可以转换为 List。这是安全的。
和
- Parent<T>
- Parent
随时都可以转换为原生类型
- Parent<T>
,但需要注意类型检查的安全性。
- Parent
- packagecom.pollyduan.generic;importjava.io.File;
- class Parent {
- privateT name;publicTgetName() {returnname;
- }public void setName(T name) {this.name = name;
- }public static void main(String[] args) {
- Parent p1=newParent<>();
- p1.setName("tom");
- System.out.println(p1.getName());
- Parent p2=p1;
- p2.setName(newFile("1.txt"));
- System.out.println(p2.getName());
- }
- }
运行没有异常,注意。
- Person<? extends XXX>
严格讲通配符限定的泛型对象不属于继承范畴,但使用中有类似继承的行为。
是
- Son
的子类型,那么
- Parent
就是
- Person<? extends Son>
的子类型。
- Person<? extends Parent>
等同于
- Person<? extends Object>
, 那么基于上以规则可以推断:
- Person<?>
是
- Person<? extends Parent>
的子类型。
- Person<?>
是
- Person<Object>
的子类型。
- Person<?>
有了泛型机制,jdk 的 reflect 包中增加了几个泛型有关的类:
- Class<T>.getGenericSuperclass()
- 获取泛型超类
- ParameterizedType
- 类型参数实体类
User.java
- packagecom.pollyduan.generic;@Data
- public class User{
- privateInteger id;privateString name;
- }
AbstractBaseDaoImpl.java
- packagecom.pollyduan.generic;public abstract class AbstractBaseDaoImpl<T> {
- public AbstractBaseDaoImpl() {
- Type t = getClass().getGenericSuperclass();
- System.out.println(t);
- }
- }
UserDaoImpl.java
- packagecom.pollyduan.generic;public
来源: http://blog.csdn.net/54powerman/article/details/70158788