Java–泛型机制
一、为什么要引入泛型
很简单,一句话,参数化类型,代码复用。例如如下例子:
private static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
如果没有泛型,每种数据类型都要加载一个add方法,通过泛型,我们只需要一个方法便可概括:
private static <T extends Number> T add(T a, T b) {
if (a instanceof Integer) {
return (T) Integer.valueOf(a.intValue() + b.intValue());
} else if (a instanceof Double) {
return (T) Double.valueOf(a.doubleValue() + b.doubleValue());
} else if (a instanceof Float) {
return (T) Float.valueOf(a.floatValue() + b.floatValue());
} else if (a instanceof Long) {
return (T) Long.valueOf(a.longValue() + b.longValue());
} else if (a instanceof Short) {
return (T) Short.valueOf((short) (a.shortValue() + b.shortValue()));
} else if (a instanceof Byte) {
return (T) Byte.valueOf((byte) (a.byteValue() + b.byteValue()));
} else {
throw new IllegalArgumentException("Unsupported number type");
}
}
二、泛型基本使用
1、泛型类
简单泛型类
package com.java.basic;
public class Generic {
static class Point<T>{
private T var;
public T getVar() {
return var;
}
public void setVar(T var) {
this.var = var;
}
}
public static void main(String[] args) {
Point p = new Point();
p.setVar("test");
System.out.println(p.getVar());
}
}
多元泛型
package com.java.basic;
public class Generic {
static class Notepad<K,V>{
private K key;
private K value;
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public K getValue() {
return value;
}
public void setValue(K value) {
this.value = value;
}
}
public static void main(String[] args) {
Notepad n = new Notepad();
n.setKey("key");
n.setValue("value");
System.out.println("key is:"+n.getKey()+" and value is:"+n.getValue());
}
}
2、泛型方法
package com.java.basic;
public class Generic {
//泛型方法
public <T> T getObject(Class<T> c) throws InstantiationException, IllegalAccessException {
T t = c.newInstance();
return t;
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Generic g = new Generic();
//返回Generic的对象
Object x = g.getObject(Class.forName("com.java.basic.Generic"));
System.out.println(x);
}
}
定义泛型方法时,必须在返回值前面加一个,用来声明这是一个泛型方法;
Class<T>
的作用就是指明泛型的具体类型,而Class<T>
类型的变量c,可以用来创建泛型类的对象。为什么要用变量c来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c的newInstance方法去创建对象,也就是利用反射创建对象。
3、泛型的上下限
Java采用的是所谓的“擦除”机制。在编译时不考虑传入的类型,把该类型的印迹全部擦除,视该类型为最基本的Object类型。但是Java会在编译时检查泛型接口传入的类型参数的实例是否存在隐含的转换情况,为了安全,泛型禁止此类转换。这就是开始我们遇到的问题,编译器检测到需要把List实例转换成List实例,这是被禁止的。明明可以实现的类型转换却被禁止,原因是编译时把类型参数擦除,当做Object,在运行时还要把Object类型的实例转换成类型参数的实例,两次转换存在运行时错误的风险,所以如下的方法会报错。
class A{}
class B extends A {}
// 如下两个方法不会报错
public static void funA(A a) {
// ...
}
public static void funB(B b) {
funA(b);
// ...
}
// 如下funD方法会报错
public static void funC(List<A> listA) {
// ...
}
public static void funD(List<B> listB) {
funC(listB); // Unresolved compilation problem: The method doPrint(List<A>) in the type test is not applicable for the arguments (List<B>)
// ...
}
可以将代码改写为如下的代码解决这个问题:
public static void funC(List<? extends A> listA) {
// ...
}
public static void funD(List<B> listB) {
funC(listB); // OK
// ...
}
为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。<? extends A>
表示该类型参数可以是A(上边界)或者A的子类类型。编译时擦除到类型A,即用A类型代替类型参数。这种方法可以解决上面的问题,编译器知道类型参数的范围,如果传入的实例类型B是在这个范围内的话允许转换,这时只要一次类型转换就可以了,运行时会把对象当做A的实例看待。
泛型上限
class Info<T extends Number>{ // 此处泛型只能是数字类型,即必须是Number的子类
private T var ; // 定义泛型变量
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
public String toString(){ // 直接打印
return this.var.toString() ;
}
}
public class demo1{
public static void main(String args[]){
Info<Integer> i1 = new Info<Integer>() ; // 声明Integer的泛型对象
}
}
泛型下限
class Info<T>{
private T var ; // 定义泛型变量
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
public String toString(){ // 直接打印
return this.var.toString() ;
}
}
public class GenericsDemo21{
public static void main(String args[]){
Info<String> i1 = new Info<String>() ; // 声明String的泛型对象
Info<Object> i2 = new Info<Object>() ; // 声明Object的泛型对象
i1.setVar("hello") ;
i2.setVar(new Object()) ;
fun(i1) ;
fun(i2) ;
}
public static void fun(Info<? super String> temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类
System.out.print(temp + ", ") ;
}
}
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型
4、泛型数组
我们在使用到泛型数组的场景下应该尽量使用列表集合替换,此外也可以通过使用 java.lang.reflect.Array.newInstance(Class<T> componentType, int length)
方法来创建一个具有指定类型和维度的数组,如下:
public class ArrayWithTypeToken<T> {
private T[] array;
public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] create() {
return array;
}
}
//...
ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();
三、泛型擦除
泛型的类型擦除原则是:
- 消除类型参数声明,即删除
<>
及其包围的部分。 - 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
以下将介绍如何进行擦除: - 擦除类定义中的类型参数 - 无限制类型擦除
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如<T>
和<?>
的类型参数都被替换为Object。
- 擦除类定义中的类型参数 - 有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>
和<? extends Number>
的类型参数被替换为Number
,<? super Number>
被替换为Object。
- 擦除方法定义中的类型参数
擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。
四、泛型的编译期检查
Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
例如:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("123");
list.add(123);//编译错误
}
在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。
那么,这个类型检查是针对谁的呢?我们先看看参数化类型和原始类型的兼容。
以 ArrayList举例子,以前的写法:
ArrayList list = new ArrayList();
现在的写法:
ArrayList<String> list = new ArrayList<String>();
如果是与以前的代码兼容,各种引用传值之间,必然会出现如下的情况:
ArrayList<String> list1 = new ArrayList(); //第一种 情况
ArrayList list2 = new ArrayList<String>(); //第二种 情况
这两种写法都是没有错误的,不过第二种写法会有个编译时警告。在第一种情况,可以实现与完全使用泛型参数一样的效果,第二种则没有效果。原因如下:类型检查是编译时完成的,new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象,而真正涉及类型检查的是它的引用也就是list2,所以调用add方法不能完成泛型类型的检查。