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

我今天读了一篇 推特的文章,上面说:

有趣的是,Java 用户抱怨类型擦除,这是 Java 唯一做对的事情,却忽略了它做错的所有事情。

因此,我的问题是:

Are there benefits from Java's type erasure? What are the technical or programming style benefits it (possibly) offers other than the JVM implementations preference for backwards compatibility and runtime performance?

15596 次浏览

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

避免类似 c + + 的代码膨胀,因为相同的代码用于多个类型; 然而,类型擦除需要虚拟分派,而 c + +-code-bloat 方法可以执行非虚拟分派的泛型

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

New T 是一个坏掉的程序。它与“所有命题都为真”的说法是同构的我对这个不感兴趣。

(这是对另一个用户声明的回应,即“在某些情况下,‘ new T’似乎更好”,这个想法是由于类型擦除,new T()是不可能的。即使 T在运行时是可用的,它也可以是一个抽象类或接口,或者它可以是 Void,或者它可以缺少一个无参数构造函数,或者它的无参数构造函数可以是私有的(例如,因为它应该是一个单例类) ,或者它的无参数构造函数可以指定一个检查的异常,泛型方法没有捕获或指定 & mash; 但这是前提。不管怎么说,如果不进行擦除,你至少可以编写 T.class.newInstance()来处理这些问题,这是事实。)

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

顺便说一句,值得记住的是,用户是在钓鱼: 尽管他可能真心喜欢(静态)类型的语言,但他是 没有,真心想说服其他人相信这种观点。相反,原始推文的主要目的是嘲笑那些持不同意见的人,在一些持不同意见的人插话之后,用户发布了后续推文,比如“ Java 有类型删除功能的原因是 Wadler 等人知道他们在做什么,不像 Java 用户”。不幸的是,这使得我们很难知道他到底在想什么; 但是幸运的是,这也意味着这样做并不重要。有实际深度的人,他们的观点一般不会诉诸于巨魔,这是 没错的内容免费。

类型是一种构造,用于以允许编译器检查程序正确性的方式编写程序。类型是一个关于值的命题——编译器验证这个命题是否正确。

During the execution of a program, there should be no need for type information - this has already been verified by the compiler. The compiler should be free to discard this information in order to perform optimisations on the code - make it run faster, generate a smaller binary etc. Erasure of type parameters facilitates this.

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

类型参数被擦除这一事实可以防止构造这些不正确程序的某些实例,但是,如果擦除了更多的类型信息并删除了反射和 instanceof 设施,则将禁止构造更多不正确的程序。

擦除对于维护数据类型的“参数性”属性非常重要。假设我有一个参数化了组件类型 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 分别做一件事。

Furthermore, I know that the List.add function can not invent values of T out of thin air. I know that if my asList("3") has a "7" added to it, the only possible answers would be constructed out of the values "3" and "7". There is no possibility of a "2" or "z" being added to the list because the function would be unable to construct it. Neither of these other values would be sensible to add, and parametricity prevents these incorrect programs from being constructed.

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

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

我发现的一个有点违反直觉的现象是,当函数签名是 更多通用的时候,它们变得更容易理解。这是因为可能的实现数量减少了。考虑一个具有这种签名的方法,我们知道它没有副作用:

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

这个函数的可能实现是什么?非常多。关于这个函数的作用,您可以知道的很少。可能是反向输入列表。它可以将整数配对在一起,对它们求和,然后返回一半大小的列表。还有很多其他的可能性。现在考虑一下:

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

这个函数有多少个实现?由于实现不能知道元素的类型,因此现在可以排除大量的实现: 元素不能合并,或者添加到列表中,或者过滤掉,等等。我们仅限于: 标识(不更改列表)、删除元素或反转列表。这个函数更容易根据它的签名进行推理。

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

如果 Java 没有类型擦除(也就是说,类型参数在运行时被具体化) ,那么这只会允许 更多这种妨碍推理的诡计。在上面的示例中,如果列表至少有一个元素,那么实现只能违反类型签名设置的期望; 但是如果实现了 T,那么即使列表为空也可以这样做。具体化的类型只会增加(已经非常多的)阻碍我们理解代码的可能性。

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

类型消除很好

我们还是实话实说吧

到目前为止,许多答案都过于关注 Twitter 用户。把注意力集中在信息而不是信使身上是很有帮助的。即使是迄今为止提到的摘录,也有一个相当一致的信息:

It's funny when Java users complain about type erasure, which is the only thing Java got right, while ignoring all the things it got wrong.

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

New T 是一个坏掉的程序。它与“所有命题都为真”的说法是同构的我对这个不感兴趣。

A goal: reasonable programs

