基本概念
引言
- 泛型(Generic)指可以把类型参数化,这个能力使得我们可以定义带类型参数的泛型类、泛型接口、泛型方法,随后编译器会用唯一的具体类型替换它。
- 主要优点是在编译时而不是运行时检测出错误。泛型类或方法允许用户指定可以和这些类或方法一起工作的对象类型。如果试图使用一个不相容的对象,编译器就会检测出这个错误。
- Java 的泛型通过擦除法实现,和 C++ 模板生成多个实例类不同。编译时会用类型实参代替类型形参进行严格的语法检查,然后擦除类型参数、生成所有实例类型共享的唯一原始类型。这样使得泛型代码能兼容老的使用原始类型的遗留代码。
引言中的这三条隐含着很多重要的信息,初看时可能无法深刻理解,后面我们将结合具体实例阐述。
类型参数
我们先引入一个例子。比如,我们需要实现一个类,这个类中提供了对于各种类型的数据求和的方法(实际上这个类的意义不大,只是为了说明问题而编写)。我们可以这样写:
public class NormalAdder{
//二个int相加
public static int add(int value1, int value2){ return value1 + value2;}
//二个double相加
public static double add(double value1, double value2){ return value1 + value2;}
//二个float相加
public static float add(float value1, float value2){ return value1 + value2;}
}
类NormalAdder
中,我们写了 3 3 3 个重载的add
函数实现了 3 3 3 种不同数据类型的加法。在实际使用的时候,编译器会根据我们提供的参数类型选择具体调用哪个add
函数。
但是现在有一个问题:这个类中没有short
类型、byte
类型、char
类型数据的相加方法。按照传统的函数重载子路,我们还需要再编写 3 3 3 个重载的 add
方法。这就显得代码比较冗长。
而这时我们就可以用上泛型函数:
public class GenericAdder {
//定义泛型函数,类型参数为T(代表某一种类型),T为类型形参
public static <T> T add(T value1, T value2){ return value1 + value2;}
}
//调用泛型函数,需要给出类型实参
GenericAdder.<Integer>add(1,2);//显示地给出类型实参为Integer,传递给形参T。
GenericAdder.add(1,2); //编译器自动可以推断出T为Integer(类型推断)
从而只需要定义一次add
函数。其中,泛型函数的类型参数放在<>
里。
这个例子仅仅是用于说明。实际上它不是很恰当,因为 Java 中没有运算符重载,上面
GenericAdder
中的add
方法在编译时会报错。一旦T
不是基本数据类型的包装类,虚拟机就不知道怎么解释+
这个运算。
泛型类
当一个类后面带上形式化参数,这个类就成为泛型类。泛型接口也是这样定义的。形式化类型参数是一个逗号分隔的变量名列表,位于类声明中类名后面的尖括号<>
中。下面的代码声明一个泛型类Wrapper
,它接受一个形式化类型参数T
:
public class Wrapper<T> {
// 一些代码……
// 需要注意的是,这里面不能出现类似于 new T() 的语句
}
T
是一个类型变量,它可以是 Java 中的任何引用类型。当把一个具体的类型实参传递给类型形参T
时,就得到了一系列的参数化类型(Parameterized Types),如Wrapper<String>
,Wrapper<Integer>
,这些参数化类型是泛型类Wrapper<T>
的实例类型:
Wrapper<String> stringWrapper = new Wrapper<String>();
Wrapper<Circle> circleWrapper = new Wrapper<Circle>();
强调:类型变量只能是引用类型,不能是
int
,double
,char
等值类型。不过可以用这些值类型的包装类。
动机和优点
泛型的概念是在 JDK 1.5 提出的,它的提出肯定有一定的动机。下面的例子能够说明这个动机。
当我们想要对两个对象进行比较时,通常会让这个类实现Comparable
接口,并重写Comparable
中的compareTo
函数。在 JDK 1.5 之前,Comparable
接口如下:
package java.lang;
public interface Comparable{
public int compareTo(Object o);
}
任何一个类A
如果实现了Comparable
接口,其中的函数compareTo
的参数总是Object
类型,这意味着我们可以让A
类的对象与非A
类的对象比较:
Comparable c = new Date();
System.out.println(c.compareTo("red"));
上面的语句能够通过编译,但是运行时会产生错误,抛出ClassCastException
异常。
显然,程序在编译的时候看不出有什么问题,但是我们一眼就能够发现Date
和String
两个不同类的对象不应该进行比较。泛型的引入解决了这个问题,JDK 1.5 之后的Comparable
接口成为了泛型接口:
package java.lang;
public interface Comparable<T>{
public int compareTo(T o);
}
引用Comparable
对象时,需要传入实际类型参数,完成“泛型实例化”。比如下面就将Comparable<T>
泛型接口实例化为了Comparable<Date>
的实例接口:
Comparable<Date> c = new Date();
System.out.println(c.compareTo("red"));
此时程序会在编译时报错,这就是引言第 2 2 2 条所说的,我们使用的"red"
是一个String
对象,与c
(Date
对象)不相容。
泛型引入后,编译时根据传入的泛型参数Date
,将Comparable<Date>
实例类型中的T
全部替换成Date
,并检查所有实例方法的调用是否正确,防止编译通过的地方运行时出错问题的发生。一旦编译检查通过,编译器会擦除类型参数,并按照非泛型年代的标准编译程序,这时不会再产生编译通过的地方运行时出错的问题了。
因此,泛型引入的最大优点就是在编译时找出类型不相容的问题,早发现、早解决。
泛型类、泛型接口、泛型方法的定义
泛型类的定义
我们可以利用泛型定义一个栈:
import java.util.ArrayList;
public class GenericStack<E> {
private ArrayList<E> list = new ArrayList<E>();
public boolean isEmpty() {
return list.isEmpty();
}
public int getSize() {
return list.size();
}
public E peek() {
return list.get(getSize() - 1);//取值不出栈
}
public E pop() {
E o = list.get(getSize() - 1) ;
list.remove(getSize() - 1);
return o;
}
public void push(E o) {
list.add(o);
}
public String toString() {
return "stack: " + list.toString();
}
}
其中的E
就是类型参数。其它具体类名在什么地方,它就也可以出现在什么地方,但是new E()
除外。具体使用这个泛型类时,只需要将实际参数赋给类型参数,即可确定栈的数据类型。例如:
GenericStack<String> stack1 = new GenericStack<String>(); // 后面的 String 可以省略
stack1.push("Londen");
stack1.push("Paris");
stack1.push("New York");
GenericStack<Integer> stack2 = new GenericStack<>();
stack1.push(5);// int 类型的 5 被自动打包成 Integer 包装类
stack1.push(10);
stack1.push(15);
泛型接口的定义
上文中我们已经看到了Comparable<T>
,这就是一个泛型接口。非泛型类如果要实现泛型接口,需要给泛型接口传递实际参数类型。上面例子中的Date
的函数头就是:
public class Date implements java.io.Serializable, Cloneable, Comparable<Date>{...}
给Comparable<T>
传递了实际参数类型Date
。如果我们写了一个类Circle
,要实现两个Circle
的比较,也可以将Circle
的头写成:
public class Circle extends ... implements ...,Comparable<Circle>{...}
并在Circle
内重写compareTo
方法。
泛型方法的定义
前文已经介绍了泛型方法,这里再对泛型方法做一个简单的描述。声明泛型方法,将类型参数<E>
置于返回类型之前。方法的类型参数可以作为形参类型,方法返回类型,也可以用在方法体内其他类型可以用的地方。同样地,不能new E()
。
而在实际调用泛型方法时,将实际类型放于<>
之中方法名之前;也可以不显式指定实际类型,而直接给实参调用,由编译器自动发现实际类型。
public class GenericMethodDemo {
public static void main(String[] args) {
Integer[] integers = {1,2,3,4,5};
String[] strings = {"Londen","Paris","New York","Austin"};
GenericMethodDemo.<Integer>print(integers);// 显式指定实际类型是 Integer
GenericMethodDemo.print(strings); // 不显示指定实际类型,编译器自己发现是 String
}
public static <E> void print(E[] list){
for(int i = 0 ; i <list.length; i++){
System.out.print(list[i]+" ");
}
}
}
运行结果:
1 2 3 4 5 Londen Paris New York Austin
受限的泛型
可以给形式化参数限定一个范围。考虑下面的一个要求:找到两个对象中较大的那个。
首先我们不难写出下面的代码:
public class Max{
public static <T> T findMax(T o1, T o2){
return o1.compareTo(o2)?o1:o2;
}
}
上面的代码通过使用泛型,防止了两个不相容的对象进行比较;但是还存在问题,因为不是所有的类都实现了Comparable
接口。也就是说不是所有的对象实例都能够调用compareTo
方法。因此,我们需要限定T
必须要是实现了Comparable
接口的类型。
改进后的代码如下:
public class Max{
public static <T extends Comparable<E>> T findMax(T o1, T o2){
return o1.compareTo(o2)?o1:o2;
}
}
<T extends Comparable<E>>
规定了传进来的T
必须实现了 Comparable<E>
接口,否则编译器报错。还可以用<T extends SomeClass>
来限定T
必须是SomeClass
的子类。
需要注意的是,无论是限定T
需要继承某些类,还是限定T
要实现某些接口,一律使用关键字extends
。