kotlin中的reified
最近公司开了一个新项目,让尝试用Kotlin
来编写。所以这段时间一直在看Kotlin
,其中在学习泛型的时候,看到reified
关键字(具体化参数)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| inline fun <reified T> f0(): Class<T> { return T::class.java }
inline fun <reified T> f1(any: Any): Boolean { return any is T }
fun main() { println(f0<String>()) println(f0<Int>()) println(f1<Int>("hello")) println(f1<String>("world")) }
|
上面的例子中被reified
修饰的泛型T
在运行时可以拿到具体化类型信息,瞬间有种真泛型
的感觉。很遗憾是reified
关键字只能在inline
函数种使用。熟悉inline
函数的朋友已经大概知道了实现的原理。被inline
修饰的函数调用处在编译时都会对其进行展开。上面的代码编译展开后类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| inline fun <reified T> f0(): Class<T> { return T::class.java }
inline fun <reified T> f1(any: Any): Boolean { return any is T }
fun main() { println(String::class.java) println(Integer::class.java) println("hello" is Int) println("world" is String) }
|
我们可以看出被编译后的代码,完全不存在函数f0/f1
的调用,随着inline
函数的编译时展开,其泛型T
的调用处编译后也被具体类型
所替代。reified
完全依赖inline
函数的编译时展开所实现。我们在普通函数中并不能使用reified
。
关于kotlin的inline关键字,可以参见《kotlin中的inline》。
Java中的泛型
Java
从1.5版本开始引入泛型,泛型的引入使我们有了参数化类型,有了类型安全的容器。一直以来Java
所使用的编译时擦除的方式来实现的泛型,在社区中经常被开发者诟病。泛型在编译时是如何进行擦除的?Java
的泛型是否真的如此不堪?
现在假设程序员A
开发了一个带有泛型的Pair
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class Pair<K, V> { private K key; private V value;
public void put(K key, V value) { this.key = key; this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
@Override public String toString() { return String.format("[%s : %s]", key, value); } }
|
由于泛型的擦除上面的Pair
类编译后其泛型都会被其上限的具体类型所替代
由于K,V没有指定上限,其上限则是Object
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Pair { private Object key; private Object value;
public void put(Object key, Object value) { this.key = key; this.value = value; }
public Object getKey() { return key; }
public Object getValue() { return value; }
@Override public String toString() { return String.format("[%s : %s]", key, value); } }
|
这就是很多人通常意义上所理解的Java泛型擦除。
声明处的泛型信息会得到保留
如果程序员A
将Pair类
构建为一个jar包
扔给了程序员B
,让其使用。那么Pair类
中所定义的泛型能否对程序员B
做到约束作用呢?有过开发经验的朋友一定知道这是可以的。程序员A
给程序员B
的是jar包
也就是编译后的字节码
而不是不是源代码
,所以我们可以得出一个结论:所有定义处的范型信息编译后在字节码中都会得到保留,一个很简单的例子,如果被擦除了,你所用到的第三方框架中定义的泛型信息将对你起不到约束作用。
在定义处的泛型被编译后都会通过signature
保留
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public class com/ybq/java/generic/type/Pair {
public put(Ljava/lang/Object;Ljava/lang/Object;)V
public getKey()Ljava/lang/Object;
|
signature
中泛型信息是给编译器看的,用于检查代码。JVM在运行时不会用signature
中泛型信息做类型检验检查。但是我们在运行时可以通过Class元数据
读取signature
中的泛型信息
1 2 3 4 5 6
| System.out.println(Pair.class.getTypeParameters()[0]); System.out.println(Pair.class.getTypeParameters()[1]); System.out.println(Pair.class.getMethod("getKey", null).getGenericReturnType()); System.out.println(Pair.class.getMethod("getValue", null).getGenericReturnType()); System.out.println(Pair.class.getMethod("put", Object.class, Object.class).getGenericParameterTypes()[0]); System.out.println(Pair.class.getMethod("put", Object.class, Object.class).getGenericParameterTypes()[1]);
|
上面的代码输出的结果
泛型在具体化时被擦除
1 2 3
| Pair<String, Integer> pair = new Pair<String, Integer>(); pair.put("age", 18); System.out.println(pair.getClass());\\ com.ybq.java.generic.type.Pair
|
上面的代码我们创建了一个Pair<K,V>
类的实例,我们为泛型K
V
指定具体类型String
和Integer
。关于String
和Integer
的信息在编译后是真正的被彻底的擦除了,在运行时我们无法得到其任何相关信息。
Java中的泛型约束只是编译器在做,Jvm方面没有对泛型做任何支持,在运行时无论是new Pair<String, Integer>()
还是new Pair<Integer,String>()或new Pair()
三者无任何区别。我们可以通过反射或字节码注入等方式绕开编译器的限制,向new Pair<String, Integer>()
中put
任何类型的数据。
C++中的模板
类似于Java
的泛型在C++
中叫做模板
,其实现方式和宏定义
、inline
的思想相同:编译时展开,也就是在编译时做代码膨胀。
1 2
| Pair<String, Integer> pair0 = new Pair<String, Integer>(); Pair<Integer, String> pair1 = new Pair<Integer, String>();
|
上面的Java代码,倘若按照C++模版的实现方式做编译则是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| public class SIPair { private String key; private Integer value;
public void put(String key, Integer value) { this.key = key; this.value = value; }
public String getKey() { return key; }
public Integer getValue() { return value; }
@Override public String toString() { return String.format("[%s : %s]", key, value); } }
public class ISPair { private Integer key; private String value;
public void put(Integer key, String value) { this.key = key; this.value = value; }
public Integer getKey() { return key; }
public String getValue() { return value; }
@Override public String toString() { return String.format("[%s : %s]", key, value); } }
...
SIPair pair0 = new SIPair(); ISPair pair1 = new ISPair();
|
按照C++模板
的思想,编译时会对new Pair<String, Integer>()
和new Pair<Integer, String>()
生成对应的其类型STPair
和TSPair
。所以pair0
和pair1
在运行时是两种不同类型。与Java
所使用的擦除
方式相比,模板
方式在运行时有了具体类型,缺点则是生成的程序占用空间也会变大。
C#中的泛型
相对于Java和C++的实现,C#
的泛型一直被成为真泛型
,因为CLR
(C#的编译后的中间码的运行环境,类似于JVM)认识泛型。在C#
中new Pair<string, int>()
编译后的string
和int
都会得到保留。new Pair<string, int>()
和new Pair<int, string>()
也是两种不同的类型,在运行由CLR
为其生成不同的类型。不同于C++的编译时展开,C#
的泛型则由CLR
在运行时进行展开,也就是运行时膨胀。
手动做代码膨胀保留Java泛型具体化信息
上面我们提到Java的泛型在声明处的信息会得到保留,具体化时的信息会被完全擦除。那么我们有没有方法可以在运行时获取泛型的具体化信息呢?
我们可以通过匿名内部类的方式,手动做代码膨胀,把泛型的具体化信息转为泛型的声明信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| public class ReifiedTypePair<K, V> { private K key; private V value;
protected ReifiedTypePair() { }
public Type getKeyType() { return getGenericType(0); }
public Type getValueType() { return getGenericType(1); }
public Type getGenericType(int index) { Type superclass = getClass().getGenericSuperclass(); ParameterizedType parameterized = (ParameterizedType) superclass; return parameterized.getActualTypeArguments()[index]; }
public void put(K key, V value) { this.key = key; this.value = value; if (getKeyType() == String.class) { this.key = (K) ((String) this.key).toUpperCase(); } }
public K getKey() { return key; }
public V getValue() { return value; }
@Override public String toString() { return String.format("[%s : %s]", key, value); } } public class Demo { public static void main(String args[]) { ReifiedTypePair reifiedTypePair = new ReifiedTypePair<String, Integer>() {}; reifiedTypePair.put("age", 18); System.out.println(reifiedTypePair.getKeyType()); System.out.println(reifiedTypePair.getValueType()); System.out.println(reifiedTypePair); } }
|
上面的代码输出结果为:
1 2 3
| class java.lang.String class java.lang.Integer [AGE : 18]
|
我们通过new ReifiedTypePair<String, Integer>(){}
也就是匿名内部类的方式为ReifiedTypePair
创建了一个实例。通过匿名类方式在编译时会为ReifiedTypePair<K,V>
生成一个具体的匿名子类,相关的具体化泛型信息(<String,Integer>
)也会伴随的子类的生成转化到声明处,上面的代码编译后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ...
class Demo$1 extends ReifiedTypePair<String, Integer> { Demo$1() { } }
...
ReifiedTypePair reifiedTypePair = new Demo$1(); reifiedTypePair.put("age", 18);
...
|
我们在使用Gson
对持有泛型信息的数据做反序列化的时候,使用的也是这种方式
1 2
| List<String> data = new Gson().fromJson("", new TypeToken<List<String>>() { }.getType());
|
在Gson
中我们通过TypeToken<T>
获取泛型信息,TypeToken<T>
的构造方法被定义为了protected
,所以我们在使用时只能通过匿名内部类
,或手动创建其子类
来做实例化。目的就是为了让泛型信息保留在声明处。
总结
对于泛型,Java
的实现是编译时擦除
,C++
是在编译时膨胀
,而C#
则是在运行时膨胀
。三种编程语言的实现方式各不相同。就Java
为什么会采用编译时擦除
的方式来实现,网上后很多讨论,我的主观看法是:有兼容性的考虑,最主要的原因还是简单省事,编译时擦除方式实现的泛型几乎没有虚拟机
什么事,改改编译器
就行了。当然泛型
本身就是语言层面的概念,所以并没有真正意义上所谓的真泛型
和伪泛型
。回到文章的标题,Jvm不认识泛型。