问题

Java 不能实现真正泛型的原因是什么?

回答
Java 在引入泛型时,虽然极大地提升了代码的类型安全和可读性,但严格来说,它并没有实现我们通常理解的“真正意义上的”泛型(相对于一些其他语言,比如 C++ 的模板)。这其中的核心原因可以追溯到 Java 的设计理念和对向后兼容性的考量,具体可以从以下几个方面来详细阐述:

1. 类型擦除 (Type Erasure)

这是 Java 泛型最根本的限制所在。Java 在编译时,会将泛型类型信息擦除掉,替换为它们的原始类型(通常是 `Object` 或指定的上界类型)。这意味着:

运行时无法获取泛型类型参数: 在 Java 虚拟机 (JVM) 中,当程序运行时,关于 `List` 或 `List` 的 `String` 和 `Integer` 信息就已经不存在了。JVM 看到的只是一个普通的 `List` 对象。因此,你无法在运行时直接判断一个集合是 `List` 还是 `List`。
示例: 你不能直接写 `if (myList instanceof List)` 这样的代码,编译器会报错。你需要写成 `if (myList instanceof List)`,然后通过其他方式(比如检查集合中的元素类型)来推断。
数组与泛型类型的不兼容性: 由于类型擦除,Java 不允许创建泛型数组,例如 `new T[10]`。这是因为如果允许,在运行时类型信息丢失后,JVM 就无法知道这个数组实际应该存储哪种类型的元素,从而导致类型不安全的操作。
替代方案: Java 提供了 `List` 集合作为替代,或者在某些情况下需要进行类型转换并带有警告(`@SuppressWarnings("unchecked")`)。
泛型方法的参数类型限制: 泛型方法虽然可以声明类型参数,但这些参数类型在运行时同样会被擦除。例如,`public void printArray(T[] array)` 这样的方法,在运行时 `T` 就会被擦除为 `Object`。

为什么会选择类型擦除?

类型擦除的主要动机是为了向后兼容。Java 的设计非常注重向后兼容性,即新的 Java 版本编写的代码能够运行在旧的 JVM 上。如果 Java 像 C++ 那样为每一种泛型类型都生成独立的机器码(类似于模板的实体化),那么旧的 JVM 就会无法理解这些新的类型信息,导致大量现有 Java 代码失效。

类型擦除允许泛型代码在编译时提供类型检查,然后在运行时以原始类型的方式运行,从而保证了与旧版本的兼容性。

2. 无法创建特定泛型类型的实例

由于类型擦除,你在泛型类内部无法直接创建该类型参数的实例。

示例: 假设有一个泛型类 `MyGeneric`,你想在构造函数中创建 `T` 的实例:
```java
class MyGeneric {
private T instance;
public MyGeneric() {
// this.instance = new T(); // 错误!Cannot instantiate the type T
}
}
```
编译器不知道 `T` 是什么类型,也就无法知道如何创建它的实例。

解决办法: 通常需要通过反射或者传递一个 `Class` 对象作为参数来解决这个问题:
```java
class MyGeneric {
private T instance;
public MyGeneric(Class clazz) {
try {
this.instance = clazz.getDeclaredConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
```
但这增加了代码的复杂性,并且仍然依赖于运行时信息,不如直接创建类型实例来得直观和高效。

3. 泛型类型不能作为静态成员的类型

你不能在泛型类的静态字段或静态方法中使用类型参数 `T`。

示例:
```java
class MyGeneric {
// private static T staticField; // 错误!Static member 'staticField' of type 'T' not allowed
// public static void staticMethod(T arg) {} // 错误!Static member 'staticMethod(T)' not allowed
}
```

原因: 静态成员是属于类的,不属于任何一个类的实例。由于类型擦除,JVM 在运行时无法知道 `T` 具体是什么类型。如果允许静态成员使用 `T`,那么 `staticField` 最终应该是什么类型?是 `Object`?还是某个具体的类型?由于类型信息丢失,静态成员无法确定其类型,也无法确定其在内存中的大小。因此,Java 禁止了这种用法。

4. 原始类型 (Raw Types) 的存在

Java 为了兼容旧的、没有使用泛型版本的代码,保留了“原始类型”的概念。这意味着你可以将一个泛型类的对象声明为原始类型,并且可以在其中混入不同类型的元素。

