转载自:
JDK 1.5 相对于 JDK 1.4 来说变化最大的部分就是泛型,甚至可以说自 Java 1.0 发布以来也是最大的一次语言变化,因为要涉及到编译器的大改动。很早的时候大家对泛型的呼声很大,正如 C++ 的模板,C# 的泛型确实是个值得借鉴的好特性。JDK1.5 这前,很多人对于泛型是急不可耐,在 JDK1.4 下搭配一个外挂的 Generic Java 编译器,通老实 -Xbootclasspath、-bootclasspath 同样能让你在 1.4 的 JDK 上使用泛型:
javac -J-Xbootclasspath/p:JSR14HOME\gjcrt.jar -bootclasspath JSR14HOME\collect.jar;JDK141HOME\jre\lib\rt.jar -source 1.5 FileName.java
java -Xbootclasspath/p:JSR14HOME\gjc-rt.jar FileName
JDK 1.5 虽说出来这么久了,JDK 7 眼看都要见得天日了,不过可能我们很多人也只是知道怎么使用支持泛型的集合,也许还没有写过自己的泛型类,更不用说泛型的一些更高阶规范了。正如我们会在 Hibernate、Spring 里很熟练的使用零配置的注解,好像也很少有机会让我们去写处理注解的代码似的。因为毕竟我们不是书写框架的,多数时候是在应用它们。多看JDK 的源代码固然是个好办法,但除看之外,练手帮助理解是必不可少的。如果你熟悉 C++,可以与 C++ 的模板类比。现在来一步步了解泛型。首先泛型这一称谓听来有点抽象,换作参数化类型,或者借用模板类的叫法会好理解的多。比如说这样一个泛型类:
package com.unmi;
public class Demo<T> {
private T first;
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
}
声明部分 public class Demo
String first;
String getFirst();
void setFirst(String first);
如果具体类型为 Integer,用 Demo
Integer first;
Integer getFirst();
void setFirst(Integer first);
可以是任何的具体类型,所以可以理解为泛型类是普通类的工厂。其实依照 C++ 的名词,前面类的定义称之为一个模板类,使用的时候是让模板中的类型具体化,这样好理解些。但要记得,Java 的泛型与 C++ 的模板实现的泛型内部机制还是有本质区别的。前面是在类的声明部分定义了一个类型参数,如果有如个类型参数也是可以的,写成public class Demo
//类声明前要加个模板声明打头
template <class Type> class Stack{
public:
Type Push(Type item);
};
//每个方法也都要以模板声明打头
template<class Type> Type Stack<Type>::Push(Type item){
return item;
}
int main(int argc, char* argv[]){
Stack<int> fishes;
cout<<fishes.Push(123)<<endl;
return 0;
}
前面讲了泛型类的定义,你也可以在一个普通类中单单定义一个泛型方法。也就是说类能够带个类型参数,方法也可以带类型参数的。还是来看个例子(包括如何应用),一个获得数组中间元素的方法,因为数组中存储元素的类型是不定的,所以把该方法定义成泛型的。
package com.unmi;
/**
* 泛型方法示例
* @author Unmi
*/
public class ArrayAlg {
//这个就是在普通类 ArrayAlg 中定义的泛型方法
public static <T> T getMiddle(T[] a){
return a[a.length/2];
}
public static void main(String[] args) {
String[] names = {"Fantasia","Unmi","Kypfos"};
//String middle = ArrayAlg.<String>getMiddle(names);
//上面那样写是可以,编译器可推断出要调用的方法,所以省去<String>
String middle = ArrayAlg.getMiddle(names);
System.out.println(middle);
}
}
我们之所以说上面的 ArrayAlg 是个普通类,是因为没有在类声明部分引入类型参数(比如声明为 public class ArrayAlg
对比前面泛型类的定义 public class Demo
ArrayAlg.getMiddle(names); 来调用该方法。通常我们是这么做的,原因是 Java 编译器通过参数类型、个数等信息能推断出调用哪一个方法。但 Java 编译器也不是完全可靠的,有时候你必须显式的用 ArrayAlg.
package com.unmi;
/**
* 泛型方法示例,泛型方法的显式调用
* @author Unmi
*/
public class ArrayAlg {
//这个就是在普通类 ArrayAlg 中定义的泛型方法
public static <T> T getMiddle(T[] a){
return a[a.length/2];
}
public static String getMiddle(String[] a){
return "Not Generic Method.";
}
public static void main(String[] args) {
String[] names = {"Fantasia","Unmi","Kypfos"};
//必须显式的用 <String> 去调用定义的泛型方法
String middle1 = ArrayAlg.<String>getMiddle(names);
System.out.println(middle1); //输入 Unmi,调用了泛型方法
//不指明参数类型 <String> 则调用的是那个普通方法
String middle2 = ArrayAlg.getMiddle(names);
System.out.println(middle2); //输出 Not Generic Method
}
}
这也有些像我们的 C++ 的模板类,在模板具体化的时候存在 隐式实例化、显式实例化、显式具体化、部分具体化的情况,怎么看 C++ 的模板类还是要比 Java 的泛型复杂。
当然,上面代码只是说明 Java 的泛型方法在语法上会出现这种情况,倘若谁真写出的泛型代码需要用 ArrayAlg.
进一步联系到前一篇,泛型类在定义的时候可以指定多个类型参数(用
package com.unmi;
import java.util.*;
/**
* 泛型方法示例,多类型参数的情况
* @author Unmi
*/
public class ArrayAlg {
//由索引获得
public static <T,U> T getByIdx(T[] a, U b){
//依照 HashMap 实现的算法,由 b 得到一个不越界的索引
int h = b.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
h = h ^ (h >>> 7) ^ (h >>> 4);
int index = h & (a.length-1);
return a[index];
}
public static void main(String[] args) {
String[] names = {"Fantasia","Kypfos","Unmi"};
//显式的用 <String, Object> 去调用定义的泛型方法
String name = ArrayAlg.<String, Date>getByIdx(names,new Date());
//隐式调用泛型方法
String name1 = ArrayAlg.getByIdx(names,"GameOver");
//会输出 Unmi:Fantasia,或 Fantasia:Fantasia
System.out.println(name + ":" + name1);
}
}
因为现在还不想涉及到调用类型参数的特定方法,所以参照 HashMap 算法,由第二个类型参数算出数组范围内的索引。留意两种调用泛型方法的方式,应用隐式调用在有些情况下也是会产生二义性的。
前面介绍的无论是泛型类还是泛型方法,基本上都是把定义的类型参数作为一个整体来操作,放到数组或取出来,顶多就是调用了一下 hashCode() 方法,因为这是 Java 的根对象拥有的方法。比如说一个泛型数组,要得到其中的最小元素:
package com.unmi;
/**
* 泛型,类型变量的限定
* @author Unmi
*/
public class ArrayAlg {
public static <T> T main(T[] a){
if(a==null || a.length ==0){
return null;
}
T smallest = a[0];
for(int i=0;i<a.length;i++){
if(smallest.compareTo(a[i])>0){
smallest = a[i];
}
}
return smallest;
}
}
在上面的代码中,要比较大小,所以调用了类型 T 的 compareTo() 方法,我们期望具体类型是一个实现了 compareTo() 方法的类型。不过,很可惜,Java 泛型还是与 C++ 的模板有别,在 C++ 中真的对于类似上面的代码,具体类型有 compareTo() 则可以通过,具体类型没有 compareTo() 则不能通过,是推延至使用代码时确定具体类型是否满足条件。而在 Java 中无论你的具体类型是什么,上面的代码本身就是无法编译通过的,错误为:
The method compareTo(T) is undefined for the type T也就是说在你编写泛型的时候就该限类型参数的一个更窄的范围,不是所有的 Object,而是那些实现了 compareTo() 方法的类型,比如说实现了 Compareable 接口的类型。为了做到这一点,对于前面方法声明部分就要做如下进一步约束:
public static这里只表示到时的具体类型 T 是 Comparable 的一种类型,extends 后是接口,还是类都是用 extends 关键字,不在此区分接口还是类,只表示 Is-A 的关系。这样你在使用该泛型的时候具体类型一定要是实现了 Comparable 接口的类型,比如用这样的代码 ArrayAlg.main(new Object[]{"1","2","3"}); 调用,则会有下面的错误提示:
Bound mismatch: The generic method main(T[]) of type ArrayAlg is not applicable for the arguments (Object[]). The inferred type Object is not a valid substitute for the bounded parameter因为 Object 并未实现 Comparable,用 ArrayAlg.main(new String[]{"1","2","3"}); 调用则无误,String 是实现了 Comparable 接口的。如果在泛型实现中会调用到多个方法,要求泛型参数同属不同的类型,就 extends 多个接口或类,中间用 & 符号隔开。
public static <T extends Comparable & Serializable> T foo(T[] a){
//a[i].compareTo(a[i-1]);
//xxx.writeObject(a[i]);
}
为什么是用 & 而不用逗号(,) 呢,因为逗号还有作他用,当有多个类型参数时就该写作:
public static <T extends Comparable & Serializable, U extends Runnable> T foo(T a, U b){
//a[i].compareTo(a[i-1]);
//xxx.writeObject(a[i]);
//new Thread(u).start();
}
虽然这里简单的用 extends 统一的表示了原有的 extends 和 implements 的概念,但仍要遵循应用的体系,Java 只能继承一个类,可以实现多个接口,所以你的某个类型需要用 extends 限定,有多种类型的时候,只能存在一个是类,而且参考变参的做法,类写在第一位,接口列在后面,也就是:
这里的例子仅演示了泛型方法的类型限定,对于泛型类中类型参数的限制用完全一样的规则,只是加在类声明的头部,如:
public class Demo<T extends Comparable & Serializable>{
//该泛型类中就可以用 Comparable 声明的方法和 Seriablizable 所拥有的特性了
}
多个类型参数的情况类推就是了。