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


98

我今天读了一条推文说:

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

因此,我的问题是:

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


6
如果您不希望以“基于观点”来关闭问题,则应以其他方式提出。(就像“类型擦除的技术优势是什么”)。更好的是,问高音喇叭他在说什么。
Stephen C

3
删除“为什么这是一件好事”。“好东西”是一种明显的主观表征,它(显然)引出了一个问题,即类型擦除一件好事……这是有争议的。
Stephen C

3
如果这个网站将变成关于Twitter上发布的所有内容的疑问,上帝会帮助我们。至少引用一个有信誉的问题来源。
洛恩侯爵,

21
为什么问题需要来自“信誉良好的来源”?那真是愚蠢。
vertti 2014年

4
嗯,这里的大多数问题都不来自任何来源,除了发问者自己对特定软件/语言/无关的问题之外,幸运的是,有很多专业人员“浪费”了他们的时间来帮助这些人。
vertti 2014年

Answers:


90

类型擦除好

让我们坚持事实

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

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

我得到了巨大的收益(例如参数性)和零成本(指定成本是想象力的极限)。

new T是一个损坏的程序。与“所有命题都是正确的”的主张是同构的。我对此并不大。

目标:合理的程序

这些推文反映了一个观点,该观点对我们是否可以使机器执行某项操作并不感兴趣,而更多地是我们是否可以推断该机器将执行我们实际想要的操作。充分的推理就是一个证明。可以用正式符号或不那么正式的形式指定证明。不论规范语言如何,它们都必须清晰且严格。非正式规范并非不可能正确构建,但在实际编程中经常存在缺陷。我们最终以自动化和探索性测试之类的补救措施来弥补非正式推理中存在的问题。这并不是说测试本质上是一个坏主意,但引用的Twitter用户暗示这是更好的方法。

因此,我们的目标是要有正确的程序,我们可以根据机器实际执行程序的方式来明确,严格地进行推理。但是,这并不是唯一的目标。我们还希望我们的逻辑具有一定的表现力。例如,我们只能用命题逻辑来表达。能够从一阶逻辑中获得通用(∀)和存在(∃)量化是一件好事。

使用类型系统进行推理

这些目标可以通过类型系统很好地解决。由于Curry-Howard的对应关系,这一点尤其明显。这种对应关系通常用以下类比表示:类型是程序,定理是证明。

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

Curry-Howard很好地发挥了我们对程序规范的期望。

类型系统在Java中有用吗?

即使对Curry-Howard有所了解,一些人还是很容易在类型系统的价值被忽略时

  1. 很难合作
  2. (通过Curry-Howard)对应于表达受限的逻辑
  3. 损坏(将系统表征为“弱”或“强”)。

关于第一点,IDE可能使Java的类型系统易于使用(这是非常主观的)。

关于第二点,爪哇恰好几乎对应于第一阶逻辑。泛型使用与通用量化等效的类型系统。不幸的是,通配符只给我们一小部分的存在量化。但是通用量化是一个很好的开始。可以说,List<A>对于所有可能的列表来说,通用工作的函数是很高兴的,因为A完全不受约束。这导致Twitter用户在谈论“参数”。

经常被引用的关于参量的论文是菲利普·沃德勒的定理是免费的!。本文的有趣之处在于,仅凭类型签名,我们就可以证明一些非常有趣的不变量。如果我们要为这些不变量编写自动化测试,那将浪费我们的时间。例如,对于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肯定具有一些语言功能,这些功能可以使我们打破推理。这些包括但不限于:

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

未擦除的泛型在许多方面与反射有关。无需删除,实现中就会附带运行时信息,可用于设计算法。这意味着静态地,当我们对程序进行推理时,我们没有完整的画面。反思严重威胁我们静态推理的任何证据的正确性。并非巧合,反射也会导致各种棘手的缺陷。

那么,未删除的泛型可能有哪些“有用”的方式?让我们考虑一下推文中提到的用法:

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

如果T没有no-arg构造函数会怎样?在某些语言中,您得到的是null。或者,您可以跳过null值并直接引发异常(无论如何,null似乎都会导致该异常)。因为我们的语言是图灵完备的,所以不可能推断出哪些调用broken将涉及带有无参数构造函数的“安全”类型,而哪些不会。我们已经失去了程序可以普遍运行的确定性。

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