示例:
```java
List stringList = new ArrayList();
List rawList = stringList; // 将泛型 List 赋值给原始类型的 List
rawList.add(123); // 这将在运行时抛出 ClassCastException
```
这种用法绕过了泛型的类型检查,引入了运行时错误的可能性,是类型擦除带来的一个副作用。虽然编译器会给出警告,但仍然是合法的 Java 代码。

5. 类型参数不能是基本类型

你不能使用基本类型(如 `int`、`char`、`boolean`)作为泛型类型参数,只能使用它们的包装类(如 `Integer`、`Character`、`Boolean`)。

示例: `List` 是不允许的,必须写成 `List`。

原因: 再次与类型擦除有关。如果允许 `List`,类型擦除后它会变成 `List`。但 `int` 本身是原始类型,不能直接存储在 `List` 中(`List` 只能存储对象)。即使 JVM 能够将其自动装箱为 `Integer`,这也会引入额外的复杂性和不一致性。原始类型参数也无法通过反射获取其原始类型信息,因为它们本身就没有类型对象。

6. 泛型边界和子类型关系的问题

在某些复杂的泛型场景下,子类型关系的行为可能会与直觉不符,这同样是类型擦除的后果。

示例: `List` 不是 `List` 的子类型,尽管 `Integer` 是 `Number` 的子类型。

```java
// List numbers = new ArrayList(); // 错误!Type mismatch
```
如果允许这样做,就会出现之前提到的类型擦除导致的运行时异常。如果一个方法接受 `List`,它只能从中读取 `Number` 对象,而不能向其中添加任何东西,因为编译器不知道 `List` 实际存储的是哪种具体的 `Number` 子类。

总结一下 Java 泛型与“真正泛型”的区别:

C++ 模板: 在 C++ 中,模板会在编译时为每一种使用到的类型参数进行“实体化”,生成一份独立的类型代码。这意味着模板在编译时就已经知道了完整的类型信息,并且运行时也不会丢失。因此,C++ 模板在类型安全、性能(避免装箱拆箱)、运行时类型信息等方面,比 Java 的类型擦除式泛型更“强大”或更“原生”。

Java 泛型: Java 泛型是一种“编译时特性”,它通过类型擦除来提供类型安全检查,同时保持了与旧版本的兼容性。它在编译时捕获了许多潜在的类型错误,但运行时却丢失了部分泛型类型信息。这是一种折衷的设计,虽然牺牲了一些“纯粹性”,但换来了更广泛的兼容性和平滑的迁移路径。

因此,当你听到说 Java 泛型“不是真正的泛型”时,通常是指它在类型擦除机制下所表现出的种种限制,尤其是在运行时类型信息丢失、与数组不兼容、无法创建泛型实例等方面,这些都与像 C++ 模板那样在编译时就完全实体化的泛型机制有所不同。

网友意见

user avatar

首先并没有什么“真正的泛型”。C++ 的模板是泛型,Java 基于擦除实现的泛型也是泛型。

事实上,对于泛型的翻译有两种策略:同构翻译(homogeneous translation)和异构翻译(heterogeneous translation)。C++ 和(目前的)Java 的策略分别处于异构翻译和同构翻译的极端,一个为每种类型组合都创建一份特化,一个所有类型共享一种实现。而 C#/CLR 处于两者中间,为共享布局的引用类型同构翻译,为值类型异构翻译。

不管是同构翻译还是异构翻译,都有自己的优缺点。Java 的擦除实现带有不小的问题,常常被人诟病,但同样具有巨大的优势(当然如果擦除实现没有这么大优势的话,Java 也不会采用这种实现了)。

类型擦除的问题

对 Java 的诟病,以及对具化泛型的期望,往往在这几点上:

  • 获取泛型参数的具体值。程序员可能会想要知道一个 List 到底是 List<String> 还是 List<Integer>,想要拿到实际泛型参数相关的信息,而因为类型擦除,实际上并不能做到这一点。
  • 布局特化。当前基于擦除的实现要求泛型参数类型必须拥有公共运行时表示(common runtime representation),在 Java 里这意味着只能是一组引用类型,而不能为原始类型。这导致我们只能用 List<Integer>,而用不了针对 int 进行布局特化的 List<int>,底层存放的全是对 Integer 对象的引用,这造成了巨大的内存浪费,同时对现代 CPU 的缓存策略极端不友好,大量间接寻址产生大量 cache miss,产生大量的性能下降。这也是当前 Java 泛型擦除最大的问题。
  • 运行时类型检查。因为泛型实际参数会被擦除,List<String> 会被擦除为 List,所以当通过一些手段(强制转换,raw type 等)将其他类型的值放入这个 List 的时候并不会出错,直到实际访问时才会发生问题。

