Java中的泛型

来源:互联网 发布:租用阿里云vps 编辑:程序博客网 时间:2024/04/30 22:14

Java中的泛型可以帮我们解决很多抽象设计层面的问题。但是要想把它用好也不是件容易的事情。本文旨在为理解Java泛型打下基础。


1. 了解泛型


1.1 泛型的协变问题

Java泛型的协变问题是从数组赋值以及集合类中的包含对象引出的。这个问题的原委是什么呢?Java中的数组是协变的(covariant),也就是说任何一个子类的数组都是适用于超类的上下文的。一个常用的例子就是Integer和Number,Integer[]和Number[]这样的关系。但是这种关系在泛型集合类中却不然,例如List<Integer>和List<Number>这样的。在泛型化的集合类中是非协变的(invariant)。虽然Integer是Number的子类,但是在泛型集合类中你不能把List<Integer>赋值给List<Number>,这么做是非法的。因为,在Java中一个子类和父类的赋值关系,不等同于泛型化集合类也存在这种关系。这个是Java泛型化中的协变问题。话说回来,如何解决这种泛型化的协变问题呢?关键就是通配符(?)和继承限定关系(extends)。那么如何让List<Integer>可以赋值给List<Number>呢?可以这么做:

List<? extends List<? extends Number>> list = new ArrayList<List<Integer>>();List<? extends Number> li = new ArrayList<Integer>();
这就是解决Java泛型化中的协变问题的钥匙。另一个需要注意的协变问题是:不能实例化泛型类型的数组。例如:

List<String>[] li = new ArrayList<String>[10];
这种写法也是非法的。



1.2 类型的擦除

因为泛型基本上都是在 Java 编译器中而不是运行库中实现的,所以在生成字节码的时候,差不多所有关于泛型类型的类型信息都被“擦掉”了。换句话说,编译器生成的代码与您手工编写的不用泛型、检查程序的类型安全后进行强制类型转换所得到的代码基本相同。与 C++ 不同,List<Integer>List<String>是同一个类。擦除意味着一个类不能同时实现Comparable<String>Comparable<Number>,因为事实上两者都在同一个接口中,指定同一个compareTo()方法。擦除的另一个后果是,对泛型类型参数是用强制类型转换或者instanceof毫无意义。下面的代码完全不会改善代码的类型安全性:

public <T> T naiveCast(T t, Object o) { return (T) o; }
编译器仅仅发出一个类型未检查转换警告,因为它不知道这种转换是否安全。naiveCast()方法实际上根本不作任何转换,T直接被替换为Object,与期望的相反,传入的对象被强制转换为Object。擦除也是造成上述构造问题的原因,即不能创建泛型类型的对象,因为编译器不知道要调用什么构造函数。如果泛型类需要构造用泛型类型参数来指定类型的对象,那么构造函数应该接受类文字(Foo.class)并将它们保存起来,以便通过反射创建实例。

下面,我们通过两个例子来说明类型的擦除问题:一个是集合类中所包含的类型;一个是集合类本身。

先看代码一:

