一、什么是泛型
Java从1.5之后支持泛型,泛型的本质是类型参数,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
若不支持泛型,则表现为支持Object,不是特定的泛型。泛型是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。许多重要的类,比如集合框架,都已经成为泛型化的了。
二、泛型有什么优点
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,以提高代码的重用率。
1、类型安全
泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就无法落实到代码中,仅仅能停留在设计方案或者注释中。
2、消除强制类型转换
泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了强制转换代码和出错机会。
3、潜在的性能收益
泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。
三、泛型如何表示
我们在泛型中是用的T,E,K,V有什么区别呢,实际上使用大写字母A,B,C,D......X,Y,Z定义的,就都是泛型,把T换成A也一样,这里T只是名字上的意义而已,如:
- ? 表示不确定的java类型,类型是未知的。
- T (type) 表示具体的一个java类型,如果要定义超过两个,三个或三个以上的泛型参数可以使用T1, T2, ..., Tn
- K V (key value) 分别代表java键值中的Key Value
- E (element) 代表Element
- extends、super 泛型的参数类型可以使用extends、super语句,例如<T extends superclass>。习惯上称为“有界类型”。
四、泛型的原理
泛型是一种语法糖,泛型这种语法糖的基本原理是类型擦除,即编译器会在编译期间「擦除」泛型语法并相应的做出一些类型转换动作。例如:
public class Caculate<T> { private T num; }
我们定义了一个泛型类,定义了一个属性成员,该成员的类型是一个泛型类型,这个 T 具体是什么类型,我们也不知道,它只是用于限定类型的。反编译一下这个 Caculate 类:
public class Caculate{ public Caculate(){} private Object num; }
发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。
那么是不是所有的泛型类型都以 Object 进行擦除呢?大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型,如:
public class Caculate<T extends String> { private T num; }
这种情况的泛型类型,num 会被替换为 String 而不再是 Object。这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Caculate 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。
实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。
实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换。这一个过程就叫做『泛型翻译』。
五、泛型使用
泛型类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法,其中类和接口使用方式大致一致。
1、泛型类和接口
访问修饰符 class/interface 类名或接口名<限定类型变量名>
如泛型类和接口:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 //在实例化泛型类时,必须指定T的具体类型 public class Generic<T>{ //key这个成员变量的类型为T,T的类型由外部指定 private T key; public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定 this.key = key; } public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定 return key; } } //定义一个泛型接口 public interface Generator<T> { public T next(); }
注意当实现泛型接口的类,未传入泛型实参时:
/** * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中 * 即:class FruitGenerator<T> implements Generator<T>{ * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class" */ class FruitGenerator<T> implements Generator<T>{ @Override public T next() { return null; } }
当实现泛型接口的类,传入泛型实参时:
/** * 传入泛型实参时: * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T> * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。 */ public class FruitGenerator implements Generator<String> { private String[] fruits = new String[]{"Apple", "Banana", "Pear"}; @Override public String next() { Random rand = new Random(); return fruits[rand.nextInt(3)]; } }
2、泛型方法
泛型类/接口,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
方法并不一定依赖其外部的类或者接口,它可以独立存在,也可以依赖外围类存在。
public E get(int index) { rangeCheck(index); return elementData(index); }
ArrayList 的这个 get 方法就是一个泛型方法,它依赖外围 ArrayList 声明的 E 这个泛型类型,也就是它没有自己声明一个泛型类型而用的外围类的。当然,另一种方式就是自己申明一个泛型类型并使用:
/** * 泛型方法的基本介绍 * @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; }
如果既不依赖外围又未自己声明的话会:
// 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E" // 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。 public E setKey(E key){ this.key = keu }
3、通配符
通配符是用于解决泛型之间引用传递问题的特殊语法。如:
public static void main(String[] args){ Integer[] integerArr = new Integer[2]; Number[] numberArr = new Number[2]; numberArr = integerArr; ArrayList<Integer> integers = new ArrayList<>(); ArrayList<Number> numbers = new ArrayList<>(); numbers = integers;//编译不通过 }
Java 中,数组是协变的,即 Integer extends Number,那么子类数组实例是可以赋值给父类数组实例的。那是由于 Java 中的数组类型本质上会由虚拟机运行时动态生成一个类型,这个类型除了记录数组的必要属性,如长度,元素类型等,会有一个指针指向内存某个位置,这个位置就是该数组元素的起始位置。
所以子类数组实例赋值父类数组实例,只不过意味着父类数组实例的引用指向堆中子类数组而已,并不会有所冲突,因此是 Java 允许这种操作的。而泛型是不允许这么做的,为什么呢?我们假设泛型允许这种协变,看看会有什么问题。
ArrayList<Integer> integers = new ArrayList<>(); ArrayList<Number> numbers = new ArrayList<>(); numbers = integers;//假设的前提下,编译器是能通过的 numbers.add(23.5);
假设 Java 允许泛型协变,那么上述代码在编译器看来是没问题的,但运行时就会出现问题。这个 add 方法实际上就将一个浮点数放入了整型容器中了,虽然由于类型擦除并不会对程序运行造成问题,但显然违背了泛型的设计初衷,容易造成逻辑混乱,所以 Java 干脆禁止泛型协变。
所以虽然 ArrayList<Integer> 和 ArrayList<Number>编译器类型擦除之后都是 ArrayList 的实例,但是起码在编译器看来,这两者是两种不同的类型。那么,假如有某种需求,我们的方法既要支持子类泛型作为形参传入,也要支持父类泛型作为形参传入,又该怎么办呢?
我们使用通配符处理这样的需求,例如:
public void test1(ArrayList<? extends Number> list){}
// 或者
public void test2(自定义类<?> obj){}
如上ArrayList<? extends Number> 表示泛型类型具体是什么不知道,但是具体类型必须是 Number 及其子类类型。例如:ArrayList<Number>,ArrayList<Integer>,ArrayList<Double> 等。
但是,通配符往往用于方法的形参中,而不允许用于定义和调用语法中。因为? 代表不确定类型,通配符匹配出来的泛型类型只能读取,不能写。所以你只能读取里面的数据,不能瞎往里面添加元素。如:
ArrayList<Number> list = new ArrayList<>(); ArrayList<?> arrayList = list; // 后面的会编译报错 arrayList.add(32); arrayList.add("fadsf"); arrayList.add(new Object());
4、类型限定
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。为泛型添加上边界,即传入的类型实参必须是指定类型的子类型。如:<? super T> 与 <? extends T>、<T extends String>
如一个类:
public class Generic<T>{ private T key; public Generic(T key) { this.key = key; } public T getKey(){ return key; } public void showKeyValue1(Generic<? extends Number> obj){ Log.d("泛型测试","key value is " + obj.getKey()); } }
那么:
Generic<String> generic1 = new Generic<String>("11111"); Generic<Integer> generic2 = new Generic<Integer>(2222); Generic<Float> generic3 = new Generic<Float>(2.4f); Generic<Double> generic4 = new Generic<Double>(2.56); //这一行代码编译器会提示错误,因为String类型并不是Number类型的子类 //showKeyValue1(generic1); showKeyValue1(generic2); showKeyValue1(generic3); showKeyValue1(generic4);
如果改为:
public class Generic<T extends Number>{ private T key; public Generic(T key) { this.key = key; } public T getKey(){ return key; } public void showKeyValue1(Generic<? extends Number> obj){ Log.d("泛型测试","key value is " + obj.getKey()); } }
//这一行代码也会报错,因为String不是Number的子类 Generic<String> generic1 = new Generic<String>("11111");
在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加:
//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound" public <T extends Number> T showKeyName(Generic<T> container){ System.out.println("container key :" + container.getKey()); T test = container.getKey(); return test; }
六、泛型限制
1、静态属性不支持泛型,如:
private static T target; //编译报错
2、在类中的静态方法无法访问类上定义的泛型
如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class StaticGenerator<T> { /** * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法) * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。 * 如:public static void show(T t){..},此时编译器会提示错误信息: "StaticGenerator cannot be refrenced from static context" */ public static <T> void show(T t){} }
3、泛型的类型参数只能是类类型(包括自定义类),不能是简单类型,可使用其包装类型代替(如:Integer)。
4、不能使用泛型类异常
5、不能实例化泛型对象:如:T t = new T();
6、不能实例化泛型数组
经过查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。
也就是说下面的这个例子是不可以的:
List<String>[] ls = new ArrayList<String>[10];
而使用通配符创建泛型数组是可以的,如下面这个例子:
List<?>[] ls = new ArrayList<?>[10];
这样也是可以的:
List<String>[] ls = new ArrayList[10];
7、一个泛型类被其所有调用共享
List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass());
会返回true,因为一个泛型类的所有实例在运行时具有相同的运行时类(class),而不管他们的实际类型参数。事实上,泛型之所以叫泛型,就是因为它对所有其可能的类型参数,有同样的行为;同样的类可以被当作许多不同的类型。作为一个结果,类的静态变量和方法也在所有的实例间共享。这就是为什么在静态方法或静态初始化代码中或者在静态变量的声明和初始化时使用类型参数(类型参数是属于具体实例的)是不合法的原因。
8、不能对确切的泛型类型使用instanceof操作
泛型类被所有其实例(instances)共享的另一个暗示是检查一个实例是不是一个特定类型的泛型类是没有意义的。如:
Collection cs = new ArrayList<String>(); if (cs instanceof Collection<String>) { ...} // 非法