Java的types擦除有什么好处?

我今天看了一条推文 ,说:

当Java用户抱怨types擦除是Java的唯一的东西,而忽略所有错误的东西时,这很有趣。

因此我的问题是:

Java的types擦除有好处吗? 它可能提供的技术或编程风格有哪些优点,而不是JVM实现偏好,以实现向后兼容性和运行时性能?

types擦除是好的

让我们坚持事实

到目前为止,许多答案都过分关注Twitter用户。 专注于信息而不是信使是有帮助的。 有一个相当一致的信息,即使是刚才提到的摘录:

当Java用户抱怨types擦除是Java的唯一的东西,而忽略所有错误的东西时,这很有趣。

我获得了巨大的收益(如参数)和零成本(所谓的成本是一个想象的极限)。

新的T是一个破碎的程序。 “所有的命题都是真的”这种说法是同构的。 我不是很大。

目标:合理的scheme

这些推文反映了一个观点,对我们是否可以让机器做些什么不感兴趣,而更多的是我们能否推断机器会做我们真正想要的事情。 好的推理是一个certificate。 certificate可以用正式表示法或不太正式的方式指定。 不pipe规范语言如何,他们都必须清晰严谨。 非正式的规范并不是不可能正确的结构,但在实际的编程中往往是有缺陷的。 我们最终采取了自动化和探索性testing等补救措施,以弥补我们用非正式推理所遇到的问题。 这并不是说testing本质上是一个坏主意,但是被引用的Twitter用户认为有更好的方法。

所以我们的目标是要有一个正确的程序,我们可以清楚而严谨地按照机器如何实际执行程序的方式进行推理。 但是,这不是唯一的目标。 我们也希望我们的逻辑具有一定程度的expression能力。 例如,我们只能用命题逻辑来expression这么多东西。 从一阶逻辑这样的东西中获得普遍的(∀)和存在(∃)量化是很好的。

使用types系统进行推理

types系统可以很好地解决这些目标。 这是因为咖喱霍华德通信特别清楚。 这种对应往往用以下类比来expression:types就是程序,因为定理是certificate的。

这个通信有点深刻。 我们可以采取逻辑expression式,并通过对应的types来翻译它们。 那么如果我们有一个具有相同types签名的程序,我们已经certificate了逻辑expression式是普遍真实的(重言式)。 这是因为信件是双向的。 types/程序和定理/certificate世界之间的转换是机械的,在很多情况下可以自动化。

库里 – 霍华德很好地扮演着我们想要做的与一个节目的规格。

types系统在Java中有用吗?

即使了解了库里 – 霍华德(Curry-Howard),有些人发现,当一个types系统的价值被忽略时,它很容易被忽视

  1. 是非常困难的工作
  2. 通过库里 – 霍华德(Curry-Howard)对应于具有有限expression性的逻辑
  3. 被破坏了(系统被认定为“弱”或“强”)。

关于第一点,也许IDE使Java的types系统足够容易处理(这是非常主观的)。

关于第二点,Java 几乎对应于一阶逻辑。 generics给出了通用量化的types体系。 不幸的是,通配符只能给我们一小部分的存在量化。 但是普遍量化是相当好的开始。 能够说List<A>函数对于所有可能的列表都是普遍的,因为A是完全不受限制的。 这导致了Twitter用户在“参数性”方面正在讨论的内容。

关于参数性的经常被引用的论文是Philip Wadler's Theorem for free! 。 本文的有趣之处在于,仅仅从types签名就可以certificate一些非常有趣的不variables。 如果我们为这些不变式编写自动化testing,那么我们将会浪费大量的时间。 例如,对于List<A> ,从单独的types签名中flatten

 <A> List<A> flatten(List<List<A>> nestedLists); 

我们可以推断这一点

 flatten(nestedList.map(l -> l.map(any_function))) ≡ flatten(nestList).map(any_function) 

这是一个简单的例子,你也许可以非正式地推理它,但是当我们从types系统中正式获得这样的certificate并且被编译器检查的时候更好。

不擦除可能导致滥用

