主要介绍java泛型,再与C++模板作一个简单的比较。

1 Java的泛型(Generic Type)

Java泛型基于类型擦除,使用了泛型的代码在编译后看不到“泛型语法”,泛型可以理解为语法糖,是写给编译器看的,虚拟机里没有泛型的概念。

1.1 为什么要使用泛型

java设计之初并没有泛型,比如,在jdk1.5以前,ArrayList操作的对象都是Object,使用时需要进行手动转换。

举个例子,我们构造一个数组,用来保存String类型的字符串,然后打印每一个字符串的长度:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import java.util.ArrayList;

public class Demo {
    public static void main(String[] args) {
        ArrayList arr=new ArrayList();
        arr.add("1");
        arr.add("22");
        arr.add("333");
        for(int i=0;i<arr.size();i++){
            String str=(String)arr.get(i);//对取出的元素执行转换,Object->String
            System.out.println("第"+(i+1)+"个字符串的长度是:"+str.length());
        }
    }
}

输出结果:

1
2
3
第1个字符串的长度是:1
第2个字符串的长度是:2
第3个字符串的长度是:3

然后我们再往数组中添加一个元素,但是输入数据时少打了个双引号,原本想要输入字符串却输入了一个整数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import java.util.ArrayList;

public class Demo {
    public static void main(String[] args) {
        ArrayList arr=new ArrayList();
        arr.add("1");
        arr.add("22");
        arr.add("333");
        arr.add(4444);//忘记了双引号,添加了一个整数
        for(int i=0;i<arr.size();i++){
            String str=(String)arr.get(i);//对取出的元素执行转换,Object->String
            System.out.println("第"+(i+1)+"个字符串的长度是:"+str.length());
        }
    }
}

重新运行这段代码,输出结果:

1
2
3
4
5
第1个字符串的长度是1
第2个字符串的长度是2
第3个字符串的长度是3
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
	at Demo.main(Demo.java:11)

从结果可以看到,在循环运行时,前面3次都正确打印出了结果,而到第4次执行到第11行-类型转换时出错了,因为我们在对应位置存储的是一个整数,而不是字符串,这是一个运行时错误(RuntimeException)

通过上面的例子,我们可以看到“手动转换”来保证类型正确的弊端:

  • 每次取出数据都要手动转换,这一点在编译时可以检查
  • 没法在编译时检查数据输入的合法性,一旦出错,就是RuntimeException

引入泛型以后,上面的例子就简单多了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import java.util.ArrayList;

public class Demo {
    public static void main(String[] args) {
        ArrayList<String> arr=new ArrayList<>();
        arr.add("1");
        arr.add("22");
        arr.add("333");
//        arr.add(4444); //编译时会进行类型检查,报错:java: incompatible types: int cannot be converted to java.lang.String
        for(int i=0;i<arr.size();i++){
            String str=arr.get(i);//无需类型转换,直接就是String
            System.out.println("第"+(i+1)+"个字符串的长度是:"+str.length());
        }
    }
}

泛型让你的代码简单又可靠。

1.2 泛型实现–类型擦除

泛型在编译的时候,具体的类型参数会被擦除,取而代之的是它的第一个“限定类型”,也就是其继承的类或者实现的接口,如果都没有的话,自然就是Object类了。编译器会在编译时检查你的代码,如果有错误的类型转换出现(如给泛型List添加一个错误类型的对象或者把从List取出的对象赋值给一个错误类型的变量),则会报错,无法通过编译。所以泛型实际上是把运行时的类型转换检查提前到了编译时,避免运行时异常

所以,在判断泛型所属类型的时候,不管你传入参数的是什么类型,泛型始终都是同一个类型:

1
2
3
4
5
6
7
8
9
import java.util.ArrayList;

public class Demo {
    public static void main(String[] args) {
        ArrayList<String> arr=new ArrayList<>();
        ArrayList<Integer> arrInt=new ArrayList<>();
        System.out.println(arr.getClass()==arrInt.getClass());//true
    }
}

PS:虽然进行了“类型擦除”,class文件中仍然有关于被擦除类型的信息,被保存在LocalVariableTypeTable中。

关于“限定类”,有几点要注意:

  • 可以是一个或者多个"限定类型",但是最多只能有一个是类,其它的必须是接口,这跟类继承是一样的,但是用&符号连接,因为逗号用来分隔不同的类型参数
  • 如果有一个是类,那么类写在第一个位置,如<T extends ClassA&InterfaceB&InterfaceC>
  • 如果全部是接口,把最复杂的接口写在最前面可以提高效率。因为类型擦除是转换成第一个"限定类型",如果第一个只是一个标记接口,那么后面用到其它接口的地方,还要再加入强制转换。

1.3 泛型的一些限制

泛型有一些应用限制,其中一部分就是因为类型擦除。

  • 不能传入8大原始类型。int,short,long,byte,boolean,double,float,char
  • 泛型中的类型参数不能被实例化,但是可以通过反射,用Class<T>newInstance()方法获得实例(JDK9以后官方推荐用Class<T>.getDeclaredConstructor().newInstance())
  • 泛型中的类型参数不能被声明为静态的
  • 不能对带有类型参数的泛型表达式使用instance of,(大部分时候)也不能进行类型转换
  • 不能创建泛型数组,Object[] stringLists = new List<String>[]; // compiler error
  • 泛型类不能继承异常类型,如AException<T> extends Throwable//compile error,也不能捕捉T,但是可以throws T
  • 不能重载一个仅仅是泛型参数类型不同的方法,因为类型擦除后,他们是一样的。

2 C++模板(template)

c++的模板就跟它的名字一样,定义的时候它是一个模板,后面使用时,编译器会把模板中的类型替换为具体的类型,生成一份“根据模板制作”的代码。c++有两种模板,类模板和函数模板,模板本身不是类或函数,可以理解创建类或函数的公式。

2.1 模板语法

以函数模板举例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//定义模板
template <typename T>
int compare(const T &v1,const T &v2){
    if(v1 < v2) return -1;
    if(v2 < v1) return 1;
    return 0
}

//使用模板
cout<< compare(0,1) <<endl;
//在上面的调用中,编译器会自动为函数模板推断类型为int,并生成下面的代码
int compareconst int &v1,const int &v2){
    if(v1 < v2) return -1;
    if(v2 < v1) return 1;
    return 0
}

3 区别

3.1编译阶段的处理方式不同

java 在编译时直接把泛型中的类型参数擦除,并在需要地方插件强制类型转换。而c++在编译到模板定义的代码时,并没有任何操作,只在使用到这份模板代码时,根据实际类型,为这个类型生成一份特定的代码。这一点其实是本质区别,后面的几点区别都是因为这个。

3.2可以接收的参数不同

java 泛型不能接收8大原始类型,c++没有这个限制。java在类型擦除时,把类型“退化”成它的限定类型,而原始类型比较特殊,无法“退化”成Object。java可以接收通配符,c++不能。c++不能接收类型限定,虽然可以通过其它手段,如c++20concept来实现类似的功能。

3.3 效率不同

java的类型擦除会增加很多类型转换的操作,效率低下。而c++模板生成一份特定类型的代码,这跟你自己手写一份和“模板”逻辑一样的代码没有什么区别。

4 总结

java选择这种看起来缺点很多的泛型实现方式,根本原因就是为了一个优点“兼容性”。java泛型和c++模板本质上还是差别挺大的,c++的模板像宏,java泛型像语法糖。