前言
当我们在阅读源码的时候通常会看到如下所示代码中存在“E”、“T”或“?”,那么,这些大写字母到底有着怎样的含义呢?接下来我们具体讨论一下。
publicinterfaceEnumeration<E> { /*** Tests if this enumeration contains more elements.** @return <code>true</code> if and only if this enumeration object* contains at least one more element to provide;* <code>false</code> otherwise.*/booleanhasMoreElements(); /*** Returns the next element of this enumeration if this enumeration* object has at least one more element to provide.** @return the next element of this enumeration.* @exception NoSuchElementException if no more elements exist.*/EnextElement(); }
一、什么是泛型?
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。
可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。
1.1 泛型的语法规则
- a、所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前;
eg 1: 比如说这是一个用来打印数组的泛型方法:
privatestatic<E>voidprintArray(E[] inputArray)
- b、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符;
eg 2: 比如下面这个方法:
privatestatic<E,T>voidprintArray(E[] inputArray, Tdata)
- c、类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符;
- d、泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(int double char等);
- e、泛型的参数类型可以使用extends语句,例如
<T extends superclass>
。
1.2 泛型的通配符
其实,Java 中的 T,E,K,V,?本质上都是通配符,常用于泛型定义的时候,E、T、K、V、N 等这些字母之间没什么区别,使用T的地方完全可以换成 U、S、Z 等任意字母。当然,一般我们会使用一些常用的字母,这些字符一般是一些类型的缩写,约定的定义如下:
1、T (type) 表⽰具体的⼀个 java 类型;
2、K、V (key value) 分别代表 java 键值中的 Key 和 Value;
3、E (element) 代表 Element;
4、N Number 代表数值类型;
5、?表示不确定的 java 类型;
1.3 泛型类
泛型类的定义格式:
格式:
修饰符 class 类名<类型>{}
范例:
public class Generic<T>{}
此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型.
publicclassDemo<T> { privateTt; publicvoidsetT(Tt){ this.t=t; } publicTgetT(){ returnt; } }
1.4 泛型方法
- 格式:
修饰符<类型> 返回值类型方法名(类型 变量名){}
- 范例:
public <T> void show(T t){}
publicclassDemo<T> { publicvoidshow(Tt){ System.out.println(t); } }
1.5 泛型接口
- 格式:
修饰符 interface 接口名<类型>{ }
- 范例:
public interface Generic<T>{ }
定义接口:
publicinterfaceDemo<T> { voidshow(Tt); }
定义接口实现类:
publicclassDemoImlp<T>implementsDemo<T> { publicvoidshow(Tt){ System.out.println(t); } }
1.6 可变参数
1.6.1 定义格式
- 格式:
修饰符 返回值类型 方法名(数据类型... a){ }
- 范例:
public static int sum(int... a){ }
1.6.2 示例
publicclassDemo { publicstaticvoidmain(String[] args) { System.out.println(sum(10,20)); System.out.println(sum(10,20,30)); System.out.println(sum(10,20,30,40)); System.out.println(sum(10,20,30,40,50)); System.out.println(sum(10,20,30,40,50,60)); System.out.println(sum(10,20,30,40,50,60,70)); } publicstaticintsum(int... a){//a其实是一个数组,用来存储参数intsum=0; for(inti:a){ sum+=i; } returnsum; } }
若可变参数与不可变参数在同一个方法定义中出现,则不可变参数要放置在第一个参数位置,例如:
publicstaticintfun1(int... a,intb){//报错 ...... } publicstaticintfun2(intb,int... a){//正常 ...... }
1.6.3 可变参数在实际中的应用
Arrays工具类中有一个静态方法:
- public static List asList(T… a):返回由指定数组支持的固定大小的列表
注意
:返回的集合不能做增删操作,可以做修改操作
List接口中有一个静态方法:
- public static List of(E… element):返回包含任意数量元素的不可变列表
注意
:返回的集合不能做增删改操作
Set接口中有一个静态方法:
- public static Set of(E… elements):返回一个包含任意数量元素的不可变集合
注意
:
a、在给元素的时候,不能给重复的元素;
b、返回的集合不能做增删操作,没有修改的方法;
二、泛型的好处
**1,类型安全
泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
2,消除强制类型转换
泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
3,潜在的性能收益
泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
三、通配符
3.1 最常用的 T,E,K,V 和 ?
T,E,K,V,?是最常用的通配符,也是最容易理解的,它们是这样约定的:
?表示不确定的java类型T (type) 表示具体的一个java类型KV (keyvalue) 分别代表java键值中的KeyValueE (element) 代表Element
3.2 "?" 无界通配符
有一个父类 Animal 和几个子类,如狗、猫等,现在我需要一个动物的列表,第一个想法是像这样的:
List<Animal>animalsList
但是老板的想法确实这样的:
List<?extendsAnimal>animalsList
为什么要使用通配符而不是简单的泛型呢?通配符其实在声明局部变量时是没有什么意义的,但是当你为一个方法声明一个参数时,它是非常重要的。
/** 统计几条腿*/staticintcountLegs1 (List<?extendsAnimal>animals ) { intretVal=0; for ( Animalanimal : animals ) { retVal+=animal.countLegs1(); } returnretVal; } /** 统计几条腿*/staticintcountLegs2 (List<Animal>animals ){ intretVal=0; for ( Animalanimal : animals ) { retVal+=animal.countLegs2(); } returnretVal; } publicstaticvoidmain(String[] args) { List<Dog>dogs=newArrayList<>(); // 不会报错countLegs( dogs ); // 报错countLegs1(dogs); }
所以总结如下:
对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。像 countLegs1 方法中,限定了上界,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs2 就不行,当调用 countLegs2 时,程序会报
类型转换异常
。
3.3 ?extends T 上界通配符
上界通配符:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:
- 如果传入的类型不是 E 或者 E 的子类,编译不成功
- 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用
private<KextendsA, EextendsB>Etest(Karg1, Earg2){ Eresult=arg2; arg2.compareTo(arg1); //.....returnresult; }
类型参数列表中如果有多个类型参数上限,用逗号分开
3.4 ? super T 下界通配符
下界通配符: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object
在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。
private<T>voidtest(List<?superT>a, List<T>b){ for (Tt : b) { a.add(t); } } publicstaticvoidmain(String[] args) { List<Dog>dogs=newArrayList<>(); List<Animal>animals=newArrayList<>(); newTest3().test(animals,dogs); } // Dog 是 Animal 的子类classDogextendsAnimal { }
a 类型 “大于等于” b 的类型,这里的“大于等于”是指 a 表示的范围比 b 要大,因此装得下 a 的容器也就能装 b。
3.5 T 和 ?的区别
3.5.1 通过T来确保泛型参数的一致性
// 通过 T 来 确保 泛型参数的一致性public<TextendsNumber>voidtest(List<T>dest, List<T>src) //通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型publicvoidtest(List<?extendsNumber>dest, List<?extendsNumber>src)
像下面的代码中,约定的 T 是 Number 的子类才可以,但是申明时是用的 String ,所以就会飘红报错。
publicstaticvoidmain() { Demo<String>demo=newDemo<>(); List<String>a=newArrayList<>(); List<String>b=newArrayList<>(); demo.test(a,b); } /*** 通过T来确保泛型参数的一致性*/public<TextendsNumbers>voidtest(List<T>a,List<T>b) { System.out.println(a); System.out.println(b); }
不能保证两个 List 具有相同的元素类型的情况
Demodemo=newDemo<>(); List<String>a=newArrayList<>(); List<Number>b=newArrayList<>(); demo.testNon(a,b);
上面的代码在编译器并不会报错,但是当进入到testNon
方法内部操作时(比如赋值),对于a
和b
而言,就还是需要进行类型转换。
3.5.2 类型参数可以多重限定而通配符不行
publiccalssDemoimplementsDemoInterfaceA,DemoInterfaceB { /*** 使用“&”符号设定多重边界 (Multi Bounds)*/publicstatic<TextendsDemoInterfaceA&DemoInterfaceB>voidtest(Tt){ } } /** 接口A*/interfaceDemoInterfaceA {} /** 接口B*/interfaceDemoInterfaceB {}
使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 DemoInterfaceA 和 DemoInterfaceB的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。
3.5.3 通配符可以使用超类限定而类型参数不行
类型参数 T 只具有一种类型限定方式:
TextendsA
但是通配符 ? 可以进行两种限定:
? extendsA
? superA
3.5.4 注意事项
// 指定结合元素只能用 T 类型List<T>list=newArrayList<T>(); // 集合元素可以是任意类型,这种没哟意义,一般方法中,只是为了说明用法List<?>list=newArrayList<?>();
?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :
// 可以Tt=operate(); // 不可以?car=operate();
简单总结下:
T 是一个确定的类型,通常用于泛型类和泛型方法的定义,?是一个不确定的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。
3.6 T 和 Object 的区别
Object 是Java的超类(所有对象的父类),在编码过程中就难免出现类型转化问题,且在编译阶段不会报错,到了运行阶段才暴露问题,大大降低了程序的安全性和健壮性!
3.6.1 转型的分类
- 向上转型 -> 用父类声明一个子类对象
实例:
Animalcat=newCat();
- 向下转型 -> 将父类对象强转为其子类
实例:
Animalcat=newCat(); CatanotherCat= (Cat) cat;
3.6.2 类型转换的问题
当我们使用Object作为泛型来使用时,不仅写起来麻烦,还要不停的进行类型转化,还很容易出现问题,如下实例:
publicstaticvoidmain(String[] args) { Objectnumber1=1; Integernumber2= (Integer) number1; Strings= (String) number1;//报错}
程序运行起来就会报类型转换异常
java.lang.Integercannotbecasttojava.lang.String
3.7 Class<T> 和 Class<?> 区别
Class<T>
和 Class<?>
最常见的是在反射场景下的使用,这里以用一段发射的代码来说明下。
// 通过反射的方式生成 demoA // 对象,这里比较明显的是,我们需要使用强制类型转换DemoAdemoA= (DemoA) Class.forName("com.xiaoliucoding.test.DemoA").newInstance();
对于上述代码,在运行期,如果反射的类型不是DemoA
类,那么一定会报 java.lang.ClassCastException
错误。
对于这种情况,则可以使用下面的代码来代替,使得在在编译期就能直接 检查到类型的问题:
publicclassTest3 { publicstatic<T>TcreateInstance(Class<T>clazz) throwsIllegalAccessException,InstantiationException { returnclazz.newInstance(); } publicstaticvoidmain(String[] args) throwsIllegalAccessException, InstantiationException { Aa=createInstance(A.class); Bb=createInstance(B.class); } } /** A*/classA {} /** B*/classB {}
Class<T>
在实例化的时候,T
要替换成具体类。Class<?>
它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况。比如,我们可以这样做申明:
// 可以publicClass<?>clazz; // 不可以,因为 T 需要指定类型publicClass<T>clazzT;
所以总结如下:
当不知道定声明什么类型的
Class
的时候可以定义一 个Class<?>
,如果已经明确要反射的类型,必须让当前的类也指定T
四、泛型的PECS原则
PECS 是 Producer Extends,Consumer Super 的缩写.
4.1 Producer Extends
现在要扩展Stack的功能,增加一个pushAll方法,我们第一版的实现可能是这样的。
publicvoidpushAll(Iterable<E>source) { for (Ee : source) { push(e); } }
上面的写法在使用时可能会报函数要求的参数类型与提供的不一致
问题,如下图所示。
publicstaticvoidmain(String[] args) { Stack<Number>statck=newStack<>(); Iterable<Integer>integers=newArrayList<>(); statck.pushAll(integers);// 报错}
为了解决这个问题,我们需要做出如下修改,也就是把参数类型改成Iterable<? extends E>
,这是一种有限制的通配符类型(bounded wildcard type
),意思是"E的某个子类型的Iterable
接口"。pushAll
的source
参数产生E实例供Stack使用,也就是source
是生产者,因此source的类型是Iterable<? extends E>
。
publicvoidpushAll(Iterable<?extendsE>source) { for (Ee : source) { push(e); } }
4.2 Consumer Super
现在要扩展Stack的功能,增加一个与pushAll对应的popAll方法,我们第一版的实现可能是这样的。
publicvoidpopAll(Collection<E>destination) { while (!isEmpty()) { destination.add(pop()); } }
上面的写法在使用时可能会报函数要求的参数类型与提供的不一致
问题,如下代码所示:
publicstaticvoidmain(String[] args) { Stack<Number>statck=newStack<>(); Collection<Object>objects=newArrayList<>(); statck.popAll(objects);// 报错}
为了解决这个问题,我们需要做出如下修改,也就是把参数类型改成Collection<? super E>
,意思是"E的某种超类
集合”。popAll的destination参数通过Stack消费E实例,也就是destination
是消费者,因此destination
的类型是Collection<? super E>
,如下所示。
publicvoidpopAll(Collection<?superE>destination) { while (!isEmpty()) { destination.add(pop()); } }
4.3 PECS原则总结
从上述 4.1 和 4.2 两方面的分析,总结 PECS 原则如下:
- 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
- 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
- 如果既要存又要取,那么就不要使用任何通配符。
总结
这篇文章我们从java源码中的泛型通配符出发,介绍了什么是泛型、使用泛型的好处、泛型的通配符区别及含义,以及泛型的PECS原则,如有疏漏及错误,烦请留言补充。
参考
- 感谢前人的经验、分享和付出,让我们可以有机会站在巨人的肩膀上眺望星辰大海!