Java的类型擦除有什么好处?

时间:2014-01-04 08:20:31

标签: java type-erasure

我今天读了tweet说:

  

当Java用户抱怨类型擦除时,这很有趣,这是Java唯一正确的事情,而忽略了所有错误的事情。

因此我的问题是:

  

Java的类型擦除是否有好处?除了JVM实现偏向于向后兼容性和运行时性能之外,它(可能)提供的技术或编程风格有哪些优势?

11 个答案:

答案 0 :(得分:88)

类型擦除良好

让我们坚持事实

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

  

当Java用户抱怨类型擦除时,这很有趣,这是Java唯一正确的做法,而忽略了所有错误的事情。

     

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

     

新T是一个破碎的程序。它与索赔同构,并且所有命题都是正确的。"我对此并不陌生。

目标:合理的计划

这些推文反映了一个视角,我们不知道我们是否可以让机器做某事,但更多的是我们是否可以推断机器会做我们真正想要的事情。良好的推理是一个证明。证明可以用正式表示法或不太正式的表示。无论规范语言如何,它们都必须清晰严谨。非正式规范并非不可能正确构造,但在实际编程中通常存在缺陷。我们最终采用自动化和探索性测试等补救措施来弥补我们在非正式推理中遇到的问题。这并不是说测试本质上是一个坏主意,但引用的Twitter用户建议有更好的方法。

因此,我们的目标是拥有正确的程序,我们可以通过与机器实际执行程序的方式相对应的方式进行清晰严谨的推理。不过,这不是唯一的目标。我们也希望我们的逻辑具有一定程度的表达能力。例如,我们只能用命题逻辑表达这么多。从一阶逻辑等方面进行通用(∀)和存在(∃)量化是很好的。

使用类型系统进行推理

类型系统可以非常好地解决这些目标。由于Curry-Howard correspondence,这一点尤为明显。这种对应关系通常用以下类比来表达:类型是程序,因为定理是证据。

这种对应有点深刻。我们可以采用逻辑表达式,并通过对应的类型转换它们。然后,如果我们有一个具有相同类型签名的程序,我们已经证明逻辑表达式是普遍正确的(重言式)。这是因为通信是双向的。类型/程序与定理/证明世界之间的转换是机械的,并且在许多情况下可以自动化。

Curry-Howard很好地介绍了我们对程序规范的看法。

类型系统在Java中是否有用?

即使对Curry-Howard有所了解,有些人发现很容易忽略类型系统的价值,当它

  1. 非常难以使用
  2. 对应(通过Curry-Howard)与具有有限表现力的逻辑
  3. 被破坏了(它将系统描述为"弱"或"强")。
  4. 关于第一点,也许IDE可以使Java的类型系统变得容易使用(这是非常主观的)。

    关于第二点,Java碰巧几乎对应于一阶逻辑。泛型使用类型系统相当于通用量化。不幸的是,通配符只给我们一小部分存在量化。但普遍量化是一个很好的开端。能够说List<A>的函数普遍用于所有可能的列表,因为A完全不受约束,这真是太好了。这导致了Twitter用户在&#34;参数化方面所谈论的内容。&#34;

    一篇经常引用的关于参数化的论文是Philip Wadler的Theorems for free!。这篇论文的有趣之处在于,仅从类型签名中我们就可以证明一些非常有趣的不变量。如果我们要为这些不变量编写自动化测试,我们将非常浪费时间。例如,对于List<A>,仅来自flatten

    的类型签名
    <A> List<A> flatten(List<List<A>> nestedLists);
    

    我们可以推理

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

    这是一个简单的例子,你可以非正式地推理它,但是当我们从类型系统正式免费获得这些证据并由编译器检查时,它会更好。

    不擦除可能导致滥用

    从语言实现的角度来看,Java的泛型(对应于通用类型)非常重视用于获取我们程序所做事情的证明的参数。这就到了提到的第三个问题。所有这些证据和正确性的提高都需要一个没有缺陷的声音类型系统。 Java绝对有一些语言功能,可以让我们打破我们的推理。这些包括但不限于:

    • 使用外部系统的副作用
    • 反射

    非擦除的泛型在许多方面与反射有关。没有擦除我们可以用来设计算法的实现带来的运行时信息。这意味着静态地说,当我们推理节目时,我们不能全面了解情况。反思严重威胁我们静态推理的任何证据的正确性。这不是巧合反映也会导致各种棘手的缺陷。

    那么,非擦除的泛型可能会有什么用处?&#34;有用吗?&#34;让我们考虑一下推文中提到的用法:

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

    如果T没有no-arg构造函数,会发生什么?在某些语言中,你得到的是null。或者你可能会跳过null值并直接引发异常(无论如何都会导致null值)。因为我们的语言是图灵完整的,所以无法推断对broken的哪些调用将涉及&#34;安全&#34;没有arg构造函数的类型以及哪些类型没有。我们已经失去了我们的计划普遍适用的确定性。

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

    因此,如果我们想要推理我们的计划,我们强烈建议不要使用强烈威胁我们推理的语言功能。一旦我们这样做,为什么不在运行时删除类型?他们不需要。我们可以获得一些效率和简单性,满意的是没有强制转换会失败,或者在调用时可能会丢失方法。

    擦除鼓励推理。