从语言实现的angular度来看,Java的generics(对应于通用types)在参数化中扮演着非常重要的angular色,用来certificate我们程序的function。 这提到了第三个问题。 所有这些certificate和正确性的提高都需要一个没有缺陷的健全的types系统。 Java肯定有一些语言特性,让我们粉碎了我们的推理。 这些包括但不限于:

  • 与外部系统的副作用
  • reflection

未擦除的仿制药在许多方面与反思有关。 如果没有擦除,那么我们可以使用实现来运行运行时信息,以devise我们的algorithm。 这意味着静态地说,当我们推理节目的时候,我们没有完整的画面。 反思严重威胁我们对静态理由的任何证据的正确性。 反思也不是巧合,会导致各种棘手的缺陷。

那么未擦除的generics可能是“有用的”方法呢? 让我们考虑推文中提到的用法:

 <T> T broken { return new T(); } 

如果T没有没有参数的构造函数,会发生什么? 在某些语言中,你得到的是null。 或者,也许你跳过空值,并直接引发exception(无论如何,这些空值似乎导致)。 因为我们的语言是图灵完整的,所以不可能推断哪个调用会broken会涉及到没有参数的构造函数的“安全”types,哪些不会。 我们失去了我们的程序普遍工作的确定性。

擦除意味着我们已经推理(所以让我们抹去)

所以如果我们想要推理我们的程序,我们强烈build议不要使用强烈威胁我们推理的语言特征。 一旦我们这样做,那么为什么不在运行时删除types呢? 他们不需要。 我们可以得到一些效率和简单的满意度,不会失败或调用时可能会丢失方法。

擦除鼓励推理。

types是一种用于编写程序的结构,允许编译器检查程序的正确性。 一个types是一个价值的命题 – 编译器validation这个命题是真实的。

在程序执行过程中,不需要types信息 – 这已经被编译器validation了。 编译器应该可以自由地丢弃这些信息,以便对代码执行优化 – 使其运行速度更快,生成更小的二进制文件等。擦除types参数有助于实现这一点。

Java通过允许在运行时查询types信息来打破静态的input – reflection,instanceof等。这允许你构build不能被静态validation的程序 – 它们绕过types系统。 它也错过了静态优化的机会。

types参数被删除的事实防止了这些不正确的程序的一些实例的被构build,然而,如果删除了更多的types信息并且删除了reflection和实例设施,将会不允许更多不正确的程序。

擦除对于保持数据types的“参数性”属性是重要的。 假设我有一个types为“List”的参数化的组件typesT.即List <T>。 这种types是一个命题,这个Listtypes对于任何typesT的作用是相同的。T是一个抽象的,无界的types参数的事实意味着我们对这个types一无所知,因此在T的特殊情况下被阻止做任何特殊的事情。

例如说我有一个List xs = asList(“3”)。 我添加一个元素:xs.add(“q”)。 我以[“3”,“q”]结束。 由于这是参数,我可以假设List xs = asList(7); xsadd(8)以[7,8]结束我从types知道,它不会为String做一件事,为Int做一件事。

而且,我知道List.add函数不能发明出T的值。 我知道,如果我的asList(“3”)加了一个“7”,唯一可能的答案将由值“3”和“7”构成。 列表中没有添加“2”或“z”的可能性,因为函数将无法构造它。 这些其他值都不会是明智的添加,参数性可以防止这些不正确的程序被构build。

基本上,擦除防止了一些违反参数的手段,从而消除了不正确程序的可能性,这是静态打字的目标。

同一个用户在同一个对话中的后续文章:

新的T是一个破碎的程序。 “所有的命题都是真的”这种说法是同构的。 我不是很大。

(这是为了回应另一个用户的陈述,即“在某些情况下'新的T似乎会更好'”,这个想法是由于types删除, new T()是不可能的(这是有争议的 – 甚至如果T在运行时可用,它可能是一个抽象类或接口,或者它可能是Void ,或者它可能缺less一个无参数构造函数,或者它的无参数构造函数可能是私有的(例如,因为它应该是一个单例类),或者它的无参数构造函数可以指定一个通用的方法没有捕获或指定的检查exception – 但这是前提,不pipe怎样,没有擦除,至less可以写T.class.newInstance() ,处理这些问题。))