这些 tweet 反映了一种观点,这种观点对我们是否能让机器执行 something操作不感兴趣,而更感兴趣的是我们是否能推断出机器将执行我们实际想要的操作。好的推理就是证明。证明可以用形式符号或不那么形式化的东西来表示。不管规约语言如何,它们必须清晰而严谨。非正式规范并非不可能正确构造,但在实际编程中常常存在缺陷。我们最终通过自动化和探索性测试等补救措施来弥补我们在非正式推理中遇到的问题。这并不是说测试本身就是一个坏主意,但是被引用的 Twitter 用户认为有一个更好的方法。

因此,我们的目标是要有正确的程序,我们可以推理清楚,严格的方式,以符合如何机器将实际执行的程序。然而,这并不是唯一的目标。我们还希望我们的逻辑具有一定程度的表达性。例如,我们只能用命题逻辑来表达这么多。从类似一阶逻辑的东西中获得普遍性(something)和存在性(something)量化感觉很好。

Using type systems for reasoning

类型系统可以很好地解决这些目标。这是特别清楚的,因为 柯里-霍华德同构。这种对应通常用下面的类比来表示: 类型对于程序就像定理对于证明一样。

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

Curry-Howard plays nicely into what we'd like to do with specifications for a program.

类型系统在 Java 中有用吗?

即使对 Curry-Howard 有所了解,有些人还是觉得很容易忽略类型系统的值

  1. 非常难相处
  2. (通过库里-霍华德)对应于表达能力有限的逻辑
  3. 被破坏(这个角色塑造可以用“弱”或“强”来形容系统)。

Regarding the first point, perhaps IDEs make Java's type system easy enough to work with (that's highly subjective).

Regarding the second point, Java happens to 差不多 correspond to a first-order logic. Generics give use the type system equivalent of universal quantification. Unfortunately, wildcards only give us a small fraction of existential quantification. But universal quantification is pretty good start. It's nice to be able to say that functions for List<A> work universally for 一切皆有可能 lists because A is completely unconstrained. This leads to what the Twitter user is talking about with respect to "parametricity."

An often-cited paper about parametricity is Philip Wadler's 免费定理. What's interesting about this paper is that from just the type signature alone, we can prove some very interesting invariants. If we were to write automated tests for these invariants we would be very much wasting our time. For example, for List<A>, from the type signature alone for flatten

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

我们可以推理

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

这是一个简单的例子,您可以非正式地推断它,但是当我们正式地从类型系统免费获得这样的证明并由编译器检查时,情况就更好了。

不删除可能导致滥用

从语言实现的角度来看,Java 的泛型(对应于通用类型)在很大程度上影响了用于证明程序功能的参数化。这就是我提到的第三个问题。所有这些证明和正确性的增益都需要一个没有缺陷的声音类型系统来实现。Java 肯定有一些语言特性,让我们可以打破我们的推理。这些措施包括但不限于:

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

Non-erased generics are in many ways related to reflection. Without erasure there's runtime information that's carried with the implementation that we can use to design our algorithms. What this means is that statically, when we reason about programs, we don't have the full picture. Reflection severely threatens the correctness of any proofs we reason about statically. It's no coincidence reflection also leads to a variety of tricky defects.

So what are ways that non-erased generics might be "useful?" Let's consider the usage mentioned in the tweet:

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

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

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

So if we want to reason about our programs, we're strongly advised to not employ language features that strongly threaten our reasoning. Once we do that, then why not just drop the types at runtime? They're not needed. We can get some efficiency and simplicity with the satisfaction that no casts will fail or that methods might be missing upon invocation.

擦除会鼓励推理。

还有一点,其他的答案似乎都没有考虑到: 如果您真的需要具有运行时类型的泛型,你可以自己实现如下所示:

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

This class is then able to do all the things that would be achievable by default if Java did not use erasure: it can allocate new Ts (assuming T has a constructor that matches the pattern it expects to use), or arrays of Ts, it can dynamically test at run time if a particular object is a T and change behaviour depending on that, and so on.

例如:

     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);
}
}

我在这里完全没有考虑到的一点是,OOP 的 运行时多态性从根本上依赖于运行时类型的具体化。如果一种语言的主干是由保留类型支撑的,那么它的类型系统就会出现一个重大的扩展,并以类型擦除为基础,那么认知失调就是不可避免的结果。这正是 Java 社区所发生的事情; 这就是类型擦除引起如此多争议的原因,也是最终出现 计划在未来的 Java 版本中撤销它的原因。在 Java 用户的抱怨中发现某些 有意思的东西,要么是对 Java 精神的诚实误解,要么是一个有意识的冷笑话。

“擦除是 Java 做对的唯一一件事”的说法暗示着“所有基于克劳斯·福尔曼的语言与运行时类型的函数参数相比都有根本性的缺陷”。尽管它本身就是一个合法的主张,甚至可以被认为是对包括 Java 在内的所有 OOP 语言的有效批评,但它不能将自己作为评估和批评特性 在 Java 的上下文中的关键点,因为在 在 Java 的上下文中中,运行时多态性是不言而喻的。

