Java泛型详解
Java泛型 (generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(
类型擦除
正确理解泛型概念的首要前提是理解 类型擦除(type erasure)。 Java中的 泛型基本上都是在编译器这个层次来实现的 。在生成的很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
- 泛型类并没有自己独有的Class类对象。
比如并不存在List<String>. class或是List<Integer>.class, 而只有List.class。 - 静态变量是被泛型类的所有实例所共享的。
对于声明为MyClass<T>的类, 访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象, 都是共享一个静态变量。 - 泛型的类型参数不能用在Java异常处理的catch语句中。
因为异常处理是由JVM在运行时刻来进行的。 由于类型信息被擦除, JVM是无法区分两个异常类型MyException< String>和MyException<Integer>的。 对于JVM来说,它们都是 MyException类型的。 也就无法执行与异常对应的catch语句。
类型擦除的基本过程也比较简单,
class MyString implements Comparable<String> { public int compareTo(String str) { return 0; } }
当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话,
实例分析
了解了类型擦除机制之后,
public void inspect(List<Object> list) { for (Object obj : list) { System.out.println(obj); } list.add(1); //这个操作在当前方法的上下文是合法的。 } public void test() { List<String> strs = new ArrayList<String>(); inspect(strs); //编译错误 }
这段代码中,inspect方法接受List<Object>
通配符(?)与上下界(extends ,super )
在使用泛型类的时候,既可以指定一个具体的类型,如List<
如上所示,试图对一个带通配符的泛型类进行操作的时候,
因为对于List<?>中的元素只能用Object来引用,
类型系统
在Java中,引入泛型之后的类型系统增加了两个维度:
- 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。
即List<String>是Collection< String> 的子类型,List<String> 可以替换Collection<String>。 这种情况也适用于带有上下界的类型声明。 - 当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对Collection<
? extends Number>来说, 其子类型可以在Collection这个维度上展开, 即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开, 即Collection<Double>和 Collection<Integer>等。如此循环下去, ArrayList<Long>和 HashSet<Double> 等也都算是Collection<? extends Number>的子类型。 - 如果泛型类中包含多个类型参数,
则对于每个类型参数分别应用上面的规则。
开发自己的泛型类
泛型类与一般的Java类基本相同,只是在类和接口定义上多出来
class ClassTest<X extends Number, Y, Z> { private X x; private static Y y; //编译错误,不能用在静态变量中 public X getFirst() { //正确用法 return x; } public void wrong() { Z z = new Z(); //编译错误,不能创建对象 ,因为泛型类并没有自己独立的Class类对象 } }
最佳实践
在使用泛型的时候可以遵循一些基本的原则,- 在代码中避免泛型类和原始类型的混用。比如List<
String>和List不应该共同使用。 这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时, 也尽可能的隔离相关的代码。 - 在使用带通配符的泛型类的时候,
需要明确通配符所代表的一组类型的概念。 由于具体的类型是未知的,很多操作是不允许的。 - 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。
这限制了数组的使用能力,而且会带来很多费解的问题。因此, 当需要类似数组的功能时候,使用集合类即可。 - 不要忽视编译器给出的警告信息。
C++模板实现
在c++中为每个模板的实例化产生不同的类型,这一现象被称为“模板代码膨胀”。
比如 vector<int>, vector<char>, vector<double>, 这里总共会生成3份不同的vector代码。
- 编译器判断泛型方法的实际类型参数的过程叫做类型推断,类型推断的实现方法是一种非常复杂的过程.
- 根据调用泛型方法时实际传递的参数类型或返回值类型来推断,具体规则如下:
- 如果某类型变量只在方法参数列表或返回值的一处被调用了,那根据调用该方法时该处的实际类型来确定,即直接根据调用方法时传递的实际类型或方法返回值的类型来确定泛型方法的参数类型.例如: swap(new String[3],3,4) --->static <E> void swap(E[] a,int i,int t)
- 当某个类型变量在方法的参数列表和返回值中被多次利用了,而且在调用方法时这多处的实际类型又是一样的,那么这也可以很明显的知道此泛型方法的参数类型.例如: add(3,5) --> static <T> T add(T a,T b)
- 当某个类型变量在方法的参数列表和返回值中被多次利用了,而且在调用方法时这多处的实际类型又对应不同的类型,且返回值是void,那么这时取多处实际变量类型的最大交集.例如: fill(new Integer[3],3.5f) --> static <T> void fill(T[] a,T i) ,此时T为Number,编译不会报错,但运行有问题.
- 当某个类型变量在方法的参数列表和返回值中被多次利用了,且返回值不为空,在调用方法时这多处的实际类型又对应不同的类型,那么优先考虑返回值的类型.int x = add(3,3.5f) --> static <T> T add(T a,T b)
- 参数类型的类型推断具有传递性,
copy(new Integer[5],new String[5]) --> static <T> void copy(T[] a, T[] b) T为Object类型,没有问题
copy(new Vector<String>(),new Integer[5]) --> static <T> void copy(Collection<T> a, T[] b) 在new Vector<String>()时决定了T为String类型,而new Integer[5]不是String类型,这样会报错
package all;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* java 泛型方法的类型推断
* @author root
*
*/
public class TypeInfer {
/**
* 1.类型变量只在方法参数列表或返回值中的一处被调用,
* 那么类型推测的结点就是直接传递 的参数类型或返回的值的类型
* @param <T>
* @param value
*/
public static<T> void onlyOneCall(T value){
System.out.println(value);
}
/**
*参数多次调用且传递参数类型相同,那么就会传入参数的实际类型
* @param <T>
* @param a
* @param b
* @return
*/
public static<T> T sameParam(T a,T b){
return a;
}
/**
* 有多个参数,且传入实参的类型都不一样,推断的出的类型则是它们的交集,运行时可能会错误
* 如: noSamePram(new Integer[2],new Float(2.0))//此时推断出来的类型为 Number,但会运行错误
* @param <T>
* @param array
* @param b
*/
public static<T> void noSameParam(T[]array,T b){
int len = array.length;
for(int i=0;i<len;i++)
array[i] = b;
}
/**
* 有多个参数,传入值类型不同,而且有返回值,那么会优先考虑返回 值
* @param <T>
* @param a
* @param b
* @return
*/
public static<T> T noSameParamWithReturn(T a,T b){
return b;
}
/**
* 类型推断具有传递性,如果参数中有需要推断的类型,如果list<T>此的T就是传递进来的参数的值的类型,如
* copy(new List<String> list,new Integer(1)),T就为 string,而此时array为integer,两者不现,不会再取
* 交集,会编译错误,因此,在参数中的泛型的泛型方法中,传递的参数一定保持一致
* 如: copy(Collection<T> col,T[]array),copy(new List<String>,new Integer[]) 首先推断出 T 类型为 string,而后面的是 Integer,所以错误
* @param <T>
* @param col
* @param array
*/
public static<T> void copy(Collection<T>col,T[]array){
}
public static<T> T copycopy(T[]array,Collection<T>col){
return array[0];
}
public static<T> T copy(T a,T b){
return a;
}
public static void main(String args[]){
Integer a = new Integer(1);
Long b = new Long(2);
//noSameParam(new Integer[3],new Float(1));
List<String> list = new ArrayList<String>();
Integer [] array = new Integer[]{1,2,3};
copy(list,array);
copy(array,list);
copy(new String(),new Integer(2));
}
}