因此,如果我们要对程序进行推理,强烈建议不要使用会严重威胁我们推理的语言功能。一旦我们做到了,那为什么不只在运行时删除类型呢?不需要它们。我们可以满意地得到所有效率和简单性,这些要求不会导致强制转换失败或调用时可能缺少方法。

擦除鼓励推理。


7
在我整理这些答案的时间里,其他一些人回答了类似的答案。但是写了这么多之后,我决定发布它而不是丢弃它。
Sukant Hajra 2014年

32
相对而言,类型擦除是在不破坏向后兼容性的前提下将功能引入Java的半途而废的尝试。这不是优势。如果主要的问题是您可能尝试实例化没有无参数构造函数的对象,那么正确的答案是允许“无参数构造函数的类型”的约束,而不是削弱语言功能。
基本

5
基本而言,您并没有提出内聚的论据。您只是在断言,擦除是在“残废”,而完全不考虑编译时证明作为程序推理的有力工具的价值。此外,这些好处与Java实现泛型所采用的弯曲路径和动机完全正交。同样,这些好处对于具有更开明的擦除实现的语言也适用。
Sukant Hajra 2015年

44
此处提出的论据不是反对擦除,而是反对反射(通常是对运行时类型检查)-它基本上断言反射是不好的,因此,擦除不能很好地与他们一起玩的事实是好的。它本身就是一个有效的观点(尽管很多人会不同意)。但这在上下文中没有多大意义,因为Java已经进行了反射/运行时检查,并且没有,也不会消失。因此,无论您如何看待,都有一个与现有的,未弃用的语言都不兼容的构造一个限制。
帕维尔·米纳夫'16

10
您的答案简直就是题外话。您输入了一个冗长的(尽管很有趣)解释,为什么类型擦除作为抽象概念通常在类型系统的设计中很有用,但是该主题的好处是Java泛型的一个特殊特性,即“类型擦除” ”,它只是从表面上类似于您描述的概念,完全无法提供有趣的不变式和引入不合理的约束。
Marko Topolnik '16

40

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

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

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

删除类型参数的事实阻止了构造这些不正确程序的某些实例,但是,如果删除了更多类型信息并且删除了reflecting和instanceof工具,那么将不允许更多不正确的程序。

擦除对于维护数据类型的“参数”属性很重要。说我在组件类型T上有一个参数化的“列表”类型,即List <T>。该类型是一个命题,即List类型对于任何类型T都相同。事实T是抽象的,无边界的类型参数,这意味着我们对该类型一无所知,因此,对于T的特殊情况,不能做任何特殊的事情。

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

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


15

(尽管我已经在这里写了一个答案,两年后重新审视了这个问题,但我意识到还有另一种完全不同的答案方式,因此我保留了先前的答案并添加了这个答案。)


在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

(擦除后,以上两个声明会折叠为相同的方法签名。)

另一方面,运行时仍可以检查对象的类型及其原因,但是由于擦除会破坏对真实类型的了解,因此静态类型违例很难实现且很难防止。

为了使问题更加复杂,原始和已删除的类型签名共存,并在编译期间并行考虑。这是因为整个过程不是要从运行时删除类型信息,而是要把通用类型系统插入旧的原始类型系统以保持向后兼容性。这个宝石是一个典型的例子:

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

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

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

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

什么确切并得到Java的吧?它是单词本身,无论含义如何?相比之下,请看一下谦虚的int类型:从未执行甚至根本没有执行过运行时类型检查,并且执行始终是完全类型安全的。正确完成后,就是类型擦除的样子:您甚至都不知道它的存在。


10

我在这里根本没有考虑的一件事是,OOP的运行时多态性从根本上取决于运行时类型的确定。当一种语言,其主干由保留的类型固定在位时,对其类型系统进行了重大扩展,并以消除类型为基础,则认知失调是必然的结果。这正是Java社区发生的事情。这就是为什么类型擦除引起如此多争议的原因,并且最终是为什么计划在Java的未来版本中撤消它。在对Java用户的抱怨中找到有趣的东西,要么是对Java精神的诚实误解,要么是有意识地贬低了玩笑。