In summary, while one may validly state "type erasure is the way to go in language design", positions supporting type erasure within Java are misplaced simply because it is much, much too late for that and had already been so even at the historical moment when Oak was embraced by Sun and renamed to Java.



As to whether static typing itself is the proper direction in the design of programming languages, this fits into a much wider philosophical context of what we think constitutes the activity of 编程. One school of thought, clearly deriving from the classical tradition of mathematics, sees programs as instances of one mathematical concept or other (propositions, functions, etc.), but there is an entirely different class of approaches, which see programming as a way to talk to the machine and explain what we want from it. In this view the program is a dynamic, organically growing entity, a dramatic opposite of the carefully erected aedifice of a statically typed program.

将动态语言看作是朝着这个方向迈出的一步似乎是很自然的: 程序的一致性是自下而上的,没有 先验的约束条件以自上而下的方式强加给它。这个范式可以被看作是一个模拟过程的一个步骤,在这个过程中,我们人类通过发展和学习成为我们自己。

(虽然我已经在这里写了一个答案,但两年后再次回顾这个问题时,我意识到还有另一种完全不同的回答方式,所以我将原封不动地保留之前的答案,并添加这一个。)


在 JavaGenerics 上完成的过程是否应该被称为“类型擦除”,这是一个很有争议的问题。由于泛型类型没有被擦除,而是用它们的原始对应类型替换,所以一个更好的选择似乎是“类型切断”。

类型擦除的典型特征是通过使运行时对其访问的数据结构“盲目”,迫使运行时停留在静态类型系统的边界内。这给了编译器全部的能力,并允许它单独证明基于静态类型的定理。它还通过限制代码的自由度来帮助程序员,给予简单推理更多的力量。

Java 的类型擦除并没有达到这种效果,它削弱了编译器,就像下面这个例子:

void doStuff(List<Integer> collection) {
}


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

(删除后,上述两个声明将折叠成相同的方法签名。)

另一方面,运行时仍然可以检查对象的类型并对其进行推理,但是由于它对真实类型的洞察力受到擦除的限制,静态类型冲突很容易实现,也很难防止。

To make things even more convoluted, the original and erased type signatures co-exist and are considered in parallel during compilation. This is because the whole process is not about removing type information from the runtime, but about shoehorning a generic type system into a legacy raw type system to maintain backwards compatibility. This gem is a classic example:

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

(必须添加冗余的 ABc0,以保留被擦除签名的向下兼容。)

现在,考虑到这一点,让我们重新审视这句话:

当 Java 用户抱怨类型擦除时,这很有趣,因为这是 Java 唯一做对的事情

Java 做对了什么 没错?是因为这个词本身吗,不管它的意思是什么?对比一下简单的 int类型: 不执行任何运行时类型检查,甚至不可能执行,并且执行总是完全类型安全的。那是什么类型的擦除看起来像当做正确的: 你甚至不知道它的存在。

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

与 C # 类型系统相比,Java 类型擦除是一个真正的痛苦,原因有二

接口不能实现两次

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

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

你不能做新的 T ()

(你需要一个对 Class 的引用)

正如其他人所评论的那样,只能通过反射完成与 new T()等效的操作,只能通过调用 Class<T>的实例,确保构造函数所需的参数。如果将 T约束为无参数构造函数,C # 允许执行 new T() 只有。如果 T不遵守该约束,则引发 编译错误

In Java, you will often be forced to write methods that look like the following

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(...,...);
}

上述代码的缺点是:

  • NewInstance 只能用于无参数构造函数,如果没有可用的构造函数,则在运行时抛出 ReflectiveOperationException
  • Reflected constructor does not highlight problems at compile time like the above. If you refactor, of you swap arguments, you will know only at runtime

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

Most answers are more concerned about programming philosophy than the actual technical details.

虽然这个问题已经有5年多的历史了,但是这个问题仍然挥之不去: 为什么从技术的角度来看类型擦除是可取的?最后,答案相当简单(在更高的层次上) : https://en.wikipedia.org/wiki/Type_erasure

C++ templates don't exist at runtime. The compiler emits a fully optimized version for each invocation, meaning the execution doesn't depend on type information. But how does a JIT deal with different versions of the same function? Wouldn't it be better to just have one function? Wouldn't want the JIT to have to optimize all the different versions of it. Well, but then what about type safety? Guess that has to go out of the window.

但是等一下。NET 做到了吗?倒影!这样他们只需要优化一个功能就可以得到执行期型态讯息。这就是为什么。NET 泛型过去比较慢(尽管它们已经变得更好了)。我不是说这不方便!但是它很昂贵,不应该在不是绝对必要的情况下使用(在动态类型语言中它并不被认为是昂贵的,因为编译器/解释器无论如何都依赖于反射)。

这样类型擦除的泛型接近于零(仍然需要一些运行时检查/强制转换) :