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

ver*_*tti 90 java type-erasure

我今天读了一条推文说:

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

因此我的问题是:

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

Suk*_*jra 90

类型擦除是好的

让我们坚持事实

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

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

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

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

目标:合理的计划

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

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

使用类型系统进行推理

类型系统可以非常好地解决这些目标.由于库里 - 霍华德的对应,这一点尤为明显.这种对应关系通常用以下类比来表达:类型是程序,因为定理是证明.

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

库里 - 霍华德很好地完成了我们想要对程序规范做的事情.

类型系统在Java中有用吗?

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

  1. 非常难以使用
  2. 对应(通过Curry-Howard)具有有限表现力的逻辑
  3. 被破坏了(它将系统的特征描述为"弱"或"强").

关于第一点,也许IDE可以使Java的类型系统足够容易使用(这是非常主观的).

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

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

<A> List<A> flatten(List<List<A>> nestedLists);
Run Code Online (Sandbox Code Playgroud)

我们可以这样说

flatten(nestedList.map(l -> l.map(any_function)))
    ? flatten(nestList).map(any_function)
Run Code Online (Sandbox Code Playgroud)

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

不擦除可能导致滥用

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

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

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

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

<T> T broken { return new T(); }
Run Code Online (Sandbox Code Playgroud)

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

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

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