答案 1 :(得分:35)

类型是用于以允许编译器检查程序正确性的方式编写程序的构造。类型是值的命题 - 编译器验证此命题是否为真。

在执行程序期间,不需要类型信息 - 这已经由编译器验证。编译器应该可以自由地丢弃这些信息,以便对代码执行优化 - 使其运行更快,生成更小的二进制文件等。类型参数的擦除有助于此。

Java通过允许在运行时查询类型信息 - 反射,实例等来打破静态类型。这允许您构建无法静态验证的程序 - 它们绕过类型系统。它也错过了静态优化的机会。

删除类型参数这一事实可以防止构造这些错误程序的某些实例,但是,如果删除了更多类型信息并删除了反射和实例设施,则不允许使用更多不正确的程序。

擦除对于维护数据类型的“参数化”属性很重要。假设我在组件类型T上具有参数化的类型“List”,即List&lt; T&gt;。该类型是一个命题,这个List类型对于任何类型T都是相同的.T是一个抽象的,无界的类型参数,这意味着我们对这种类型一无所知,因此无法对T的特殊情况做任何特殊处理。 / p>

e.g。说我有一个List xs = asList(“3”)。我添加了一个元素:xs.add(“q”)。我最终得到[“3”,“q”]。 由于这是参数化的,我可以假设List xs = asList(7); xs.add(8)以[7,8]结束 我从类型中知道它对String没有做任何事情,对Int有一件事。

此外,我知道List.add函数无法凭空创造T值。我知道如果我的asList(“3”)添加了一个“7”,唯一可能的答案将由值“3”和“7”构成。列表中不可能添加“2”或“z”,因为该函数无法构造它。这些其他值都不适合添加,参数化可以防止构造这些不正确的程序。

基本上,擦除可以防止某些违反参数的方法,从而消除了错误程序的可能性,这是静态类型的目标。

答案 2 :(得分:11)

(虽然我已经在这里写了一个答案,两年后重新审视这个问题,我意识到还有另一种完全不同的方式来回答它,所以我完整地保留了之前的答案并添加了这个答案。 )

在Java Generics上完成的过程是否应该得到名称&#34; type erasure&#34;这是非常有争议的。由于通用类型不会被删除,而是被原始类型替换,因此更好的选择似乎是“切断”#34;

在其通常理解的意义上,类型擦除的典型特征是迫使运行时通过使其成为静态类型系统的边界而使其成为盲目的&#34;到它访问的数据的结构。这为编译器提供了全部功能,并允许它仅基于静态类型来证明定理。它还通过约束代码的自由度来帮助程序员,为简单的推理提供更多的动力。

Java的类型擦除没有实现这一点 - 它使编译器瘫痪,就像在这个例子中一样:

void doStuff(List<Integer> collection) { 
}

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

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

