为什么Java集合不能直接存储原始类型?

145

Java集合只能存储对象,而不能存储基本类型;但是我们可以存储包装类。

为什么有这个限制?


3
当你处理基本类型并且想使用队列进行发送,而你的发送速度非常快时,这种限制确实很糟糕。我现在正在处理自动装箱所花费的过多时间的问题。 - JPM
1
从技术上讲,原始类型是对象(确切地说是这些对象的单例实例),它们没有被“class”定义,而是由JVM定义。语句“int i = 1”定义了一个指针,指向JVM中定义“int”的对象的单例实例,该实例被设置为在JVM中某个地方定义的值 “1”。是的,在Java中有指针——这只是语言实现抽象出来的。原始类型无法用作通用类型,因为语言谓词所有通用类型必须是超类型“Object”——这就是为什么运行时“A<?>”编译为“A<Object>”。 - Shuba
2
@RobertEFry 原始类型在Java中不是对象,因此你所写的有关单例实例等的一切都是基本错误和令人困惑的。我建议阅读Java语言规范的"类型、值和变量"章节,其中定义了对象是什么:“对象(§4.3.1)是类类型的动态创建实例或动态创建数组。” - typeracer
7个回答

118

这是Java设计时的一个决定,有些人认为这是一个错误。容器需要对象,而原始类型不派生自Object。

这是.NET设计师从JVM学到的一点,并实现了值类型和泛型,从而在许多情况下消除了装箱。在CLR中,通用容器可以将值类型存储为底层容器结构的一部分。

Java选择在编译器中100%添加通用支持,但没有得到JVM的支持。由于JVM的特性,不支持“非对象”对象。Java泛型允许您假装没有包装器,但您仍然要支付装箱的性能代价。对于某些类别的程序来说,这很重要。

装箱是一种技术妥协,我认为它是实现细节泄漏到语言中。自动装箱是一种不错的语法糖,但仍然会导致性能损失。如果可能的话,我希望编译器在自动装箱时警告我。(我不知道,也许现在已经有了,我在2010年写了这篇答案)。

关于装箱的一个好的解释:

为什么一些语言需要装箱和拆箱?

以及对Java泛型的批评:为什么一些人认为Java的泛型实现很糟糕?

为了捍卫Java,倒退回去批评很容易。JVM已经经受住了时间的考验,在许多方面都是一个好的设计。


6
不是错误,而是一个经过精心选择的权衡,我相信这个决定确实为Java服务得很好。 - DJClayworth
16
.NET从错误中学习并从一开始实现了自动装箱和泛型虚拟机级别的实现,避免了装箱开销。Java的尝试只是在语法层面上进行了改正,仍然存在自动装箱与不装箱之间性能损失的问题。Java的实现在处理大型数据结构时表现较差。 - codenheim
3
我认为默认自动装箱是一个错误,但应该有一种方法来标记变量和参数,以便在给它们赋值时自动装箱。即使现在,我认为应该添加一种方法来指定某些变量或参数不应接受新自动装箱的值[例如,应该可以传递类型为Object的Object.ReferenceEquals引用来标识已装箱的整数,但不应该允许传递整数值]。Java的自动拆箱在我看来非常令人讨厌。 - supercat

21
使实现更加容易。由于Java原始类型不被视为对象,因此您需要为每个原始类型创建一个单独的集合类(没有可共享的模板代码)。
当然,您可以这样做,只需查看GNU TroveApache Commons PrimitivesHPPC
除非您有非常大的集合,否则包装器的开销对人们来说并不重要(当您确实拥有非常大的原始集合时,您可能希望花费精力考虑使用/构建专门的数据结构)。

13

这是两个事实的结合:

  • Java基本类型不是引用类型(例如,int 不是 Object)。
  • Java使用引用类型的类型擦除来实现泛型(例如,在运行时,List<?> 实际上是一个 List<Object>)。

由于这两个事实都是真实存在的,泛型 Java 集合不能直接存储基本类型。为了方便起见,自动装箱被引入以允许将基本类型自动转换为引用类型。不过,无论如何,集合仍然存储对象引用。

这可以避免吗?或许可以。

  • 如果intObject,则根本不需要包装类型。
  • 如果不使用类型擦除来实现泛型,则可以使用基本类型作为类型参数。

8

有一个叫做自动装箱和自动拆箱的概念。如果你试图将一个int存储在List<Integer>中,Java编译器会自动将其转换为Integer


1
自动装箱是在Java 1.5与泛型一起引入的。 - Jeremy
1
但这只是编译时的事情;语法糖并没有性能优势。Java编译器会自动装箱,因此与像.NET这样的VM实现相比,存在性能损失,后者的泛型不涉及装箱。 - codenheim
1
@mrjoltcola:你的意思是什么?我只是在分享事实,没有进行争论。 - Jeremy
3
我的观点是指出语法和性能之间的区别很重要。我也认为我的评论是分享事实,而不是争论。谢谢。 - codenheim

4

这不算一个限制,对吧?

考虑一下,如果你想创建一个存储基本值类型的集合,你会怎么写一个可以存储int、float或char的集合呢?最有可能的是你需要多个集合,比如intlist和charlist等。

利用Java面向对象的特性,当你编写一个集合类时,它可以存储任何对象,所以你只需要一个集合类。这个想法,即多态性,非常强大,极大地简化了库的设计。


7
你如何编写一个能够存储整型、浮点型或字符的集合?—— 使用正确实现的泛型/模板,就像其他不会将所有内容都视为对象的语言一样。 - codenheim
在我六年的Java编程经验中,我几乎从未想过要存储一组原始数据类型。即使在少数情况下可能需要使用引用对象,额外的时间和空间成本也是微不足道的。特别是我发现很多人认为他们需要Map<int,T>,却忘记了数组可以很好地完成这个任务。 - DJClayworth
2
@DJClayworth 只有在键值非稀疏的情况下才能很好地工作。当然,您可以使用辅助数组来跟踪键,但这也有其自身的问题:相对有效的访问需要基于键顺序使两个数组都排序,以允许二进制搜索。这反过来将使插入和删除变得低效,除非插入/删除是模式化的,使得插入的项可能会出现在先前删除的项所在位置,并且/或某些缓冲区被插入到数组中等等。虽然有可用的资源,但能够在Java本身中实现这一点将是不错的。 - JAB
@JAB,如果键是稀疏的话,它实际上可以正常工作,只是需要比非稀疏键更多的内存。如果键是稀疏的,那就意味着它们不多,使用 Integer 作为键就可以正常工作。使用需要最少内存的方法。或者如果您不在意的话,随便用哪种方法都可以。 - DJClayworth

1
主要原因是Java的设计策略。 ++ 1)集合需要对象来操作,原始类型不是从对象派生的,这可能是另一个原因。 2)Java原始数据类型不是引用类型,例如int不是对象。
克服方法: 我们有自动装箱和自动拆箱的概念。因此,如果您尝试存储原始数据类型,编译器将自动将其转换为该原始数据类的对象。

0

我认为我们可能会在JDK中看到这个领域的进展,可能是在Java 10中,基于这个JEP - http://openjdk.java.net/jeps/218

如果你想避免在集合中装箱原始类型,现在有几个第三方选择。除了之前提到的第三方选项外,还有Eclipse Collections, FastUtilKoloboke

关于原始类型映射的比较也在一段时间前发布,标题为:大型HashMap概述:JDK, FastUtil, Goldman Sachs, HPPC, Koloboke, Trove。GS Collections(Goldman Sachs)库已迁移到Eclipse基金会,现在是Eclipse Collections。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接