package com.homeland.myapp;import java.util.ArrayList;import java.util.List;public class GenericDemo {public static void main(String[] args) {List<String> list1 = new ArrayList<String>();  list1.add("a");          List<Integer> list2 = new ArrayList<Integer>();          list2.add(1);          System.out.println(list1.getClass()==list2.getClass()); // true}}
这个例子用来说明在经过Java编译器处理过的文件中泛型化的String和Integer是被擦除掉的,实际上就只有List这个类型而已。

再来看代码二:

package com.homeland.myapp;import java.util.ArrayList;import java.util.List;public class GenericDemo {    public static void main(String[] args) throws Exception {        List<Integer> list3 = new ArrayList<Integer>();        list3.add(1);        list3.getClass().getMethod("add", Object.class).invoke(list3, "b");        for (int i = 0; i < list3.size(); i++) {            System.out.println(list3.get(i));        }    }}
输出:
1b
这个是通过反射来调用List的方法add()从而达到可以在同一个List中添加Integer和String的目的。它再次说明了Java泛型的类型擦除会把泛型化参数信息完全抹掉,实际上只用了原始类型(raw type)或者说未泛型化的类型。



1.3 类型擦除引起的问题



1.3.1 类型匹配

在使用泛型化的集合类的时候,往往会遇到类型不匹配的问题。这个问题是由于变量声明的过程中等号左右两侧的泛型类型不一致所导致的。一个统一的写法类似如下代码:

List<String> list = new ArrayList<String>();



1.3.2 类型判定

形如下列代码的类型判定是非法的,因为类型信息已经被擦除了:

if (list instanceof ArrayList<String>) {// do something...}



1.3.3 不能使用基本类型

形如下列代码的泛型声明是非法的,这是协变过程所决定的:

List<double> list = new ArrayList<double>();
Java的泛型化参数是通过类的继承关系来确定一个类对象的。这注定了泛型化参数是不能使用基本类型的,只能使用包装类型。



1.3.4 不能将类声明中的泛型化参数静态化

形如下列代码的泛型声明是非法的,因为静态化的过程和泛型化过程是矛盾的:

package com.homeland.myapp;import java.util.ArrayList;import java.util.List;public class GenericDemo<T> {private static T one;public static void main(String[] args) {// do something...}}
但是,静态化的泛型方法是可行的:

package com.homeland.myapp;public class GenericDemo<T> {public static void main(String[] args) throws Exception {// do something...}private static <T> T doSomething(T arg) {return null;}}
这些问题可以大致上这么理解:当你使用泛型的时候,泛型化的参数类型要保持一致或者兼容,当类型未确定的时候,一切执行类型判定,类型静态化,用基本类型代替包装类的做法都是非法的。类型的擦除所导致的问题的根本就是:你不能把一个擦除了类型信息的对象当做具体的类型来判定,任何实例化的,静态化的行为都是非法的。类型擦除的问题相对泛型协变问题没有协变那么隐晦,难懂。一般来说,擦除所导致的问题都可以在书写过程中所避免。



2. 泛型的声明及使用

通常来讲泛型在声明过程中有多种写法,来标明或者限定它的用途。泛型可以声明在类,接口,方法上。同时,可以对类型的继承关系进行限定。通配符的使用是泛型使用中的重点。



2.1 类的泛型声明

请参考如下代码:

package com.homeland.myapp;public class GenericDemo {public static void main(String[] args) throws Exception {Demo<String> d = new Demo<String>();d.setOne("Demo::one");System.out.println("T of Demo: " + d.getOne());}}class Demo<T> {private T one;public T getOne() {return one;}public void setOne(T one) {this.one = one;}}
类的泛型化参数在紧挨着类名的后面有一对尖括号“<>”,通常在这对尖括号里面,我们用大写的T来代表泛型化的参数。然后,接下来的私有变量以及getter()和setter()方法都是在这个基础上的。没有这个声明,私有变量及方法都无法使用。

另外有一种双参数的泛型声明,如下:

class Node<K, V> {private K key;private V value;public K getKey() {return key;}public void setKey(K key) {this.key = key;}public V getValue() {return value;}public void setValue(V value) {this.value = value;}}



2.2 接口的泛型声明

请参考如下代码:

interface Info<T> {public T getOne();}class InfoImpl<T> implements Info<T> {private T one;public void setOne(T one) {this.one = one;}@Overridepublic T getOne() {return one;}public InfoImpl(T one) {this.setOne(one);}}
在接口的泛型化参数声明中和类的泛型化参数声明的格式是一样的。有一点需要注意的是,作为实现类,其泛型化参数需要与接口的泛型化参数保持一致。


2.3 方法的泛型声明

请参考如下代码:

class DemoGenericMethod {public <T> T doSomething(T t) {return null;}}
方法的泛型声明是这样的:在访问限定修饰符的后面有一个尖括号的类型声明,然后必须声明方法的返回类型为该泛型类型,即声明和返回类型使用一样的字符。最后才能在方法的参数列表中使用它。有三个地方必须保持一致:泛型类型的声明,方法的返回类型,方法的参数列表。



2.4 泛型类型的限定

泛型类型的限定是泛型使用过程中的两个重点之一。另一个是通配符的使用。限定泛型类型有两个手段。一个是extends关键字,另一个是super关键字。

请参考如下代码(super):

public class GenericDemo {public static void main(String[] args) throws Exception {Demo<String> d = new Demo<String>();d.setOne("Demo::one");Demo<Object> d1 = new Demo<Object>();d1.setOne(new Object());doSomething(d);doSomething(d1);}public static void doSomething(Demo<? super String> d) {System.out.println("d: " + d);}}class Demo<T> {private T one;public T getOne() {return one;}public void setOne(T one) {this.one = one;}public String toString() {return this.one.toString();}}
输出:

d: Demo::oned: java.lang.Object@69066caf
请参考如下代码(extends):

public class GenericDemo {public static void main(String[] args) throws Exception {Demo<String> d = new Demo<String>();d.setOne("Demo::one");Demo<Object> d1 = new Demo<Object>();d1.setOne(new Object());doSomething(d);doSomething(d1);Demo<Integer> d2 = new Demo<Integer>();d2.setOne(1);Demo<Float> d3 = new Demo<Float>();d3.setOne(1.1f);doMore(d2);doMore(d3);}public static void doSomething(Demo<? super String> d) {System.out.println("d: " + d);}public static void doMore(Demo<? extends Number> d) {System.out.println("d: " + d);}}class Demo<T> {private T one;public T getOne() {return one;}public void setOne(T one) {this.one = one;}public String toString() {return this.one.toString();}}
输出:

d: Demo::oned: java.lang.Object@38093b59d: 1d: 1.1



2.5 通配符的使用

Java泛型中的通配符就是“?”这个字符。没有限定的通配符表示任意类型。例如:

List<?> list = new ArrayList<?>();
这句表示任何类型都适用。既可以是String类型,也可以是Number类型等。通配符的使用基于其与限定修饰符的结合所表达的范围的理解。这个很拗口。换个说法,当你在使用通配符来进行泛型表达式的赋值,以及其所修饰类的操作的时候,你必须要反复的想想,等号两边或者操作者和被操作者之间的数据类型或者数据类型的集合,是否是一致的。这就是泛型操作的本质,无论JVM怎么样的类型擦除,类型强转,协变判定,其目的就是要保证所操作的数据集合不会扩大,也不会缩小。


关于通配符,还必须介绍通配符捕获。在声明泛型通配符的时候,JVM会给每一个通配符一个占位符来表示这个未知的类型也即通配符捕获,通常这么写:

capture#337 of ?
那么,如果在你的方法中有多个通配符,这个占位符也会有多个,比如:

public void doSomethin(List<?> list1, List<?> list2);
像这种,每一个通配符就会有一个自己的占位符来标识。不用担心JVM把它们弄混淆。



2.6 返回泛型实例

请参考如下代码:

public class GenericDemo {public static void main(String[] args) throws Exception {MyData<Integer> m = doMore(30);System.out.println("MyData: " + m.getOne());}public static <T extends Number> MyData<T> doMore(T one) {MyData<T> md = new MyData<T>();md.setOne(one);return md;}}class MyData<T extends Number> {private T one;public void setOne(T one) {this.one = one;}public T getOne() {return one;}public String toString() {return this.one.toString();}}



2.7 泛型数组

请参考以下代码:

public class GenericDemo {public static void main(String[] args) throws Exception {Integer[] i = doInput(1, 2, 3, 4, 5);doOutput(i);}public static <T> T[] doInput(T...args) {return args;}public static <T> void doOutput(T[] params) {System.out.println("the input array: ");for (T t:params) {System.out.println("T is: " + t);}}}
这里有个地方需要特别注意,就是方法doInput(),因为这里用到了可变参数列表,而且还是泛型化的。在Eclipse里面,如果你这么写,会有一个警告:

Type safety: Potential heap pollution via varargs parameter args
这个警告的大体意思就是:如果你用泛型化的可变参数列表,那么你在赋值以及其他涉及到类型转换的操作的时候,可能会存在“堆污染”的情况。即会抛出一个异常:

ClassCastException
这就像“七伤拳”伤人伤己啊!解决方法也不是木有嘛,那揍是:保证可变参数列表的类型都是同一类型。



2.8 嵌套的泛型

形如以下代码的:

public class GenericDemo {public static void main(String[] args) throws Exception {MyData<Integer> m = new MyData<Integer>();m.setOne(30);Demo2<MyData<Integer>> s = new Demo2<MyData<Integer>>();s.setMydata(m);}}class Demo2<S> {private S mydata;public void setMydata(S mydata) {this.mydata = mydata;}public S getMydata() {return mydata;}}
这个就是泛型嵌套了。



2.9 类的继承限定

形如以下代码的:

class MyData<T extends Number & Serializable & Comparable<T>> {// do something...}
这种用的不多,稍微解释下:外来参数必须是Number的子类,同时实现了Serializable接口和Comparable<T>接口。


综上所述,Java中的泛型使用是基于理解的,否则很容易出错。另外,泛型的使用能够相当程度的简化抽象层的设计,甚至能够将代码生成自动化。

0 0
原创粉丝点击