宣称“清除是Java唯一正确的做法”的主张暗示着“基于针对函数参数的运行时类型的动态分派的所有语言从根本上都是有缺陷的”。尽管可以肯定地说它本身是一项合法的主张,甚至可以被认为是对包括Java在内的所有OOP语言的有效批评,但是它不能成为在Java上下文中评估和批评功能的枢轴点,在Java上下文中,运行时多态性是公理的。

总而言之,尽管人们可以有效地指出“类型擦除是语言设计的一种方式”,但在Java中支持类型擦除的职位却错位了,因为这样做太晚了,甚至在历史的时刻也是如此。当Oak被Sun拥抱并重命名为Java时。



关于静态类型本身是否是编程语言设计的正确方向,这适合于我们认为构成编程活动的更广泛的哲学上下文。一门学派显然是从数学的经典传统中衍生出来的,它把程序看作一个数学概念或其他数学概念(命题,函数等)的实例,但是有完全不同的一类方法,它们将编程视为一种实现方法。与机器对话并解释我们想要的机器。在这种情况下,该程序是一个动态的,有机增长的实体,与静态类型的程序精心构建的大厦截然相反。

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


感谢那。我希望对这个问题有一种更深刻的理解,其中涉及到的哲学。
Daniel Langdon 2014年

动态调度完全不依赖于类型化的类型。它通常依赖于编程文献所说的“标签”,但是即使如此,它也不需要使用标签。如果创建一个接口(用于一个对象),并创建该接口的匿名实例,那么您就在进行动态分派,根本不需要任何类型化的类型。
Sukant Hajra

@sukanthajra那只是劈头发。标记是解决实例表现出的行为类型所需的运行时信息。它超出了静态类型系统的范围,这就是所讨论概念的本质。
Marko Topolnik '16

正是出于这种推理而设计的一种非常静态的类型称为“求和类型”。它也被称为“变量类型”(在本杰明·皮尔斯的非常好的类型和编程语言书中)。因此,这根本不是分裂头发。另外,您可以使用完全变形/折叠/访问者来实现完全无标签的方法。
Sukant Hajra

谢谢您的理论课,但我看不到相关性。我们的主题是Java的类型系统。
Marko Topolnik '16

9

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

new T是一个损坏的程序。与“所有命题都是正确的”的主张是同构的。我对此并不大。

(这是对另一位用户的声明的回应,即“在某些情况下'new T'似乎会更好”,这种想法new T()是由于类型擦除是不可能的。(这值得商—-即使T在运行时,它可能是抽象类或接口,或者可能是Void,或者可能缺少no-arg构造函数,或者它的no-arg构造函数可能是私有的(例如,因为它应该是单例类),或者no-arg构造函数可以指定一个通用方法无法捕获或指定的检查异常,但这是前提。无论如何,确实可以不删除就至少可以编写T.class.newInstance()处理这些问题的方法。))

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

顺便提一下,值得牢记的是用户正在拖钓:尽管他可能真诚地喜欢(静态)键入的语言,但他并没有真诚地试图说服其他人。相反,原始推文的主要目的是嘲笑那些不同意的人,在听到一些不同意见后,用户发布了后续推文,例如“ Java删除类型的原因是Wadler等人知道什么。他们正在做,与Java用户不同。” 不幸的是,这使得很难找出他的实际想法。但幸运的是,这也可能意味着这样做不是很重要。真正有见地的人通常不会诉诸那些完全没有内容的巨魔。


2
它无法在Java中进行编译,因为它具有类型擦除功能
Mark Rotteveel 2014年

1
@MarkRotteveel:谢谢;我添加了一个段落来解释(并对此发表评论)。
ruakh 2014年

1
从赞成票和反对票的混合来看,这是我有史以来最具争议的答案。我感到很自豪。
ruakh 2014年


5

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

我发现有些反直觉的发现是,当函数签名更为通用时,它们将变得更易于理解。这是因为减少了可能的实现方式。考虑具有此签名的方法,我们以某种方式知道它没有副作用:

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

此功能可能实现什么?非常多。您几乎无法知道此功能的作用。可能是反转输入列表。可能是将整数配对在一起,将它们求和并返回一半大小的列表。还有许多其他可以想象的可能性。现在考虑:

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