擦除鼓励推理.

  • 这里提出的论点不是反对擦除,而是反对反射(以及一般的运行时类型检查) - 它基本上断言反射是坏的,所以擦除不能很好地发挥它们的事实是好的.这本身就是一个有效的观点(虽然许多人不同意); 但它在上下文中没有多大意义,因为Java已经有反射/运行时检查,并且它们没有也不会消失.因此,无论你怎么看它,都有一个不使用该语言的现有的,不推荐的部分的构造_是一个限制. (37认同)
  • 相比之下,类型擦除是一种半实体尝试,在不破坏向后兼容性的情况下将功能引入Java.这不是一种力量.如果主要关注的是您可能尝试实例化没有无参数构造函数的对象,那么正确的答案是允许使用"无参数构造函数的类型"的约束,而不是削弱语言特征. (29认同)
  • 你的回答只是偏离主题.你输入一个冗长的(虽然它自己的术语很有趣)解释为什么类型擦除作为抽象概念在类型系统的设计中通常是有用的,但主题是标记为"类型擦除"的Java泛型的特定特性的好处",它只是表面上类似于你描述的概念,完全没有提供有趣的不变量和引入不合理的约束. (10认同)
  • 在我把这个回应拼凑起来的时候,其他一些人回答了类似的答案.但是写了这么多,我决定发布它而不是丢弃它. (7认同)
  • FWIW,我很喜欢:) (4认同)
  • 您的帖子包含诸如“无需擦除,我们就可以使用实现来设计算法的运行时信息”之类的语句。这意味着擦除后,没有运行时类型信息。这要么是错误的,要么不适用于 Java,因为它确实具有“类型擦除”并且还携带运行时类型信息。你说“不擦除会导致滥用”,就好像 Java 通过运动擦除来防止滥用一样。所有这些使我将您的帖子最多描述为偏离主题(适用于真正的删除),但误导将是一个更诚实的描述。 (4认同)
  • 让我提醒您,这个主题是关于_Java 的_类型擦除。如果不是我特别关心给你自由度并允许你谈论类型擦除的_一些_定义,我只能得出结论,你的帖子充满了虚假和不一致。您有责任提供与上下文相关的陈述和引文。 (4认同)
  • 嗯......从理论上讲,您可以将T静态约束到具有无参数构造函数的类型.从内存来看,C#具有此功能.与具有"默认"方法的类型类的类型限制没有什么不同 - 例如Haskell的Data.Monoid上的mempty.尽管如此,你仍然专注于这种类型 - 你需要从中获得一些东西. (3认同)
  • 基本的,尊重,你没有做出有凝聚力的论证.你只是断言擦除是完全无视编译时证明作为推理程序的有力工具的价值而"瘫痪".此外,这些好处与Java用于实现泛型的弯曲路径和动机完全正交.此外,这些好处适用于具有更加开明的擦除实现的语言. (3认同)
  • 我相信 Phillip Wadler(他在 Java 泛型和 Pizza 方面有很多投入)并且也发表了很多关于自由定理和参数化的文章,他不同意形式主义和实现仅与“表面相似”有关。关系非常明确,有许多库利用了 JVM 上的对应关系(Functional Java、Scalaz、Cats 等)。 (3认同)
  • "对于包含单一反射​​或原始类型访问实例的程序,无法证明任何定理." 我同意.不要这样做.类型擦除奖励没有的人,并惩罚那些做的人.这就是这篇文章的重点.获得自由定理的好处是正确的,你可能必须建立代码不反射的墙.这可以/应该/在许多基于JVM的项目中完成.这样做是为了获得自由定理,而不是放弃工程过程. (3认同)
  • `&lt;T&gt; T 损坏 { return new T(); }`是的。编出一段糟糕的代码并归咎于一种语言。也可以怪它允许使用有符号整数作为数组中的索引?`int i=-1; 数组[i];`——看到了吗?语言有问题!另外仅供参考,以 c# 为例。如果您尝试使用 `return new T();` 而没有对泛型的无参数构造函数约束,编译器会给你一个错误。 (3认同)
  • 实际上,.NET可以安全地将类型参数限制为具有无参数构造函数的类型.例如在C#`其中T:new()`http://msdn.microsoft.com/en-us/library/d5x73970.aspx.当然,这仍然非常有限. (2认同)
  • WRT 到 Java 具有“正确的类型擦除”,我很想知道为什么这是正确的:`public void doStuff(List&lt;ClassOne&gt; collection) { } public void doStuff(List&lt;ClassTwo&gt; collection) // 错误:方法不能有仅在类型参数上不同的重载 (2认同)
  • 嗨,Marko,临时多态性以牺牲推理为代价,为了语法方便(对不同的类型签名使用相同的名称)而强行引入歧义。某些语言具有*类型类* 或能够对它们进行编码。这是一个卓越的解决方案,它使您可以重用名称,而不会破坏我们可以静态推理的内容。所以我并不是说 Java 的实现是完美的,而是说具体化泛型的选择是不合理的(字面意思)。 (2认同)
  • 嗨,帕维尔。我认为你的观点是公平的。我建议你所说的“限制”对我来说类似于护栏如何“限制”我从悬崖上开我的车。一些限制更好地被视为架构约束,我们从中推导出有用的系统不变量。但我同意,如果你已经从悬崖上起飞,擦除就没有降落伞的作用。 (2认同)
  • 因此,当你说“关系非常明确”时,你指的一定是Java的类型擦除以外的东西,这是这里的主题。您还必须指的是 Java 类型系统整体以外的其他东西,它允许从原始类型转换为参数化类型。 (2认同)
  • 实际上,真正的类型擦除会防止人们做坏事。这就是我一直以来的观点:Java 的类型擦除不是一个功能,而是遗留下来的强加给它的 hack。与原始类型的真正擦除进行比较。因此,虽然泛型有好处,但“擦除”的好处为零。 (2认同)

tec*_*nts 36

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

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

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

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

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

例如说我有一个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",因为该函数无法构造它.这些其他值都不适合添加,并且参数化可以防止构造这些不正确的程序.

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


Mar*_*nik 14

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


在Java Generics上完成的过程是否值得称之为"类型擦除",这是非常有争议的.由于通用类型没有被删除,而是被原始类型替换,更好的选择似乎是"类型残割".

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

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

void doStuff(List<Integer> collection) { 
}

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

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

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

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

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
Run Code Online (Sandbox Code Playgroud)

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

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

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

Java 究竟做了什么?这个词本身,无论含义如何?相比之下,请看一下简单的int类型:没有执行任何运行时类型检查,甚至可能执行,执行始终是完全类型安全的.这就是正确完成时类型擦除的样子:你甚至都不知道它就在那里.


rua*_*akh 9

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

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

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

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

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

  • 它不能用Java**编译,因为**它有类型擦除 (2认同)

Mar*_*nik 9

我在这里看不到的一件事就是OOP的运行时多态性从根本上依赖于运行时类型的具体化.当一种语言的骨干由重新定义的类型保持到位时,它引入了类型系统的重大扩展并以类型擦除为基础,认知失调是不可避免的结果.这恰恰是Java社区发生的事情; 这就是为什么类型擦除已经吸引了如此多的争议,并最终为什么有计划在未来的Java版本中撤消它.在Java用户的抱怨中找到一些有趣的东西,可能会对Java的精神产生诚实的误解,或者有意识地贬低笑话.

声称"擦除是Java唯一正确的东西"意味着声称"所有基于动态调度的语言都违反了运行时类型的函数参数,从根本上说是有缺陷的".虽然它本身就是一个合法的主张,甚至可以被认为是对所有OOP语言(包括Java)的有效批评,但它不能作为评估和批评Java上下文中的特性的关键点,其中运行时多态性是公理的.

总而言之,虽然人们可以有效地说"类型擦除是语言设计的方式",但Java中支持类型擦除的位置是错误的,因为它已经太晚了,甚至在历史时刻已经如此当Oak被Sun拥抱并重命名为Java时.



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

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

  • 存在一种非常静态的类型,专为这种推理而设计,称为“和类型”。它也被称为“变体类型”(在 Benjamin Pierce 的非常好的类型和编程语言书中)。所以这根本不是头发分裂。此外,您可以使用 catamorphism/fold/visitor 来实现完全无标签的方法。 (2认同)
  • 谢谢你的理论课,但我不认为有什么相关性。我们的主题是Java 的类型系统。 (2认同)
  • @anthavio你似乎从根本上误解了这个概念。Java 所拥有的只是所谓的类型擦除,但它实际上并不是这样。`List` 和 `Set` 仍然是具体化类型,没有什么可以改变这一点。在 Kotlin 中,您也可以获得具体化的泛型类型,尽管形式相当有限——最多可达底层 JVM 允许的范围,而不会破坏互操作性。 (2认同)

usr*_*ΛΩΝ 6

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

与 C# 类型系统相比,Java 类型擦除确实令人痛苦,原因有两个

你不能两次实现一个接口

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

实际例子:在Spring框架中,你不能ApplicationListener<? extends ApplicationEvent>多次实现。如果您根据T需要测试需要不同的行为instanceof

你不能做 new T()

(并且您需要对类的引用才能做到这一点)

正如其他人评论的那样,只能通过反射来完成相当于 的操作new T(),只能通过调用 的实例来完成Class<T>,以确保构造函数所需的参数。new T() 当您限制为无参数构造函数时,C# 才允许您执行此操作T。如果T不遵守该约束,则会出现编译错误则会引发

在 Java 中,您经常被迫编写如下所示的方法

public <T> T create(....params, Class<T> classOfT)
{

    ... whatever you do
    ... you will end up
    T = classOfT.newInstance();


    ... or more advanced reflection
    Constructor<T> parameterizedConstructorThatYouKnowAbout = classOfT.getConstructor(...,...);
}
Run Code Online (Sandbox Code Playgroud)

上述代码的缺点是:

  • Class.newInstance仅适用于无参数构造函数。如果没有可用的,ReflectiveOperationException则在运行时抛出
  • 反射构造函数不会像上面那样在编译时突出显示问题。如果你重构,你交换参数,你只会在运行时知道

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


Evg*_*eev 5

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

  • JVM相比有何变化?模板也不需要 JVM 更改。 (2认同)

Lac*_*lan 5

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

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

public List<Integer> XXX(final List<Integer> l);
Run Code Online (Sandbox Code Playgroud)

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

public <T> List<T> XXX(final List<T> l);
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • 看来你要么认为泛型对于 Java 开发人员来说太复杂而无法“理解和推理”,要么不能相信他们如果有机会不会搬起石头砸自己的脚。后者似乎确实与 Java 的精神同步(没有无符号类型等),但对开发人员来说似乎有点居高临下 (2认同)