这种观点认为,这些types同构于命题,表明用户具有formstypes理论的背景。 (S)他很可能不喜欢“dynamictypes”或者“运行时types”,并且更喜欢没有downcast和instanceof和reflection等的Java。 (想象一下像标准ML这样的语言,它有一个非常丰富的(静态)types系统,其dynamic语义不依赖于任何types的信息。

顺便说一下,用户需要记住的是:虽然他可能真正喜欢(静态)types化的语言,但是他并不是真诚地试图说服其他人这种观点。 相反,原来推特的主要目的是模仿那些不同意的人,在一些不同意者之后,用户发布了后续推文,比如“java删除types的原因是Wadler等人知道什么他们正在做,不像用户的Java“。 不幸的是,这使得很难发现他实际上在想什么, 但幸运的是,这也可能意味着这样做并不重要。 真正有深度的人通常不会求助于那些没有内容的巨魔。

在这里我没有看到的一件事是OOP的运行时多态性从根本上取决于运行时types的具体化。 当一种语言的骨干通过反思式被引入到语言types系统的一个重要的扩展中,并且基于types的删除,认知失调是必然的结果。 这正是Java社区发生的事情; 这就是为什么types擦除引起了如此多的争议,并最终为什么有计划在未来的Java版本中取消它 。 在Java用户的抱怨中发现一些有趣的事情,或者是对Java精神的一个诚实的误解,或者是一个有意识的贬低的笑话。

“擦除是Java唯一正确的”声明暗示了“所有基于dynamic调度的函数参数的运行时types的语言都有根本的缺陷”。 虽然它本身是一个合法的主张,甚至可以被认为是包括Java在内的所有OOP语言的有效批评,但它不能作为一个关键点来评估和批评Java环境中的特性, 在这种情况下 ,运行时多态是公理的。

总之,虽然可以有效地说“types擦除是语言devise的一种方式”,但支持Javatypes擦除的位置是错误的,因为它太多了,太迟了,甚至在历史时刻当时Oak被Sun接受并重新命名为Java。



至于静态types本身是否是编程语言devise的正确方向,这符合我们认为构成编程活动的更广泛的哲学环境。 显然源于math古典传统的一门思想学派把程序视为一个math概念或其他(命题,函数等)的实例,但是有一种完全不同的方法,它把程序devise看作是一种方法与机器交谈并解释我们想要的东西。 从这个angular度来看,这个计划是一个dynamic的,有机的增长实体,与静态types程序的精心devise大相径庭。

把dynamic语言看作是朝着这个方向迈出的一步似乎是自然不过的:程序的一致性是从下到上的,没有先天的限制,会自上而下地强加于人。 这种范式可以被看作是对我们人类通过发展和学习成为现实的过程进行build模的一个步骤。

(尽pipe我已经在这里写了一个答案,两年后重新回顾这个问题,我意识到还有另外一种完全不同的方式来回答这个问题,所以我把原来的答案完好无损地添加进去。)


在Javagenerics上进行的过程是否值得称为“types擦除”是非常有争议的。 由于generics没有被删除,而是被原始的types取代,所以更好的select似乎是“型式残缺”。

types擦除的典型特征就是通常理解的意思是强制运行时通过使其对所访问的数据的结构“盲”而使其停留在静态types系统的边界之内。 这给编译器提供了全部的能力,并且允许它仅仅基于静态types来certificate定理。 它还通过限制代码的自由度来帮助程序员,给予简单的推理更多的权力。

Java的types擦除并不能达到这个目的 – 这会削弱编译器,就像这个例子:

 void doStuff(List<Integer> collection) { } void doStuff(List<String> collection) // ERROR: a method cannot have // overloads which only differ in type parameters 

(上述两个声明在擦除之后折叠成相同的方法签名。)

另一方面,运行时仍然可以检查对象的types和原因,但是由于对真实types的洞察被擦除损坏,所以静态types违规很容易实现并且难以防止。

为了使事情更加复杂,原始的和删除的types签名是并存的,并且在编译期间被并行地考虑。 这是因为整个过程不是关于从运行时中删除types信息,而是关于将通用types系统locking到传统的原始types系统以维持向后兼容性。 这个gem是一个典型的例子:

 public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) 

(必须添加冗余extends Object以保持已擦除签名的向后兼容性。)

现在,考虑到这一点,让我们重新回顾报价:

