Java 类型推导的不确定性

96
qc1iu
2018.05.17 11:33 字数 800

本文是读完oopsla16的一些心得

Figure 1

上面这个测试例运行的结果是,main函数中抛出一个ClassCastException,异常信息为 “java.lang.Integer cannot be cast to java.lang.String”。原因简单说就是因为JVM运行时检查出了类型问题,发现代码企图将一个Integer类型的对象转化为String类型,这种转换是不允许的。

但是,这种类型问题本应该是在编译期检查出来的。这里是什么导致编译器检查不出这个类型错误?

我们模拟一下Java类型推导的过程。类型推导总是先假设程序类型是对的,然后在这个前提下得出一些条件,最后验证这些条件是否成立。用类型Z代替Constrain<U, ? super T>中的,可以得到Figure 2中的信息。

Figure 2

a)和b)是显而易见的。c)是因为upcast函数的第一个参数的类型是Constrain<A, B>, 而这里传入了一个合法的参数,参数类型为Constrain<U, ? super T>。由于将U作为Bind的类型构造符,所以得到Z==B。upcast第二个参数的类型为T,而声明的类型为B,因此得到T<:B,因为B<:A,这里A已经因为Bind<U>而变为U,因此B<:U。

对于c), d), e)3个条件,Figure-1的例子中c)是满足的,因为upcast接受了一个合法的Constrain<U, Z>类型的参数。由于c)条件满足,所以d)和e)也满足了。
但是, Figure-1的正确仅仅是一个巧合。原因是Java类型推导验证条件的顺序是不确定的。如果顺序的验证条件c), d), e),那么条件满足。如果先验证d),e),再验证c),那么T<:U这个条件就不能满足了。所以不同编译器在编译Figure-1时结果是不同的。

不过我个人认为,造成这种类型推导结果不一致的原因并不能完全归结于条件的验证顺序。如果Java像Ocaml一样严格的区分Some和None,那么对于Figure-1的例子,我们就不可能向upcast函数传一个合法的Constrain<U, ? super T>类型的对象。

oopsla16中还给出了一些其他例子,用于说明Java类型推导的不一致性。

Ocaml 实现

type ('a, 'b) eq = Refl : ('a, 'a) eq



let cast (type a) (type b) (eq: (a, b) eq) (x : a) : b =
  match eq with
  | Refl -> x

let lies : (int, string)eq = Obj.magic 0;;

cast lies 42

上面的Ocaml也实现了一个与Figure-1一样的问题的例子。执行后内存错直接导致ocaml解释器崩溃。不过与Java不同的是,构造(int ,string)eq类型需要使用Obj.magic作为hack手段,而Java直接使用null就可以。

结论

  1. Java的类型推导是难以捉摸的,因此使用时应该尽可能的给出类型参数信息。
  2. 强类型语言的确更安全一些。

参考文献:
[1] Java and Scala’s Type Systems are Unsound
[2] Java's unsoundness (from an ML-ish point of view)

Technical Articles
Web note ad 1