1. 概述
1.1 什么是泛型
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型 参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
1.2 泛型的优点
-
泛型的本质是为了参数化类型,也就是在在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型,很明显这种方法提高了代码的复用性。
-
泛型的引入提高了安全性,泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。。
-
在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。
那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。
public class GlmapperGeneric<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
public static void main(String[] args) {
// do nothing
}
/**
* 不指定类型
*/
public void noSpecifyType(){
GlmapperGeneric glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 需要强制类型转换
String test = (String) glmapperGeneric.get();
System.out.println(test);
}
/**
* 指定类型
*/
public void specifyType(){
GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 不需要强制类型转换
String test = glmapperGeneric.get();
System.out.println(test);
}
}
1.3 为什么提高了安全性?
再举例子说明一下
不安全举例
package keyAndDifficultPoints.Generic;
import java.util.ArrayList;
import java.util.List;
public class Test_Safe {
public static void main(String[] args) {
test();
}
public static void test() {
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for (int i = 0; i < arrayList.size(); i++) {
String s = (String) arrayList.get(i);
System.out.println(s);
}
}
}
结果:
aaaa
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at keyAndDifficultPoints.Generic.Test_Safe.test(Test_Safe.java:25)
at keyAndDifficultPoints.Generic.Test_Safe.main(Test_Safe.java:16)
很明显的一个类型转换错误。ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。
泛型提高安全性
将上面的代码稍微改一下
public static void test01(){
List<String> arrayList = new ArrayList<>();
arrayList.add("aaaa");
//下面代码编译时就直接报错了
arrayList.add(100);
for (int i = 0; i < arrayList.size(); i++) {
String s = (String) arrayList.get(i);
System.out.println(s);
}
}
通过泛型来提前检测类型,编译时就通不过。
2. 泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
2.1 一个普通的泛型类
public class Test_GenericClass {
public static void main(String[] args) {
test();
}
public static void test(){
/**
* 1、泛型的类型参数只能是类类型(包括自定义类),不能是简单数据类型(比如int,long这些)
* 2、传入的实参类型需与泛型的类型参数类型相同,即为这里的Integer。
* 3、new 后面的泛型参数可以省略
*/
Generic<Integer> genericInteger1 = new Generic<Integer>(123);
Generic<Integer> genericInteger = new Generic<>(123);
Generic<String> genericString = new Generic<String>("my");
System.out.println(genericInteger.getVar());
System.out.println(genericString.getVar());
}
}
/**
* 1、此处T虽然可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
* 但是为了代码的可读性一般来说:
* K,V用来表示键值对
* E是Element的缩写,常用来遍历时表示
* T就是Type的缩写,常用在普通泛型类上
* 2、还有一些不常见的U,R啥的
*/
class Generic<T> {
//key这个成员变量的类型为T,T的类型由外部指定
private T var;
public Generic(T var) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.var = var;
}
public T getVar() { //泛型方法getKey的返回值类型为T,T的类型由外部指定
return var;
}
}
class MyMap<K, V> { // 此处指定了两个泛型类型
private K key; // 此变量的类型由外部决定
private V value; // 此变量的类型由外部决定
public K getKey() {
return this.key;
}
public V getValue() {
return this.value;
}
public void setKey(K key) {
this.key = key;
}
public void setValue(V value) {
this.value = value;
}
};
结果:
123
my
Process finished with exit code 0
不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。
if(ex_num instanceof Generic<Number>){
}
因为编译后会进行泛型擦除,会被擦除掉
2.2 泛型与异常
//异常类不能声明为泛型类,编译报错
class MyException<T> extends Exception{
}
3. 泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:
interface Info<T>{ // 在接口上定义泛型
public T getVar() ; // 定义方法,方法的返回值就是泛型类型
}
当实现泛型接口的类,未传入泛型实参时:
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class InfoImpl<T> implements Info<T>
* 如果不声明泛型,如:class InfoImpl implements Info<T>,编译器会报错:"Unknown class"
*/
class InfoImpl<T> implements Info<T> { // 定义泛型接口的子类
private T var;
public InfoImpl(T var) {
this.setVar(var);
}
public void setVar(T var) {
this.var = var;
}
public T getVar() {
return this.var;
}
}
当实现泛型接口的类,传入泛型实参时:
/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Info<T>
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:InfoImpl01<T>,public String getVar();中的的T都要替换成传入的String类型。
*/
class InfoImpl01 implements Info<String> { // 定义泛型接口的子类
private String var;
public InfoImpl01(String var) {
this.setVar(var);
}
public void setVar(String var) {
this.var = var;
}
public String getVar() {
return this.var;
}
}
4. 泛型方法
4.1 简介
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
一个简单泛型方法
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
Object obj = genericMethod(Class.forName("com.test.test"));
4.2 泛型方法和泛型类的对比
public class GenericTest {
//这个类是个泛型类,在上面已经介绍过
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
public E setKey(E key){
this.key = keu
}
*/
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public <T> T showKeyName(Generic<E> container){
...
}
*/
/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
* 所以这也不是一个正确的泛型方法声明。
public void showkey(T genericObj){
}
*/
public static void main(String[] args) {
}
}
4.3 泛型类中的泛型方法
public class GenericFruit {
class Fruit{
@Override
public String toString() {
return "fruit";
}
}
class Apple extends Fruit{
@Override
public String toString() {
return "apple";
}
}
class Person{
@Override
public String toString() {
return "Person";
}
}
class GenerateTest<T>{
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
//apple是Fruit的子类,所以这里可以
generateTest.show_1(apple);
//编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
//generateTest.show_1(person);
//使用这两个方法都可以成功
generateTest.show_2(apple);
generateTest.show_2(person);
//使用这两个方法也都可以成功
generateTest.show_3(apple);
generateTest.show_3(person);
}
}
4.4 静态方法与泛型
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
5. 泛型通配符
5.1 出现原因
我们知道Ingeter是Number的一个子类,同时在特性章节中我们也验证过Generic与Generic实际上是相同的一种基本类型。那么问题来了,在使用Generic作为形参的方法中,能否使用Generic的实例传入呢?在逻辑上类似于Generic和Generic是否可以看成具有父子关系的泛型类型呢?
为了弄清楚这个问题,我们使用Generic这个泛型类继续看下面的例子:
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
showKeyValue(gNumber);
// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer>
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);
通过提示信息我们可以看到Generic不能被看作为`Generic的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic类型的类,这显然与java中的多台理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic和Generic父类的引用类型。由此类型通配符应运而生。
5.2 ? 无界通配符
5.2.1 基本用法
我们可以将上面的方法改一下:
public void showKeyValue1(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。重要说三遍!此处’?’是类型实参,而不是类型形参 ! 此处’?’是类型实参,而不是类型形参 !再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
5.2.2 ?通配符的继承
/*
2. 通配符的使用
通配符:?
类A是类B的父类,G<A>和G<B>是没有关系的,二者共同的父类是:G<?>
*/
@Test
public void test3() {
List<Object> list1 = null;
List<String> list2 = null;
List<?> list = null;
list = list1;
list = list2;
//编译通过
// print(list1);
// print(list2);
//
List<String> list3 = new ArrayList<>();
list3.add("AA");
list3.add("BB");
list3.add("CC");
list = list3;
//添加(写入):对于List<?>就不能向其内部添加数据。
//除了添加null之外。
// list.add("DD");
// list.add('?');
list.add(null);
//获取(读取):允许读取数据,读取的数据类型为Object。
Object o = list.get(0);
System.out.println(o);
}
5.2.3 ?与T的区别
I 通过T来确保泛型参数的一致性
import java.util.ArrayList;
import java.util.List;
public class Test_difference {
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>();
List<Float> floatList = new ArrayList<>();
//编译报错
// test(integerList, floatList);
//编译通过
test1(integerList, floatList);
//编译通过
test(integerList, integerList);
test1(integerList, integerList);
}
// 通过 T 来 确保 泛型参数的一致性
public static <T extends Number> void test(List<T> dest, List<T> src){
}
//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
public static void test1(List<? extends Number> dest, List<? extends Number> src){
}
}
II T可以通过&进行多重限定
public class Test_difference {
public static void main(String[] args) {
/*---------------------测试多重限定符---------------------*/
ArrayList list = new ArrayList<>();
ArrayDeque deque = new ArrayDeque<>();
LinkedList<Object> linkedList = new LinkedList<>();
//多重限定时,在编译的时候取最小范围或共同子类
test2(list);
// test3(list); 编译报错
//编译报错
// test2(deque);
// test3(deque);
//编译通过
test2(linkedList);
test3(linkedList);
}
//可以进行多重限定
public static <T extends List & Collection> void test2(T t) {
}
//可以进行多重限定
public static <T extends Queue & List> void test3(T t) {
}
//编译报错,无法进行多重限定
// public static <? extends List & Collection> void test4(List<T> dest, List<T> src){
//
// }
}
III ?通配符可以使用超类限定而T不行
类型参数 T 只具有 一种 类型限定方式:
T extends A
但是通配符 ? 可以进行 两种限定:
? extends A
? super A
5.3 泛型上下边界
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
5.3.1 上界通配符 < ? extends E>
上界:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:
- 如果传入的类型不是 E 或者 E 的子类,编译不成功
- 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用
5.3.2 下界通配符 < ? super E>
下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object
在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。
5.3.3 举例
/*
3.有限制条件的通配符的使用。
? extends A:
G<? extends A> 可以作为G<A>和G<B>的父类,其中B是A的子类
? super A:
G<? super A> 可以作为G<A>和G<B>的父类,其中B是A的父类
*/
@Test
public void test4() {
List<? extends Person> list1 = null; //[Person,无穷]
List<? super Person> list2 = null; //[-,Person]
List<Student> list3 = new ArrayList<Student>();
List<Person> list4 = new ArrayList<Person>();
List<Object> list5 = new ArrayList<Object>();
list1 = list3;
list1 = list4;
// list1 = list5;
// list2 = list3;
list2 = list4;
list2 = list5;
//下面的东西很奇怪
//读取数据:
list1 = list3;
Person p = list1.get(0);
//编译不通过
//Student s = list1.get(0);
list2 = list4;
Object obj = list2.get(0);
////编译不通过
// Person obj = list2.get(0);
//写入数据:
//编译不通过
// list1.add(new Student());
//编译通过
list2.add(new Person());
list2.add(new Student());
}
}
class Person {
}
class Student extends Person {
}
6. 泛型数组
在java中是**”不能创建一个确切的泛型类型的数组”**的。
也就是说下面的这个例子是不可以的:
List<String>[] ls = new ArrayList<String>[10];
而使用通配符创建泛型数组是可以的,如下面这个例子:
List<?>[] ls = new ArrayList<?>[10];
这样也是可以的:
List<String>[] ls = new ArrayList[10];
6.1 不允许原因
List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error: ClassCastException.
这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。
而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
6.2 通配符允许原因
下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK
7. 泛型与继承
/*
1. 泛型在继承方面的体现
虽然类A是类B的父类,但是G<A> 和G<B>二者不具备子父类关系,二者是并列关系。
补充:类A是类B的父类,A<G> 是 B<G> 的父类
*/
@Test
public void test1() {
/**
* 下面是有继承关系,所以可以赋值
*/
Object obj = null;
String str = null;
obj = str;
Object[] arr1 = null;
String[] arr2 = null;
arr1 = arr2;
/**
* 下面属于并列关系,无继承关系。无法赋值
*/
//编译不通过
// Date date = new Date();
// str = date;
List<Object> list1 = null;
List<String> list2 = new ArrayList<String>();
//此时的list1和list2的类型不具有子父类关系
//编译不通过
// list1 = list2;
/*
反证法:
假设list1 = list2;
list1.add(123);导致混入非String的数据。出错。
*/
}
@Test
public void test2() {
AbstractList<String> list1 = null;
List<String> list2 = null;
ArrayList<String> list3 = null;
list1 = list3;
list2 = list3;
List<String> list4 = new ArrayList<>();
}
ArrayList<Object> list1 = new ArrayList<Object>();
list1.add(new Object());
list1.add(new Object());
ArrayList<String> list2 = list1; //编译错误
实际上,在第4行代码的时候,就会有编译错误。那么,我们先假设它编译没错。那么当我们使用list2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的),可是它里面实际上已经被我们存放了Object类型的对象,这样就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。
ArrayList<String> list1 = new ArrayList<String>();
list1.add(new String());
list1.add(new String());
ArrayList<Object> list2 = list1; //编译错误
没错,这样的情况比第一种情况好的多,最起码,在我们用list2
取值的时候不会出现ClassCastException
,因为是从String
转换为Object
。可是,这样做有什么意义呢,泛型出现的原因,就是为了解决类型转换的问题。我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。再说,你如果又用list2
往里面add()
新的对象,那么到时候取得时候,我怎么知道我取出来的到底是String
类型的,还是Object
类型的呢?
8. 泛型与反射
public class Test_Reflect {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
A a = createInstance(A.class);
B b = createInstance(B.class);
}
/**
* 这样写明显是要安全很多的
*/
public static <T> T createInstance(Class<T> clazz) throws IllegalAccessException, InstantiationException {
return clazz.newInstance();
}
public static void getA(String path) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
A a = (A) Class.forName("keyAndDifficultPoints.Wildcard_Character.A").newInstance();
//很明显下面的这行代码是错的,但是写代码的时候你不知道path是哪个
// B b = (B)Class.forName("keyAndDifficultPoints.Wildcard_Character.A").newInstance();
System.out.println(a.toString());
}
}
class A {
String name;
@Override
public String toString() {
return "我是对象A";
}
}
class B {
String name;
@Override
public String toString() {
return "我是对象B";
}
}
class C {
//所以当不知道声明什么类型的 Class 的时候可以定义一 个Class<?>。
public Class<?> clazz1;
//因为T没有声明,所以编译报错
// public Class<T> clazz2;
}
class D<T> {
public Class<?> clazz;
// 不会报错
public Class<T> clazzT;
}
9. 泛型原理(泛型擦除)
9.1 类型擦除简介
Java的泛型是伪泛型,为什么说Java的泛型是伪泛型呢?因为在编译期间,所有的泛型信息都会被擦除掉,我们常称为泛型擦除。
Java中的泛型基本上都是在编译器这个层次来实现的,在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,编译器在编译的时候去掉,这个过程就称为类型擦除。
如在代码中定义的List<object>
和List<String>
等类型,在编译后都会变成List,JVM看到的只是List。而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。
可以通过两个例子,来证明java泛型的类型擦除。
9.1.1 原始类型相同
例1:
@Test
public void test() {
List<String> stringList = new ArrayList<String>();
stringList.add("my");
List<Integer> integerList = new ArrayList<Integer>();
integerList.add(123);
System.out.println(stringList.getClass() == integerList.getClass());
}
结果:
true
Process finished with exit code 0
在这个例子中,我们定义了两个List,不过一个是List泛型类型,只能存储字符串。一个是List泛型类型,只能存储整形。最后,我们通过stringList对象和integerList对象的getClass方法获取它们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下了原始类型。
9.1.2 通过反射添加其他类型元素
@Test
public void test01() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<Integer> list = new ArrayList<Integer>();
//这样调用add方法只能存储整形,因为泛型类型的实例为Integer
list.add(1);
//这样写编译就会报错
// list.add("my");
//通过反射的方式则可以存储String
list.getClass().getMethod("add", Object.class).invoke(list, "my");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
结果:
1
my
Process finished with exit code 0
在程序中定义了一个List泛型类型,如果直接调用add方法,那么只能存储整形的数据。不过当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了 原始类型。
9.2 类型擦除后保留的原始类型
在上面,几次提到了原始类型。什么是原始类型?
原始类型就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除,并使用其限定类型(无限定的变量用Object替换)。
例3:
public class Test_principle02 {
public static void main(String[] args) {
}
}
class Test_Generic<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
下面我们用IDEA的工具,查看这个类的字节码信息。我把完整的字节码复制在下方:
// class version 52.0 (52)
// access flags 0x20
// signature <T:Ljava/lang/Object;>Ljava/lang/Object;
// declaration: keyAndDifficultPoints/principle/Test_Generic<T>
class keyAndDifficultPoints/principle/Test_Generic {
// compiled from: Test_principle02.java
// access flags 0x2
// signature TT;
// declaration: T
private Ljava/lang/Object; value
// access flags 0x0
<init>()V
L0
LINENUMBER 13 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_Generic; L0 L1 0
// signature LkeyAndDifficultPoints/principle/Test_Generic<TT;>;
// declaration: keyAndDifficultPoints.principle.Test_Generic<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature ()TT;
// declaration: T getValue()
public getValue()Ljava/lang/Object;
L0
LINENUMBER 17 L0
ALOAD 0
GETFIELD keyAndDifficultPoints/principle/Test_Generic.value : Ljava/lang/Object;
ARETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_Generic; L0 L1 0
// signature LkeyAndDifficultPoints/principle/Test_Generic<TT;>;
// declaration: keyAndDifficultPoints.principle.Test_Generic<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature (TT;)V
// declaration: void setValue(T)
public setValue(Ljava/lang/Object;)V
L0
LINENUMBER 21 L0
ALOAD 0
ALOAD 1
PUTFIELD keyAndDifficultPoints/principle/Test_Generic.value : Ljava/lang/Object;
L1
LINENUMBER 22 L1
RETURN
L2
LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_Generic; L0 L2 0
// signature LkeyAndDifficultPoints/principle/Test_Generic<TT;>;
// declaration: keyAndDifficultPoints.principle.Test_Generic<T>
LOCALVARIABLE value Ljava/lang/Object; L0 L2 1
// signature TT;
// declaration: T
MAXSTACK = 2
MAXLOCALS = 2
}
可以明显的看到泛型T被替换成了Object。
因为在Test_Generic中,T是一个无限定的类型变量,所以用Object替换。其结果就是一个普通的类,如同泛型加入java变成语言之前已经实现的那样。
在程序中可以包含不同类型的Test_Generic,如Test_Generic或Test_Generic,但是,擦除类型后它们就成为原始的Test_Generic类型了,原始类型都是Object。
从上面的那个例2中,我们也可以明白List被擦除类型后,原始类型也变成了Object,所以通过反射我们就可以存储字符串了。
2、如果类型变量有限定,那么原始类型就用第一个边界的类型变量来替换。
比如Test_Generic这样声明
class Test_Generic1<T extends List & Collection>
我们还是看字节码(后面如无必须,只截取部分字节码)
// class version 52.0 (52)
// access flags 0x20
// signature <T::Ljava/util/List;:Ljava/util/Collection;>Ljava/lang/Object;
// declaration: keyAndDifficultPoints/principle/Test_Generic1<T extends java.util.List, java.util.Collection>
class keyAndDifficultPoints/principle/Test_Generic1 {
// compiled from: Test_principle03.java
// access flags 0x2
// signature TT;
// declaration: T
private Ljava/util/List; value
会发现T变成了List
如果顺序变一下
class Test_Generic1<T extends Collection & List>
字节码就变了
T变成了Collection
// class version 52.0 (52)
// access flags 0x20
// signature <T::Ljava/util/Collection;:Ljava/util/List;>Ljava/lang/Object;
// declaration: keyAndDifficultPoints/principle/Test_Generic1<T extends
java.util.Collection, java.util.List>
class keyAndDifficultPoints/principle/Test_Generic1 {
// compiled from: Test_principle03.java
// access flags 0x2
// signature TT;
// declaration: T
private Ljava/util/Collection; value
也就是说在进行字节码编译的时候是使用离T最近的一个类型。
9.2.1 解答一个疑惑
在上文说到&的多重限定时
public class Test_difference {
public static void main(String[] args) {
/*---------------------测试多重限定符---------------------*/
ArrayList list = new ArrayList<>();
ArrayDeque deque = new ArrayDeque<>();
LinkedList<Object> linkedList = new LinkedList<>();
//多重限定时,在编译的时候取最小范围或共同子类
test2(list);
// test3(list); 编译报错
//编译报错
// test2(deque);
// test3(deque);
//编译通过
test2(linkedList);
test3(linkedList);
}
//可以进行多重限定
public static <T extends List & Collection> void test2(T t) {
}
//可以进行多重限定
public static <T extends Queue & List> void test3(T t) {
}
//编译报错,无法进行多重限定
// public static <? extends List & Collection> void test4(List<T> dest, List<T> src){
//
// }
}
test4()
方法里离T最近的是Collection
,那么T在编译后就被Collection
代替了。那按理来说
test4(queue);
1、这里我们传一个Collection的实现类Queue,也应该是可以的啊,但是为什么报错了呢?注意一点报错报的是编译错误,泛型提供编译前检测机制,也就是说在没运行前,泛型规定了多重限定时,在编译的时候取最小范围或共同子类
。
2、那实际上到底可以不可以传Queue呢?根据之前的讲解,我相信大家已经有了结论。实际上是可以的,只不过要跳过编译检测机制,通过反射来放Queue。
9.2.2泛型方法调用
在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。
class Test {
public static void main(String[] args) {
//不指定泛型的时候 i
nt a1 = add(1, 2); //这两个参数都是Integer,所以T为Integer类型
Number b1 = add(1, 1.2);//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number
Object c1 = add(1, "my");//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object
//指定泛型的时候
int a = Test.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类//
int b = Test.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float
Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
}
//这是一个简单的泛型方法
public static <T> T add(T x, T y)
{ return x;
}
}
9.3类型擦除引起的问题及解决方法
9.3.1 类型检测针对谁?
public static void main(String[] args)
{
ArrayList<String> arrayList=new ArrayList<String>();
arrayList.add("123");
arrayList.add(123);//编译错误
}
类型擦除后,原始类型为Object,是应该运行任意引用类型的添加的。可实际上却不是这样,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。
那么,这么类型检查是针对谁的呢?我们来看例子:
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList();
list1.add("1"); //编译通过
list1.add(1); //编译错误
String str1 = list1.get(0); //返回类型就是String
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //编译通过
list2.add(1); //编译通过
Object object = list2.get(0); //返回类型就是Object
new ArrayList<String>().add("11"); //编译通过
new ArrayList<String>().add(22); //编译错误
String str2 = new ArrayList<String>().get(0); //返回类型就是String
}
通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
9.3.2 自动类型转换
因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?
我么来看一下List的get()方法:
public E get(int index)
{
rangeCheck(index);
return elementData(index);
}
E elementData(int index)
{
return (E) elementData[index];
}
可以看到基本各个类都已经自动帮你转了。
9.3.3 类型擦除与多态的冲突和解决方法
这个其实是类型擦除引起的最大的问题了。
public class Test_principle05
{
public static void main(String[] args) {
}
}
class Generic<T> {
//key这个成员变量的类型为T,T的类型由外部指定
private T var;
public T getVar() {
return var;
}
public void setVar(T var) {
this.var = var;
}
}
class MyGeneric extends Generic<Integer>{
@Override
public Integer getVar() {
return super.getVar();
}
@Override
public void setVar(Integer var) {
super.setVar(var);
}
}
实际上,从他们的@Override标签中也可以看到,在子类中重写这两个方法一点问题也没有,实际上是这样的吗?
分析:
泛型擦除后,父类是下面这样子
class Generic {
//key这个成员变量的类型为T,T的类型由外部指定
private Object var;
public Object getVar() {
return var;
}
public void setVar(Object var) {
this.var = var;
}
}
子类还是这样
class MyGeneric extends Generic<Integer>{
@Override
public Integer getVar() {
return super.getVar();
}
@Override
public void setVar(Integer var) {
super.setVar(var);
}
}
先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。
重载(Overload):首先是位于一个类之中或者其子类中,具有相同的方法名,但是方法的参数不同,返回值类型可以相同也可以不同。
(1):方法名必须相同。
(2):方法的参数列表一定不一样。
(3):访问修饰符和返回值类型可以相同也可以不同。
重写(override):一般都是表示子类和父类之间的关系,其主要的特征是:方法名相同,参数相同,但是具体的实现不同。
重写的特征:
(1):方法名必须相同,返回值类型必须相同
(2):参数列表必须相同
(3):访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
(4):子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
(5):构造方法不能被重写
我们来测试下到底是重载还是重写
public static void main(String[] args) {
MyGeneric myGeneric = new MyGeneric();
myGeneric.setVar(new Integer(1));
myGeneric.setVar(new Object());//编译错误
}
如果是重载的话,第四行代码是不会报错的,因为调的是不同的重载方法。但是发现编译报错了,也就是说没有参数是Object的这样的重载函数。所以说是重写了,导致MyGeneric对象只能调用自己重写的方法。
为什么会这样呢?
原因是这样的,我们传入父类的泛型类型是Integer,Generic,我们的本意是将泛型类变为如下:
class Generic {
//key这个成员变量的类型为T,T的类型由外部指定
private Integer var;
public Integer getVar() {
return var;
}
public void setVar(Integer var) {
this.var = var;
}
}
然后再子类中重写参数类型为Date的那两个方法,实现继承中的多态。
可是由于种种原因,虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。JVM知道你的本意吗?知道,可是它能直接实现吗,不能。如果真的不能的话,那我们怎么去重写我们想要的Integer类型参数的方法啊。
JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。
我们对下面这个类进行编译,看其字节码
public class MyGeneric extends Generic<Integer> {
public static void main(String[] args) {
}
@Override
public Integer getVar() {
return super.getVar();
}
@Override
public void setVar(Integer var) {
super.setVar(var);
}
}
字节码:
// class version 52.0 (52)
// access flags 0x21
// signature LkeyAndDifficultPoints/principle/Generic<Ljava/lang/Integer;>;
// declaration: keyAndDifficultPoints/principle/MyGeneric extends keyAndDifficultPoints.principle.Generic<java.lang.Integer>
public class keyAndDifficultPoints/principle/MyGeneric extends keyAndDifficultPoints/principle/Generic {
// compiled from: MyGeneric.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 9 L0
ALOAD 0
INVOKESPECIAL keyAndDifficultPoints/principle/Generic.<init> ()V
RETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 12 L0
RETURN
L1
LOCALVARIABLE args [Ljava/lang/String; L0 L1 0
MAXSTACK = 0
MAXLOCALS = 1
// access flags 0x1
public getVar()Ljava/lang/Integer; //这是我们重写的getVar()方法
L0
LINENUMBER 16 L0
ALOAD 0
INVOKESPECIAL keyAndDifficultPoints/principle/Generic.getVar ()Ljava/lang/Object;
CHECKCAST java/lang/Integer
ARETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public setVar(Ljava/lang/Integer;)V ////这是我们重写的setVar()方法
L0
LINENUMBER 20 L0
ALOAD 0
ALOAD 1
INVOKESPECIAL keyAndDifficultPoints/principle/Generic.setVar (Ljava/lang/Object;)V
L1
LINENUMBER 21 L1
RETURN
L2
LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L2 0
LOCALVARIABLE var Ljava/lang/Integer; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1041
public synthetic bridge setVar(Ljava/lang/Object;)V //编译时由编译器生成的桥方法
L0
LINENUMBER 9 L0
ALOAD 0
ALOAD 1
CHECKCAST java/lang/Integer
INVOKEVIRTUAL keyAndDifficultPoints/principle/MyGeneric.setVar (Ljava/lang/Integer;)V
RETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1041
public synthetic bridge getVar()Ljava/lang/Object; //编译时由编译器生成的桥方法
L0
LINENUMBER 9 L0
ALOAD 0
INVOKEVIRTUAL keyAndDifficultPoints/principle/MyGeneric.getVar ()Ljava/lang/Integer;
ARETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}
从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法。最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
所以,虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。
并且,还有一点也许会有疑问,子类中的桥方法Object getValue()
和Date getValue()
是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。
10. 类型参数的推断
编译器判断泛型方法的实际类型参数的过程称为类型推断。
-
当某个类型变量只在整个参数列表的所有参数和返回值中的一处被应用了,那么根据调用方法时该处的实际应用类型来确定。即直接根据调用方法时传递的参数类型或返回值来决定泛型参数的类型。
例如:swap(new String[3],1,2) -> static <E> void swap(E[]a,int i,int j)
-
当某个类型变量在整个参数列表的所有参数和返回值中的多处被应用了,如果调用方法时这么多处的实际应用类型都 对应同一种类型,则泛型参数的类型就是该类型。
例如:add(3,5) -> static <T> T add(T a,T b)
-
当某个类型变量在整个参数列表的所有参数和返回值中的多处被应用了,如果调用方法时这么多处的实际应用类型 对应不同的类型,且没有返回值*,则取多个参数中的最大交集类型,即第一个公共父类。
例如:
fill(new Integer[3],3.5) -> static <T> void fill(T a[],T v)
该例子实际对应的类型就是Number,编译通过,运行出问题。
- 当某个类型变量在整个参数列表的所有参数和返回值中的多处被应用了,如果调用方法时这么多处的实际应用类型对应不同的类型,且使用有返回值,则优先考虑返回值的类型
例如:
int x = add(3,3.5) -> static <T> T add(T a,T b)
上例编译报错,x类型改为float也报错,改为Number成功。
- 参数类型的类型推断具有传递性
例子:
copy(new Integer[5],new String[5]) -> static <T> void copy(T []a,T []b)
该例推断实际参数类型为Object,编译通过.
copy(new ArrayList<String>,new Integer[5]) -> static <T> void copy(Collection<T>a,T[]b)
该例则根据参数化的ArrayList类实例将类型变量直接确定为String类型,编译报错。
11. 杂项
- 类型推断只对赋值操作有效,其他时候并不起作用
- 泛型方法可以显式指明类型
New.<x,x>f()
- 可以声明ArrayList.class 但是不能声明ArrayList.class
- 静态变量是被泛型类的所有实例所共享的。对于声明为
MyClass<T>
的类,访问其中的静态变量的方法仍然是MyClass.myStaticVar
。不管是通过new MyClass<String>
还是new MyClass<Integer>
创建的对象,都是共享一个静态变量。 - 泛型的类型参数不能用在
Java
异常处理的catch
语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM
是无法区分两个异常类型MyException<String>
和MyException<Integer>
的。对于JVM
来说,它们都是MyException
类型的。也就无法执行与异常对应的catch
语句。