对于我这种写惯了C 的人来说Java的泛型真的很难用。运行时没有类型信息,进一步导致像是jackson
之类的库在做convertValue
之类的操作时,方法虽然是个泛型方法,但是还得带上一个Class<T>
的参数才能做转换。
自从有了kotlin,一切都不一样了。
jackson-kotlin-module提供了基于reified
的简化版本
inline fun <reified T> ObjectMapper.convertValue(from: Any): T = convertValue(from, jacksonTypeRef<T>())
对比Java的版本
代码语言:txt复制public <T> T convertValue(Object fromValue, TypeReference<?> toValueTypeRef)
少了个参数简直爽爆了
实现
那这具体是怎么实现的呢?
我们知道Java的泛型只是编译时做的参数检查,运行时并没有保留任何信息,任何泛型类/方法也是普通的类/方法。
kotlin也是基于Jvm平台的,那kotlin中是如何实现泛型参数传递的呢。我们可以通过反编译字节码一窥究竟。
`kotlin` fun main() { jacksonObjectMapper().readValue<String>("") }inline fun <reified T> ObjectMapper.readValue(content: String): T = readValue(content, jacksonTypeRef<T>()) inline fun <reified T> jacksonTypeRef(): TypeReference<T> = object: TypeReference<T>() {}
上面kotlin代码编译后的字节码可以反编译可以得到类似下面Java代码
代码语言:txt复制public final class AppKt$main$$inlined$readValue$1 extends TypeReference<String>
{
}
public final class AppKt
{
public static final void main()
{
ObjectMapper localObjectMapper1 = ExtensionsKt.jacksonObjectMapper();
String content$iv = "";
int $i$f$readValue;
ObjectMapper $receiver$iv;
String str1 = content$iv;
ObjectMapper localObjectMapper2 = $receiver$iv;
int $i$f$jacksonTypeRef;
TypeReference localTypeReference = (TypeReference)new AppKt.main..inlined.readValue.1(); localObjectMapper2.readValue(
str1, localTypeReference);
}
}
可以看到由于inline的关系kotlin的readValue
倍直接展开到main函数中
另外jacksonTypeRef<T>
被转换换成AppKt$main$$inlined$readValue$1
类型直接包含了泛型参数String
看起来挺简单的就是直接展开代码嘛,那和C 一样泛型满天飞不就行了么。
限制
其实即便kotlin用inline实现了泛型代码运行时携带泛型信息,也没有达到C 模板展开的层次。具体来说就是下面这个例子
代码语言:txt复制data class Wrapper<T>(val data:T)
data class A(val a: Int)
inline fun <reified T> convert(a: String): Wrapper<T> {
return jacksonObjectMapper().readValue(a)
}
fun main() {
println(convert<A>("""{"data":{"a":1}}"""))
println(jacksonObjectMapper().readValue<Wrapper<A>>("""{"data":{"a":1}}"""))
}
输出
代码语言:txt复制Wrapper(data={a=1})
Wrapper(data=A(a=1))
可以看到convert
方法没有把data字段转换成类A
,而是Map
回头看一下反编译的Java代码就很清晰了
代码语言:txt复制public final class AppKt
{
private static final <T> Wrapper<T> convert(String a)
{
int $i$f$convert = 0; ObjectMapper $receiver$iv = ExtensionsKt.jacksonObjectMapper();
int $i$f$readValue;
String str = a; ObjectMapper localObjectMapper1 = $receiver$iv;
int $i$f$jacksonTypeRef;
TypeReference localTypeReference = (TypeReference)new AppKt.convert..inlined.readValue.1();
return (Wrapper)
localObjectMapper1.readValue(
str, localTypeReference);
}
public static final void main()
{
String a$iv = "{"data":{"a":1}}"; int $i$f$convert = 0;
ObjectMapper $receiver$iv$iv = ExtensionsKt.jacksonObjectMapper();
int $i$f$readValue;
Object localObject1 = a$iv; Object localObject2 = $receiver$iv$iv;
int $i$f$jacksonTypeRef;
TypeReference localTypeReference = (TypeReference)new AppKt.convert..inlined.readValue.2();
a$iv = (Wrapper)
((ObjectMapper)localObject2).readValue(
(String)localObject1, localTypeReference);
System.out.println(a$iv);
a$iv = ExtensionsKt.jacksonObjectMapper(); String content$iv = "{"data":{"a":1}}";
int $i$f$readValue;
$receiver$iv$iv = content$iv; localObject1 = $receiver$iv;
int $i$f$jacksonTypeRef;
localObject2 = (TypeReference)new AppKt.main..inlined.readValue.1();
ObjectMapper $receiver$iv =
((ObjectMapper)localObject1).readValue(
$receiver$iv$iv, (TypeReference)localObject2);
System.out.println($receiver$iv);
}
}
public final class AppKt$convert$$inlined$readValue$1 extends TypeReference<Wrapper<T>>
{
}
public final class AppKt$convert$$inlined$readValue$2 extends TypeReference<Wrapper<T>>
{
}
public final class AppKt$main$$inlined$readValue$1 extends TypeReference<Wrapper<A>>
{
}
对于convert
方法使用的AppKt$convert$$inlined$readValue$2
没有包含A类型,因此转换的时候并不会转换成A,而是普通的Map
那为什么会这样呢,我们可以看到这里的convert
方法是inline的因此,main中调用的地方并不会直接调用convert
方法,而是将convert
方法的代码在main
中展开
因为convert
的代码并不知道T是什么类型,因此生成的中间类型也没有具体的参数
那为什么直接调用jackson的extesion有效呢?
总结起来就是一句话,inline reified方法的泛型参数必须就地使用,不能传递给别的inline reified方法。