另一方面,运行时仍然可以检查对象的类型及其原因,但由于它对真实类型的洞察力因擦除而瘫痪,因此静态类型违规很难实现并且很难防止。

为了使事情更复杂,原始和擦除类型签名共存,并在编译期间并行考虑。这是因为整个过程不是从运行时删除类型信息,而是关于将泛型类型系统转换为传统原始类型系统以保持向后兼容性。这个宝石是一个典型的例子:

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

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

现在,考虑到这一点,让我们重温一下这句话:

  

当Java用户抱怨类型擦除时,这很有趣,这是Java唯一正确的选择

这个词本身,无论含义如何?相比之下,请看一下简陋的int类型:没有执行任何运行时类型检查,甚至可能执行,执行始终是完全类型安全的。 什么类型的擦除看起来像是正确的:你甚至不知道它在那里。

答案 3 :(得分:9)

同一用户在同一对话中的后续帖子:

  

新T是一个破碎的程序。这与“所有命题都是真实的”主张是同构的。我对此并不陌生。

(这是对另一个用户的声明的回应,即“在某些情况下似乎'新T'会更好”,因为类型擦除导致new T()是不可能的。是值得商榷的 - 即使T在运行时可用,它可能是一个抽象类或接口,或者它可能是Void,或者它可能缺少一个无参数构造函数,或者它的无参数构造函数可能是私有的(例如,因为它应该是一个单例类),或者它的无参数构造函数可以指定泛型方法没有捕获或指定的已检查异常 - 但这是前提。无论如何,没有擦除它是真的你至少可以写T.class.newInstance()来处理这些问题。))

这种类型与命题同构的观点表明,用户具有形式理论的背景。 (S)他很可能不喜欢“动态类型”或“运行时类型”,并且更喜欢没有向下转换的Java和instanceof以及反射等等。 (想象一下像Standard ML这样的语言,它有一个非常丰富的(静态)类型系统,其动态语义不依赖于任何类型的信息。)

顺便说一句,值得记住的是,用户正在拖钓:虽然他可能真诚地喜欢(静态)打字的语言,但他是而不是真诚地试图说服其他人的观点。更确切地说,原始推文的主要目的是嘲笑那些不同意的人,并且在其中一些不同意见之后,用户发布了后续推文,例如“java有类型擦除的原因是Wadler等人知道什么他们正在做,不像java的用户“。不幸的是,这使得很难找出他实际在想的是什么;但幸运的是,它也可能意味着这样做并不是很重要。对他们的观点有实际深度的人通常不会使用相当这种无内容的巨魔。

答案 4 :(得分:8)

我在这里看不到的一件事就是OOP的运行时多态从根本上取决于运行时类型的具体化。当一种语言的骨干由重新定义的类型保持到位时,它引入了类型系统的重大扩展并以类型擦除为基础,认知失调是不可避免的结果。这恰恰是Java社区发生的事情;这就是为什么类型擦除引起了如此多的争议,最终为什么有plans to undo it in a future release of Java。在Java用户的抱怨中找到有趣的东西,或者背叛了对Java精神的诚实误解,或者有意识地弃用了这个笑话。

声称“擦除是Java唯一正确的做法”意味着声称“所有基于动态调度的语言都是针对运行时类型的函数参数存在根本缺陷”。虽然它本身就是一个合法的主张,甚至可以被认为是对包括Java在内的所有OOP语言的有效批评,但它不能作为评估和批评Java语境中的的关键点。 / em>,其中运行时多态性是公理化的。

总而言之,虽然有人可能有效地说“类型擦除是进行语言设计的方式”,但在Java中支持类型擦除的位置是错误的,因为它已经太晚了甚至在橡树被Sun拥抱并更名为Java的历史时刻已经如此。



关于静态类型本身是否是编程语言设计中的正确方向,这适用于我们认为构成编程活动的更广泛的哲学背景。一个学派,显然源于古典数学传统,将程序视为一个数学概念或其他(命题,函数等)的实例,但是有一种完全不同的方法,它将编程视为一种方式与机器交谈并解释我们想要的东西。在这种观点中,该计划是一个充满活力,有机增长的实体,与静态打造的静态类型计划的戏剧性相反。