当Java用户抱怨types擦除是Java的唯一一件事情时,这很有趣

Java 究竟做了什么? 这个词本身是不是意思呢? 对比一下humble inttypes:没有执行任何运行时types检查,甚至是可能的,执行总是完全types安全的。 这是什么types的擦除看起来像做得对:你甚至不知道它在那里。

一件好事是在引入generics时不需要改变JVM。 Java仅在编译器级别实现generics。

types擦除的原因是一件好事是它使不可能的东西是有害的。 防止在运行时检查types参数使得对程序的理解和推理更容易。

我发现有些反直觉的观察是,当函数签名通用时,它们变得更容易理解。 这是因为可能的实现数量减less了。 考虑一下这个签名的方法,我们知道它没有任何副作用:

 public List<Integer> XXX(final List<Integer> l); 

这个函数有什么可能的实现? 非常多。 你可以告诉一下这个函数的作用。 这可能是扭转input列表。 它可能是一起整合,总结它们,并返回一半大小的列表。 还有很多其他的可能性是可以想象的。 现在考虑:

 public <T> List<T> XXX(final List<T> l); 

这个函数有多less个实现? 由于实现无法知道元素的types,所以现在可以排除大量的实现:元素不能被组合,或者被添加到列表中或者被过滤掉,等等。 我们仅限于以下内容:标识(不改变列表),删除元素或反转列表。 这个函数根据它的签名就更容易推理了。

除了…在Java中,你总是可以欺骗types系统。 因为这个generics方法的实现可以使用像instanceof检查和/或强制转换为任意types的东西,所以我们基于types签名的推理很容易变得无用。 该函数可以检查元素的types,并根据结果做任何事情。 如果允许这些运行时攻击,则参数化的方法签名对我们来说变得不那么有用。

如果Java没有types擦除(也就是说,types参数在运行时被指定),那么这只会导致更多的推理性损害这种恶意软件。 在上面的例子中,如果列表中至less有一个元素,则实现只能违反types签名设置的期望; 但是如果T被通知了,即使列表是空的,也可以这样做。 指定types只会增加阻止我们理解代码的(已经很多)可能性。

types擦除使得语言不那么“强大”。 但某些forms的“权力”实际上是有害的。

这不是一个直接的答案(OP问“有什么好处”,我在回答“有什么好处”)

与C#types的系统相比,Javatypes的擦除是一个真正的痛苦两个raesons

你不能实现一个接口两次

在C#中,您可以安全地实现IEnumerable<T1>IEnumerable<T2> ,特别是如果两种types不共享一个共同的祖先(即他们的祖先 Object )。

实际的例子:在Spring框架中,你不能实现ApplicationListener<? extends ApplicationEvent> ApplicationListener<? extends ApplicationEvent>多次。 如果您需要基于T不同行为,则需要testinginstanceof

你不能做新的T()

正如其他人所说,做new T()只能通过reflection来完成,确保构造函数所需的参数。 C#只允许你将new T()约束为无参数的构造函数。 如果T不遵守这个约束,就会出现编译错误

如果我是C#的作者,我将引入指定一个或多个构造函数约束的能力,这些约束在编译时很容易validation(所以我可以要求例如带有string,string参数的构造函数)。 但最后一个是猜测

还有一点没有其他的答案似乎已经考虑到了:如果你真的需要运行时input的generics, 你可以像这样自己实现它

 public class GenericClass<T> { private Class<T> targetClass; public GenericClass(Class<T> targetClass) { this.targetClass = targetClass; } 

如果Java不使用擦除,那么这个类可以完成所有默认情况下可以实现的事情:它可以分配新的T (假设T有一个与它期望使用的模式相匹配的构造函数),或者T s ,它可以在运行时dynamic地testing一个特定的对象是否是一个T并根据它来改变行为,等等。

例如:

  public T newT () { try { return targetClass.newInstance(); } catch(/* I forget which exceptions can be thrown here */) { ... } } private T value; /** @throws ClassCastException if object is not a T */ public void setValueFromObject (Object object) { value = targetClass.cast(object); } } 

避免了类似于C ++的代码膨胀,因为相同的代码被用于多种types; 然而,types擦除需要虚拟调度,而c ++的代码膨胀方法可以做非虚拟调度的generics