实际上这不是单一的需求和目标,各自的目的不同,付出的成本与对应的收益也不同,不能从一而论。

模板(异构翻译)的问题

看起来类型擦除有不少的问题,但另一边的模板也存在一些问题。

模板可以把具体操作推迟到被实例化的时候,根据传入的类型参数具体内容而确定表达式的具体含义,并且每个特化有单独的布局,编译器能为每个特化单独优化,这看起来不错。

但是,异构翻译的问题也是显然的。模板的每个实例都要有着不同的代码,这意味着模板展开会导致代码膨胀,产生更大的硬盘和内存占用。像 Scala 提供了 @specialized 注解,可以为原始类型生成特化的版本,结果就是因为指数爆炸,可以很轻松的用几行代码生成上百兆的 JAR。当然这是很极端的例子,一般不可能产生如此之多的模板实例,但 C++ 模板产生的代码膨胀也是必须要关心的问题。

同时,因为 vector<int>vector<float> 是无关的两个类型,C++ 缺乏 Java 的类型通配符或者 C# 的声明处类型变异等能力。事实上,参数化类型组更适合通过同构翻译来表现。

那么,类型擦除的优势有哪些?

前面说了擦除的问题所在,这些一直被诟病的问题让擦除看起来并不是一个好的选择。但事实上,选择类型擦除的方式实现泛型对于 2004 年的 Java 来说是一个明智且务实的选择,即使放到现在来看,它的优点依然是不可忽视的。

1. 兼容性

使得当时的 Java 选择擦除最主要的原因是兼容性

Java 在 2004 年已经积累了大量的生态,如果把现有的类修改成泛型类,需要让所有用户全部重新修改或编译,那完全是不可想象的,会直接导致 Java 1.4 前后生态完全分裂。而 C# 的选择是原类放在那不修改,新创建一套平行的泛型化的库,让用户慢慢抛弃老库。这种选择对于当时用户不多的 C# 来说是可以接受的,但对于已经有大量用户的 Java 来说,这一样是会让生态产生巨大的分裂,并不是一个好的选择。

而 Java 现在的擦除实现做到了一个非常夸张的目标:它完全维护了二进制兼容性和源代码兼容性。

虽然一些类修改为了泛型类,但所有用到它的地方都不受任何影响。用 Java 1.4 或更早版本编译出的字节码一样能用,同时也能直接不做修改的在 Java 1.5 中进行编译。所有用户都可以自由的选择如何迁移到泛型类,用户的用户也不会受到影响。能够维护这种完全的兼容性,Java 的泛型设计在当时来说可谓完全成功。

2. 类型系统的自由度

其次,擦除维护了 JVM 生态类型系统的自由度。

虽然 Java 和 JVM 常常被绑定在一起,但它们是各自独立的,有着各自的规范。根据一些统计,有超过 200 种语言会编译到字节码。其中一些语言和 Java 的类型系统并不完全兼容,渴求更高的表达能力。

类型擦除是一个很好的实现复杂类型的方式,Haskell 就是一个使用类型擦除的典型例子,它大多数类型检查都放在编译时,而在编译后擦除类型。

由于通过类型擦除实现泛型,像 Scala 这样的语言可以以与 Java 泛型高度协同的方案实现远远超出 Java 类型系统表达能力的类型系统,同时保持高度的互操作性。

一个典型的反例就是 C#/CLR。CLR 的泛型系统严重制约了其上语言泛型的表达能力,C#、F# 等语言都因此难以拥有更强的类型系统,任何与 CLR 冲突的小区别都是致命的(譬如,因为 CLR 缺乏 bottom type,Scala 的 ListOption 等实现在其上就几乎无法直接表达,需要重写改写以适应 CLR),必须和 CLR 共同演化才能实现很多能力。在 CLR 上擦除泛型以实现更强大的类型系统是可能的,但代价就是完全和 CLR 原生的泛型生态撕裂,而原生就基于擦除实现的泛型就不会有这个问题。

3. 运行时开销

前面已经说过异构翻译导致的代码膨胀问题,这会产生难以避免的运行时开销。当然,这个开销往往是值得的,但异构翻译的开销不止于此,另一个重要的开销问题就是运行时类型检查。