将动态语言视为朝着这个方向迈出的一步似乎很自然:程序的一致性从下到上出现,没有先验约束,这会将它强加于顶层 - 下来的方式。这种范式可以被视为迈向我们人类通过发展和学习成为我们所处过程的过程的一步。

答案 5 :(得分:5)

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

答案 6 :(得分:5)

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

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

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

此功能有哪些可能的实现?非常多。你可以很清楚地知道这个功能是做什么的。它可能会颠倒输入列表。它可能是将int整合在一起,将它们相加并返回一半大小的列表。还有许多其他可能的想法。现在考虑:

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

这个函数有多少个实现?由于实现无法知道元素的类型,现在可以排除大量的实现:元素不能组合,或者添加到列表中或过滤掉,等等。我们仅限于:身份(不更改列表),删除元素或撤消列表。此功能仅基于其签名更易于推理。

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

如果Java没有类型擦除(即,类型参数在运行时被确定),那么这只会允许更多推理 - 削弱这种类型的恶作剧。在上面的例子中,如果列表至少有一个元素,则实现只能违反类型签名设置的期望;但如果T被确定,即使列表为空,也可以这样做。改进的类型只会增加(已经非常多)阻碍我们理解代码的可能性。

类型擦除使语言不那么“强大”。但某些形式的“权力”实际上是有害的。

答案 7 :(得分:3)

这不是一个直接的答案(OP询问&#34;有什么好处&#34;,我回复&#34;有什么缺点&#34;)

与C#类型系统相比,Java类型擦除对两个raesons来说是一个真正的痛苦

您无法实施两次接口

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

实际示例:在Spring Framework中,您无法多次实现ApplicationListener<? extends ApplicationEvent>。如果您需要基于T的不同行为,则需要测试instanceof

你不能做新的T()

正如其他人评论的那样,只能通过反射来完成new T(),确保构造函数所需的参数。如果您将new T()约束为无参数构造函数,则C#允许您仅T 。如果T不遵守该约束,则会引发编译错误

如果我是C#的作者,我会介绍能够指定一个或多个构造函数约束,这些约束在编译时很容易验证(因此我可以要求例如带string,string参数的构造函数)。但最后一个是猜测

答案 8 :(得分:2)

另外一点似乎没有考虑其他答案:如果你真的需要使用运行时输入的泛型,你可以自己实现这样:

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

然后,如果Java不使用擦除,则此类可以执行默认情况下可以实现的所有操作:它可以分配新的T s(假设T具有与模式匹配的构造函数它希望使用)或T的数组,它可以在运行时动态测试特定对象是否为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);
     }
}

答案 9 :(得分:0)

避免类似c ++的代码膨胀,因为相同的代码用于多种类型;但是,类型擦除需要虚拟调度,而c ++ - 代码膨胀方法可以执行非虚拟调度泛型

答案 10 :(得分:0)

大多数答案更关注编程原理,而不是实际的技术细节。

尽管这个问题已有5年以上的历史了,但这个问题仍然存在:为什么从技术的角度来看,擦除类型是可取的?最后,答案很简单(在更高级别上):https://en.wikipedia.org/wiki/Type_erasure

C ++模板在运行时不存在。编译器为每次调用发出完全优化的版本,这意味着执行不依赖于类型信息。 但是,JIT如何处理同一功能的不同版本?仅具有一项功能会更好吗?不想让JIT对其所有不同的版本进行优化。好吧,那类型安全呢?猜猜必须离开窗口。

但是请稍等:.NET是如何做到的?反射!这样,他们仅需优化一个功能并获得运行时类型信息即可。这就是.NET泛型曾经较慢的原因(尽管它们变得更好了)。我不是在说那不方便!但是它很昂贵,并且在并非绝对必要时不应该使用它(在动态类型语言中,它不被认为是昂贵的,因为编译器/解释器仍然依赖于反射)。

这种带有类型擦除的通用编程的开销几乎为零(仍然需要一些运行时检查/强制转换):https://docs.oracle.com/javase/tutorial/java/generics/erasure.html