Java和C++在细节上的差异(四)
八、泛型程序设计:
1. 泛型类的定义,见如下代码:
1 public class Pair<T,U> {
2 public Pair() { first = null; second = null; }
3 public Pair(T first,U second) { this.first = first; this.second = second; }
4
5 public T getFirst() { return first; }
6 public U getSecond() { return second; }
7 public void setFirst(T first) { this.first = first; }
8 public void setSecond(U second) { this.second = second; }
9
10 private T first;
11 private U second;
12 }
以上代码中的T,U都是泛型类Pair的类型参数。以下为C++中模板类的定义方式:
1 template<typename T,typename U>
2 class Pair {
3 public:
4 Pair(T first,U second): _first(first),_second(second) {}
5 ~Pair() {}
6 public:
7 T getFirst() { return _first; }
8 U getSecond() { return _second; }
9 void setFirst(T frist) { _first = first; }
10 void setSecond(U second) { _second = second; }
11 private:
12 T _first;
13 U _second;
14 }
2. 泛型方法的定义,在Java中泛型方法不一定声明在泛型类中,可以声明在普通类中,见如下代码:
1 public class MyFirst {
2 public static void main(String[] args) throws Exception {
3 String[] names = {"john","Q.","Public"};
4 String middle = ArrayAlgo.<String>getMiddle(names);
5 System.out.println(middle);
6 }
7 }
8
9 class ArrayAlgo {
10 public static <T> T getMiddle(T[] a) {
11 return a[a.length/2];
12 }
13 }
在以上代码中可以看出getMiddle方法为静态泛型方法,类型变量位于修饰符"public static" 的后面,返回值的前面。调用的时候在方法名的前面给出了参数类型。由于Java的编译器提供类型推演的功能,既类型参数可以通过函数参数的类型进行推演,因此也可以直接调用泛型函数,如String middle = ArrayAlgo.getMiddle(names)。如果编译器无法通过函数参数的类型推演出类型参数的实际类型,这样将会导致编译错误。在C++中同样存在模板函数,也同样存在模板函数的类型推演,在这一点上主要的差异来自于函数声明的语法,见如下C++代码:
1 class ArrayAlgo {
2 public:
3 template<typename T>
4 static T getMiddle(T* a,size_t len) {
5 return a[len/2];
6 }
7 };
8
9 int main()
10 {
11 int v[] = {1,2,3};
12 int ret = ArrayAlgo::getMiddle(v,3);
13 printf("This value is %d.\n",ret);
14 return 0;
15 }
3. 类型参数的限定:有些泛型函数在使用类型参数变量时,经常会用到该类型的特殊方法,如在进行数组元素比较时,要求数组中的元素必须是Comparable接口的实现类,见如下代码:
1 public static <T> T min(T[] a) {
2 if (a == null || a.length == 0)
3 return null;
4 T smallest = a[0];
5 for (int i = 0; i < a.length; ++i) {
6 if (smallest.compareTo(a[i]) > 0)
7 smallest = a[i];
8 }
9 return smallest;
10 }
在以上代码中,数组元素的类型为T,如果该类型并未提供compareTo域方法,将会导致编译错误,如何确保类型参数确实提供了compareTo方法呢?如果T是Comparable接口的实现类,那么该方法一定会被提供,因此可以通过Java语法中提供的类型参数限定的方式来确保这一点。见如下修订代码:
1 public static <T extends Comparable> T min(T[] a) {
2 if (a == null || a.length == 0)
3 return null;
4 T smallest = a[0];
5 for (int i = 0; i < a.length; ++i) {
6 if (smallest.compareTo(a[i]) > 0)
7 smallest = a[i];
8 }
9 return smallest;
10 }
其中的<T extends Comparable>语法保证了类型参数必须是Comparable接口的实现类,否则将会导致编译错误。Java中可以支持多接口的限定,之间用&分隔,如<T extends Comparable & Serializable>和之前的例子一样,尽管同样都会导致编译错误,但是后者不仅会产生更为明确的编译错误信息,同样也使使用者能够更加清晰的看到该方法的使用规则。在标准C++中并未提供这样的限定,但是在C++中对该种方式有另外一种称谓,叫做"类型绑定",在Boost等开源库中通过更复杂的模板技巧模仿了该功能的实现。然而,就泛型的该功能而言,C#的支持也是相当不错的,可参考C#泛型中的which关键字。
在标准C++中,其模板的实现较Java而言更为灵活和强大。对于第一个例子中的代码,只是要求模参必须提供compareTo方法即可通过编译。
1 template<typename T>
2 static T min(T* a,size_t len) {
3 T smallest = a[0];
4 for (int i = 0; i < len; ++i) {
5 if (smallest.compareTo(a[i]) > 0)
6 smallest = a[i];
7 }
8 return smallest;
9 }
注:C++中的模板是在引用时才编译的,因此如果在模板类型中出现任何语法错误,但此时尚未有任何引用时,编译器是不会报错的。
4. 泛型代码中的类型擦除:记得在我阅读Thinking in Java 4th 的时候,书中给出了一些比较明确的解释,为什么Java会这样实现泛型,其最主要的原因是为了考虑向前兼容,也承认这样的实现方式有着很多的缺陷和弊病,希望Java在今后的版本中予以补足。
简单的说类型擦除,就是几乎所有的泛型相关的行为都是由编译器通过暗插各种各样的代码,或者是暗自修订部分代码的声明,然后再将修订后的代码(基本不再包含泛型信息)生成字节码后交给JVM去执行,因此可以据此判断在JVM中对我们的泛型类型是一无所知的。C++也是同样的道理,只是编译器完成的工作被定义为类型展开或类型实例化,因此,同样的模板类,如果实例化的类型参数不同,那么用他们声明出来的类对象也同样不属于相同类型的对象,其限制主要表现为,不能通过缺省copy constructor或者缺省赋值操作符来完成对象之间的复制,除非其中某个类型实例化后的对象专门针对另外一种类型实例化后的类型进行了copy constructor和赋值等于的重载。
1) 类型擦除:将类型参数替换为限定类型,如果没有限定类型则替换为Object,见如下代码:
1 public class Pair<T> {
2 public Pair(T first,T second) { this.first = first; this.second = second; }
3 public T getFirst() { return first; }
4 public T getSecond() { return second; }
5 public void setFirst(T first) { this.first = first; }
6 public void setSecond(T second) { this.second = second; }
7 private T first;
8 private T second;
9 }
由于Pair中的类型参数T没有限定类型,因此类型擦除后将会变成如下代码:
1 public class Pair {
2 public Pair(Object first,Object second) { this.first = first; this.second = second; }
3 public Object getFirst() { return first; }
4 public Object getSecond() { return second; }
5 public void setFirst(Object first) { this.first = first; }
6 public void setSecond(Object second) { this.second = second; }
7 private Object first;
8 private Object second;
9 }
因此尽管在调用Pair时,传递的类型参数有所不同,如String、Date,但是在类型擦除之后,他们将成为相同的类型。如果类型参数存在多个限定类型,则取第一个限定类型作为擦除后的类型参数,见如下代码:
1 public class Interval<T extends Comparable & Serializable> implements Serializable {
2 public Interval(T first, T second) {
3 if (first.compareTo(second) <= 0) {
4 lower = first;
5 upper = second;
6 } else {
7 lower = second;
8 uppper = first;
9 }
10 }
11 private T lower;
12 private T upper;
13 }
擦除类型信息后的原始类型如下:
1 public class Interval implements Serializable {
2 public Interval(Comparable first, Comparable second) {
3 if (first.compareTo(second) <= 0) {
4 lower = first;
5 upper = second;
6 } else {
7 lower = second;
8 uppper = first;
9 }
10 }
11 private Comparable lower;
12 private Comparable upper;
13 }
5. 泛型类向遗留代码的兼容:由于编译器自动完成了类型信息的擦除,因此在原有调用原始类型的地方,可以直接传入等价的泛型类,只要保证该泛型类在类型擦除后可以符合被调用函数参数的语法要求即可,见如下代码:
1 public class TestMain {
2 public static void test(MyClass t) {
3 System.out.println(t.getValue());
4 }
5
6 public static void main(String[] args) {
7 MyClass<Integer> v = new MyClass<Integer>(5);
8 test(v);
9 }
10 }
11 class MyClass<T> {
12 public MyClass(T t) {
13 this.t = t;
14 }
15 public T getValue() { return t;}
16 private T t;
17 }
6. 约束与局限性:
1) 不能使用原始类型作为类型参数,如int、double等,因为他们和Object之间没有直接的继承关系,因此在需要时只能使用包装类,如Integer、Double分别予以替换,不能这样的替换确实也带来了效率上的折损,C++中没有这样的限制,因此模板类的增多只会影响编译的效率和不会影响运行时的效率。
2) 运行时的类型查询只适用于原始类型,即if (a instanceof Pair<String>) 等价于 if (a instanceof Pair)。
3) 泛型类对象调用getClass()方法返回的Class对象都是擦除类型信息的原始Class类型,因此在做比较时,他们将为真,见如下代码:
1 public class TestMain {
2 public static void main(String[] args) {
3 MyClass<Integer> i = new MyClass<Integer>(5);
4 MyClass<Double> d = new MyClass<Double>(5.0);
5 //返回的均为MyClass.Class
6 if (d.getClass() == i.getClass())
7 System.out.println("Type info will be ignored here");
8 }
9 }
10 class MyClass<T> {
11 public MyClass(T t) {
12 this.t = t;
13 }
14 public T getValue() { return t;}
15 private T t;
16 }
17 /* 输入结果:
18 Type info will be ignored here
19 */
4) 泛型类不能实现Throwable接口,换言之泛型类不能成为异常类,否则会导致编译错误。
5) 不能声明参数化类型的数组,如Pair<String>[] table = new Pair<String>[10]; 在擦除类型后将会变为Pair[] table = new Pair[10]; 因此可以执行该转换:Objec[] objarray = table; 由于数组可以记住元素的类型,如果此时试图插入错误的类型元素,将会导致异常ArrayStoreException的抛出。C++中没有该限制。
6) 不能实例化泛型类型的变量,如public Pair() { first = new T(); second = new T();},C++中不存在这样的限制,针对以上写法,类型T只要存在缺省的构造函数即可。如果确实需要实例化类型参数的对象,见如下代码:
1 public static <T> Pair<T> makePair(Class<T> c1) {
2 return new Pair<T>(c1.newInstance(),c1.newInstance());
3 }
4 public static void main(String[] args) {
5 //String.class的类型为Class<String>
6 Pair<String> p = Pair.makePair(String.class);
7 }
这里主要是利用Class类型本身也是泛型类型,可以利用Class<T>的类型参数推演出Pair<T>中T的类型。同样的道理带有类型参数的数组对象也不能直接创建,需要利用Array的反射机制来辅助完成,见如下代码:
1 public static <T extends Comparable> T[] minmax(T[] a) {
2 T[] mm = (T[])Array.newInstance(a.getClass().getComponentType(),a.length);
3 //do something here based on mm
4 return mm;
5 }
7) 泛型类不能应用于静态上下文中,见如下代码:
1 public class Singleton<T> {
2 public static T getInstance() { //Compilation ERROR
3 return singleInstance;
4 }
5 private T singleInstance; //Compilation ERROR
6 }
因为这样的写法在定义Singleton<String>和Singleton<Date>之后,由于类型擦除,将会生成唯一一个Singleton原始共享对象,事实上这并不是我们所期望的结果,在C++中没有这样的限制,甚至有的时候还可以利用这样的机制针对不同类型的对象作声明计数器用,见如下代码:
1 template<typename T>
2 class MyClassCounter {
3 public:
4 MyClassCounter(T t) {
5 _t = t;
6 _counter++;
7 }
8
9 operator T() {
10 return _t;
11 }
12
13 T* operator->() {
14 return &_t;
15 }
16 int getCount() const { return _counter; }
17 private:
18 T _t;
19 static int _counter;
20 }
8) 泛型类不能同时继承或实现只是拥有不同参数类型的同一泛型类,如 public class MyClass implements Comparable<String>, Comparable<Date> {}
7. 泛型类型的继承规则:
1) 如果 public class Manager extends Employee {},那么Pair<Employee> pe = new Pair<Manager>()将会是非常的赋值操作,会导致编译错误。试想如下代码引发的运行时问题。在C++中这种赋值方式同样会导致编译错误,因为他们在类型实例化之后就被视为完全无关的两个类型。
1 public void test() {
2 Pair<Manager> manager = new Pair<Manager>();
3 Pair<Employee> employee = manager; //compilation error
4 employee.setFirst(otherEmployeeButNotManager); //employee的另外一个子类,但不是Manager。
5 }
2) 数组由于在运行时会记住元素的类型,因此数组可以完成这样的赋值,如Manager[] manager = {}; Employee[] employee = manager;如果赋值之后出现错误的元素赋值将会引发ArrayStoreException异常。
3) 泛型类型可以直接赋值给擦除类型后的原始类型,但是同样也会出现2)中数组赋值的问题,只是触发的异常改为ClassCastException,见如下代码:
1 public void test() {
2 Pair<Manager> manager = new Pair<Manager>();
3 Pair rawType = manager;
4 rawType.setFirst("Hello"); //only compilation warning, but will encounter runtime error.
5 }
4) 如果 public class ArrayList<Manager> extends List<Manager> {}, 那么从ArrayList<Manager>到List<Manager>的赋值是允许的,这一点和普通类型是一致的,该规则同样适用于C++。
8. 泛型类型的通配符类型:该泛型特征在标准C++中完全不被支持,C#中存在类似的特征。见以下代码:
1) 子类型限定:
1 public class Manager extends Employee {}
2
3 public static void printBuddies(Pair<Employee> p) {
4 }
5
6 public static void main(String[] args) {
7 printBuddies(new Pair<Employee>()); //legal
8 printBuddies(new Pair<Manager>()); //illegal;
9 }
但是如果将printBuddies改为:void printBuddies(Pair<? extends Employee> p),上例中main函数将可以通过编译。<? extends Employee>的语义为所有Employee的子类都可以做printBuddies函数参数的类型参数。对于7-1)中的示例代码,如果改为通配符类型将可以通过编译并正常运行,但是仍然存在一定的限制,见如下代码:
1 public void test() {
2 Pair<Manager> manager = new Pair<Manager>();
3 Pair<? extends Employee> employee = manager; //legal here.
4 //由于otherEmployeeButNotManager虽为Employee子类,但可能并非Manager类,
5 //由于setFirst的参数将会声明为void setFirst(? extends Employee),由于
6 //编译器无法确定setFirst参数的实际类型,因此将会直接报告编译错误。
7 employee.setFirst(otherEmployeeButNotManager); //compilation error
8 }
和setFirst相比,getFirst将会正常编译并运行,因为返回值无论是什么子类型,都不会带来影响和破坏。
2) 超类型限定:Pair<? super Manager>表示参数类型一定是Manager的超类。因此和子类型限定刚好相反,setFirst将是合法的,而getFirst将会产生编译错误。
3) 无限定通配符,如Pair<?>,该泛型类型的setFirst(?)方法不能被调用,即便传入的参数是Object,这样是Pair<?>和Pair之间最大的差异。该方法还有一个比较重要的作用就是用于提示泛型函数的调用者,该泛型函数更期望参数是带有类型参数的泛型类型,而不是原始类型,即便原始类型也可能正常的工作,见如下代码:
1 public class TestMain {
2 @SuppressWarnings("unchecked")
3 //public static <T> T print(MyClass myclass),同样可以正常的工作,
4 //但是会有编译警告产生。
5 public static <T> T print(MyClass<?> myclass) {
6 myclass.print();
7 return (T)myclass.get();
8 }
9
10 public static void main(String[] args) {
11 Integer ii = new Integer(5);
12 print(new MyClass<Integer>(ii));
13 }
14 }
15
16 class MyClass<T> {
17 public MyClass(T t) {
18 _t = t;
19 }
20 T get() { return _t;}
21
22 public void print() {
23 System.out.println(_t);
24 }
25 private T _t;
26 }