有多少个此功能的实现?由于实现无法知道元素的类型,因此现在可以排除大量实现:无法将元素组合,添加到列表或过滤掉,等等。我们仅限于以下方面:身份(不更改列表),删除元素或反转列表。仅凭其签名就更容易推断此功能。

除了……在Java中,您总是可以欺骗类型系统。因为该通用方法的实现可以使用诸如instanceof检查和/或强制转换为任意类型之类的东西,所以基于类型签名的推理很容易变得无用。该函数可以检查元素的类型,并根据结果执行任何数量的操作。如果允许这些运行时破解,则参数化方法签名对我们的用处将大大减少。

如果Java没有类型擦除(即,在运行时对类型参数进行了更改),则这将仅允许更多此类破坏推理的恶作剧。在上面的示例中,如果列表具有至少一个元素,则实现只能违反类型签名所设置的期望;但是T,如果列表被清空,即使列表为空,它也可以这样做。修饰类型只会增加(已经非常多)阻碍我们对代码理解的可能性。

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


1
似乎您是在争论泛型对于Java开发人员来说太复杂了,以至于无法“理解和推理”,或者,如果有机会,他们就不能被信任不要not脚。后者的确与Java的风格保持同步(也没有无符号类型,等等),但对开发人员而言却有些屈尊
基本

1
@Basic我肯定表现得很差,因为那根本不是我的意思。问题不在于泛型是复杂的,而是诸如强制转换之类的事情,并且instanceof阻碍了我们根据类型推断代码做什么的能力。如果Java要验证类型参数,那只会使这个问题变得更糟。在运行时擦除类型具有使类型系统更有用的作用。
Lachlan'1

@lachlan对我来说非常有意义,而且我认为正常阅读它不会对Java开发人员造成任何侮辱。
罗伯·格兰特

3

这不是直接答案(OP询问“有什么好处”,我在答复“有什么缺点”)

与C#类型的系统相比,Java类型的擦除对于两个版本来说确实是一个痛苦

您不能两次实现接口

在C#中,你可以同时实现IEnumerable<T1>IEnumerable<T2>安全,尤其是如果这两种类型不共享一个共同的祖先(即他们的祖先 Object)。

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

你不能做新的T()

(并且您需要引用Class来做到这一点)

正如其他人所评论的new T()那样,只能通过反射,仅通过调用的实例来完成等效的操作Class<T>,并确保构造函数所需的参数。C#new T() 允许您约束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(...,...);
}

上面的代码的缺点是:

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

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


2

另外,似乎没有其他答案可以考虑:如果您确实需要使用运行时类型的泛型,则可以这样实现自己

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

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

有人在乎解释拒绝投票的原因吗?据我所知,这是对Java类型擦除系统所谓的缺点实际上根本不是真正的限制的一个很好的解释。所以有什么问题?
Jules 2014年

我正在投票。我并不是说我支持运行时类型检查,但是这种技巧在盐矿中很方便。
techtangents

3
Java 盐矿的一种语言,鉴于缺少运行时类型信息,这使得它成为绝对必要。希望在将来的Java版本中取消类型擦除,使泛型成为更有用的功能。目前的共识是,它们的推力/重量比低于1
马尔科Topolnik

好吧,确定...只要您的TargetType本身不使用泛型即可。但这似乎很局限。
基本

它也适用于泛型类型。唯一的限制是,如果要创建实例,则需要具有正确参数类型的构造函数,但是我知道的所有其他语言都具有相同的限制,因此我看不到限制。
Jules 2014年

0

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


3
但是,这些虚拟调度中的大多数都会在运行时转换为静态调度,因此它看起来还不错。
PiotrKołaczkowski14年

1
的确,相对于c ++,运行时编译器的可用性使java做很多很酷的事情(并实现高性能)。
necromancer 2014年

哇,让我的地方写下来的Java实现相对较高的性能,以CPP ..
jungle_mole

1
@jungle_mole是的,谢谢。很少有人意识到在运行时使用编译器来进行性能分析后再次编译代码的好处。(或者说垃圾收集是大垃圾(非垃圾),而不是天真的和不正确的信念,即垃圾收集是大垃圾(垃圾),或者分配))。这是一个悲伤的世界,人们盲目地采纳他们的社区所相信的东西,并停止从首要原则中推理。
necromancer

0

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

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

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.