运行时类型检查可以避免堆污染,维护安全性,并对于 List<String> 这样的简单类型来说,看起来开销往往是微不足道的。但是,在遇到复杂的类型(比如说 Map<? extends List<? super Foo>>, ? super Set<? extends Bar>>)时 ,类型检查的开销可能会出乎意料的大,这也是不得不考虑的问题。

未来 —— 通往 Valhalla 的道路

异构翻译和同构翻译都存在自己的弊病,而 OpenJDK 的 Project Valhalla 吸取了探索者们的经验与教训,在 Java 语言和 JVM 中融合两种方案尝试中接近了终点。

原始类型方面,Valhalla 已经准备好了很漂亮的答卷(JEP 401JEP 402)。而在泛型方面,Project Valhalla 有着非常具有野心的目标:它专注于实现布局特化,弥补 Java 完全擦除泛型最主要的性能问题,绕开运行时类型检查等高开销操作,同时还要保留逐步迁移的兼容性,还要保持擦除带来的类型系统高自由度的优势。

我翻译了一篇 Valhalla 项目的设计说明,从中可以更深入的了解 Valhalla 的目标、历史,以及 L-World 下全新的类型系统。

Project Valhalla 在权衡下只关注于布局特化,对其他具化泛型的用途并没有太多帮助。不过具化泛型的其他功能仅在语言层面就可以很好的实现。譬如 Scala 用 ClassTagTypeTag 就能简单地做到保留类型的功能:

       def newArray[T : ClassTag](n: Int) = new Array[T](n) newArray[String](10) // ok  class Foo[T : ClassTag] {   def newArray(n: Int): Array[T] = new Array(n) }  new Foo[String]().newArray(10) // ok  def typeOf[T : TypeTag] = implicitly[TypeTag[T]].tpe println(typeOf[Map[_, Seq[_ <: String]]]) // Map[_, Seq[_ <: String]]     

相对来说,这些功能的代价较为高昂,受益却略偏低,Valhalla 不关心于此也是能接受的。

参考:

valhalla-docs-zh: in-defense-of-erasure.md

valhalla-docs-zh: 01-background.md

