为什么要使用泛型程序设计
泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。
类型参数的好处
在Java中增加泛型类之前,泛型程序设计使用继承实现的。ArrayList类只维护一个Object引用的数组:
public class ArrayList // before generic classes
{
private Object[] elementData;
...
public Object get(int i) { ... }
public void add(Object 0) { ... }
}
这种方法有两个问题。
- 当获得一个值时必须进行强制类型转换
ArrayList files = new ArrayList();
String filename = (String) files.get(0);
- 这里没有错误检查。可以向数组列表中添加任何类的对象。
`files.add(new File("..."));
对于这个调用,编译和运行都不会出错。然而在其他地方,如果将get的结果强制类型转换为String类型,就会产生一个错误。
泛型提供了一个更好的解决方案:类型参数(type parameters)。ArrayList类有一个类型参数用来指示元素的类型:
ArrayList<String> files = new ArrayList<String>();
在Java SE 7 及以后版本中,构造函数中可以省略泛型类型:
ArrayList<String> files = new ArrayList<>();
省略的类型可以从变量的类型推断得出。当调用get的时候,不需要进行强制类型转换,编译器就知道返回值类型:
String filename = files.get(0);
编译器可以进行检查,避免插入错误类型的对象。例如,通过下面方式向ArrayList添加元素是无法通过编译的:
files.add(new File("...")); // can only add String objects to an ArrayList<String>
出现编译错误比类在运行时出现类的强制类型转换异常要好得多。
类型参数的魅力在于:使得程序具有更好的可读性和安全性。
泛型编程
使用像ArrayList的泛型类很容易。但实现一个泛型类并没有那么容易。
Java语言的设计者发明了一个具有独创性的新概念,通配符类型(wildcard type),允许应用程序员实现自己的泛型类与泛型方法。
定义简单泛型类
一个泛型类(generic class)就是具有一个或多个类型变量的类。例如:
public class Pair<T>
{
private T first;
private T second;
public Pair() { fist = null; second = null; }
public Pair(T first, T second) { this.first = first; this.second = second; }
public T getFirst() { return first; }
public T getSecond() { return second; }
public void setFirst(T newValue) { first = newValue; }
public void setSecond(T newValue) { second = newValue; }
}
Pair类引入了一个类型变量T,用尖括号(<>)括起来,并放在类名的后面。泛型类可以有多个类型变量。例如:
public class Pair<T, U> { ... }
类定义中的类型比变量指定方法的返回类型以及域和局部变量的类型。例如:
private T first; // use the type variable
在Java库中,使用变量E标识集合的元素类型,K和V分别表示表的关键字与值的类型。T(需要时还可以用临近的字母U和S)表示“任意类型”。
使用具体的类型替换类型变量来实例化泛型类型。例如:
Pair<String>
可以将结果想象成带有构造器的普通类:
Pair<String>()
Pair<String>(String, String)
和方法:
String getFirst()
String getSecond()
void setFirst(String)
void setSecond(String)
下面给出使用泛型类Pair的示例:
package pair1;
public class PairTest1
{
public static void main(String[] args)
{
String[] words = {"Mary", "had", "a", "little", "lamb"};
Pair<String> mm = ArrayAlg.minmax(words);
System.out.println("min = " + mm.getFirst());
System.out.println("max = " + mm.getSecond());
}
}
class ArrayAlg
{
/**
* Gets the minimum and maximum of an array of strings
* @Param a an array of strings
* @return a pair with the min and max value, or null if a is null or empty
*/
public static Pair<String> minmax(String[] a)
{
if(a == null || a.length == 0) return null;
String min = a[0];
String max = a[0];
for (int i = 1; i < a.length; i++)
{
if (min.compareTo(a[i]) > 0) min = a[i];
if (max.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<>(min, max);
}
}
泛型方法
可以定义一个带有类型参数的简单方法。
class ArrayAlg
{
public static <T> T getMiddle(T... a)
{
return a[a.length / 2];
}
}
这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法。注意,类型变量放在修饰符(这里是public static)的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");
在这种情况下,方法调用中可以省略类型参数。编译器有足够的信息能够推断出所调用的方法。
String middel = ArrayAlg.getMiddle("John", "Q.", "Public");
大多数情况下,泛型方法的类型引用没有问题。偶尔,编译器会提示错误。例如:
double middle = ArrayAlg.getMiddel(3.14, 1729, 0);
错误消息会以晦涩的方式指出:解释这句代码有两种方法,而且这两种方法都是合法的。简单地说,编译器会自动打包参数为1个为Double(3.14)和2个Integer对象(1729和0),而后寻找这些类的共同超类型。事实上,找到2个这样的超类型:Number和Comparable接口,其本身也是一个泛型类型。可以采取的补救措施是将所有的参数都写为double值(1729写为1729.00和0写为0.00)。
当我使用IntelliJ IDEA测试上面代码时,idea插件的代码补全功能会自动声明middle
变为Number
类型,强制使用double
类型,idea提示错误:
类型变量的限定
有时,类或方法需要对类型变量加以约束。例如:
class ArrayAlg
{
public static <T> T min(T[] a) // almost correct
{
if (a == null | a.length == 0) return null;
T smallest = a[0];
for (int i = 1; i < a.length; i++)
if (smallest.compareTo(a[i] > 0) smallest = a[i];
return smallest;
}
}
为了确信T所属的类有compareTo方法,可以通过对类型变量T设置限定(bound),将T限制为实现了Comparable接口的类:
public static <T extends Comparable> T min(T[] a) ...
实际上Comparable接口本身就是一个泛型类型。目前,暂时忽略其复杂性以及编译器产生的警告。
对于在此为什么使用关键字extends而不是implements,下面的记法:
<T extends BoundingType>
表示T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。选择关键字extends
的原因是更接近子类的概念,且Java设计者也不打算在语言中添加一个新的关键字(如sub)。
一个类型变量或通配符可以有多个限定,限定类型用“&”分隔,而逗号用来分隔类型变量。
<T extends Comparable & Serializable>
在Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类(毕竟一个类只能继承一个父类)。如果用一个类作为限定,它必须是限定列表中的第一个。