类似的话题

  • 回答
    Java 在引入泛型时,虽然极大地提升了代码的类型安全和可读性,但严格来说,它并没有实现我们通常理解的“真正意义上的”泛型(相对于一些其他语言,比如 C++ 的模板)。这其中的核心原因可以追溯到 Java 的设计理念和对向后兼容性的考量,具体可以从以下几个方面来详细阐述:1. 类型擦除 (Type .............
  • 回答
    在 Java 编程中,我们常常会看到这样一种写法:使用 `Map` 或 `List` 这样的接口声明变量,而不是直接使用 `HashMap`、`ArrayList` 这样的具体实现类。这背后蕴含着一种非常重要的编程思想,也是 Java 语言设计上的一个亮点,我们来深入聊聊为什么这样做。核心思想:面向.............
  • 回答
    这个问题啊,问得挺实在的。很多人听到Python和Java都是用C/C++实现的,就觉得,“既然底层都是C/C++,那直接用C/C++不就得了?省事儿。” 这话听起来没毛病,但其实这里面涉及到很多关于编程语言设计、生态构建和实际应用场景的取舍,远不是“省事”两个字能概括的。咱们一层一层剥开来看。 为.............
  • 回答
    C++ 和 Java 都是非常流行且强大的编程语言,它们各有优劣,并在不同的领域发挥着重要作用。虽然 Java 在很多方面都非常出色,并且在某些领域已经取代了 C++,但仍然有一些 C++ 的独特之处是 Java 无法完全取代的,或者说取代的成本非常高。以下是 C++ 的一些 Java 不能(或难以.............
  • 回答
    .......
  • 回答
    .......
  • 回答
    Java 中 `String` 的设计,特别是关于 `==` 和 `.equals()` 的区别,是初学者常常会遇到的一个“坑”,也是 Java 语言设计者们深思熟虑的结果。要理解为什么不能直接用 `==` 比较 `String` 的值,我们需要深入探讨 Java 中对象的内存模型以及 `Strin.............
  • 回答
    Java 和 JavaScript 等语言之所以需要虚拟机(VM),而不是直接操作内存堆栈空间,是出于多方面的原因,这些原因共同构成了现代编程语言设计的重要基石。简单来说,虚拟机提供了一种 抽象层,它屏蔽了底层硬件的细节,带来了跨平台性、安全性、内存管理自动化、更高级别的抽象等诸多优势。下面我们来详.............
  • 回答
    .......
  • 回答
    这个问题很有意思!“360 垃圾清理”这个概念,如果用在 Java 的世界里,就好像是问:“为什么 Java 的垃圾回收机制,不像我们电脑上安装的 360 软件那样,主动去到处扫描、删除那些我们认为‘没用’的文件?”要弄明白这个,咱们得先聊聊 Java 的垃圾回收,它其实是个非常聪明且有组织的过程,.............
  • 回答
    好,咱就掰扯掰扯java为啥对泛型数组这事儿这么“矫情”,不直接给你整明白。这事儿啊,说起来也算是一段公案,得从java这门语言设计之初,以及它如何处理类型安全这件大事儿上头说起。核心矛盾:类型擦除与运行时类型检查的冲突你得明白java的泛型,尤其是泛型数组这块儿,最大的“绊脚石”就是它的类型擦除(.............
  • 回答
    Java 之所以选择不直接支持多重继承(Multiple Inheritance),并非出于某种简化的考虑,而是为了规避其可能带来的复杂性和潜在的开发陷阱。这个问题,如果深入挖掘,会涉及到语言设计哲学、代码的稳定性和可维护性等多个层面。首先,要理解多重继承的核心问题,我们可以想象一个场景:如果一个类.............
  • 回答
    Java 的 `switch` 语句在不加 `break` 的情况下继续执行下一个 `case`,这是一种被称为“穿透”或“fallthrough”的特性。这种设计并非是为了让程序“不用匹配条件”就执行下一个 `case`,而是为了提供一种代码流程控制的灵活性,允许开发者在特定场景下合并多个 `ca.............
  • 回答
    确实,在嵌入式领域,Java 语言的应用远不如 C/C++ 那么普遍,这背后有着多方面的原因,我们可以从几个关键维度来深入探讨。首先,我们得明白嵌入式系统的根本特性:资源受限。这体现在处理器性能、内存大小(RAM和ROM)、功耗需求以及实时性要求等多个方面。Java,作为一种高级语言,其设计哲学和运.............
  • 回答
    哎呀,这个问题真是让人头疼!安装 Java 遇上麻烦,确实挺让人沮丧的。你不是一个人在战斗,很多小伙伴在安装 Java 的时候都会遇到这样那样的问题。别急,咱们一点一点来捋清楚,看看这到底是怎么回事。要解决“安装不了 Java”的问题,首先得搞清楚“安装不了”具体表现是什么?这个问题就像是在问“车开.............
  • 回答
    要探讨 Go 的 Web 框架在速度上是否一定不如 Java,这是一个复杂且容易引起争议的话题,因为“速度”这个概念本身就需要具体化,而且在实际应用中,影响 Web 应用性能的因素远不止语言本身。不过,我们可以从几个关键方面来分析为什么在某些场景下,大家会有“Java Web 框架更快”的印象,以及.............
  • 回答
    当然,我们来聊聊 Go 和 Java 在性能上的那些事儿。你说 Go 在某些方面不如 Java,这个说法挺有意思的。我个人觉得,与其说是“不如”,不如说是“侧重点不同”导致的结果。Go 和 Java 的设计哲学就不一样,这直接影响到了它们各自的性能表现和适用场景。首先,咱们得说说 Go 的几个设计亮.............
  • 回答
    这确实是很多学习者和开发者都关心的问题。为什么我们依然在很多高校课堂上见到 C、C++、Java 的身影,而 Rust、Go、Scala 这样被认为“更强大”的语言却不那么普及呢?这背后涉及到一个复杂的多方面因素,不能简单归结为“高校不愿意教”或者“这些新语言不够好”。我尝试从几个关键角度来剖析这个.............
  • 回答
    特斯拉作为一家在汽车制造和能源领域都处于前沿的创新型公司,选择技术栈必然是经过深思熟虑的。关于为何特斯拉(或其部分业务)会选择 .NET Core,而可能不选择 Java 或 Go,这背后有其复杂的技术和商业考量。我们要明白,一家大规模的科技公司,其技术栈往往是多元化的,可能在不同的项目、不同的团队.............
  • 回答
    Java 官方一直以来都坚持不在函数中提供直接的“传址调用”(Pass by Address)机制,这背后有深刻的设计哲学和技术考量。理解这一点,需要从Java的核心设计理念以及它所解决的问题出发。以下是对这个问题的详细阐述: 1. Java 的核心设计理念:简洁、安全、面向对象Java 在设计之初.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有