为啥 Java Collections 不能直接存储 Primitives 类型?

Posted

技术标签:

【中文标题】为啥 Java Collections 不能直接存储 Primitives 类型?【英文标题】:Why can Java Collections not directly store Primitives types?为什么 Java Collections 不能直接存储 Primitives 类型? 【发布时间】:2011-01-31 02:08:19 【问题描述】:

Java 集合只存储对象,不存储原始类型;但是我们可以存储包装类。

为什么会有这个限制?

【问题讨论】:

当您处理原语并想要使用队列发送并且您的发送速率非常快时,此约束确实很糟糕。我现在正在处理自动装箱花费太多时间的问题。 从技术上讲,原始类型是对象(确切地说是此类的单个实例),它们只是不是由 class 定义的,而是由 JVM 定义的。语句int i = 1 定义了一个指向在JVM 中定义int 的对象的单例实例的指针,设置为在JVM 中某处定义的值1。是的,Java 中的指针——这只是通过语言实现从你那里抽象出来的。基元不能用作泛型,因为语言谓词所有泛型类型必须是超类型Object - 因此A<?> 在运行时编译为A<Object> @RobertEFry 原始类型在 Java 中是 not objects,所以你写的关于单例实例的所有内容都是错误的和令人困惑的。我建议阅读 Java 语言规范的 "Types, Values, and Variables" 章节,该章节定义了对象是什么:“对象(第 4.3.1 节)是类类型的动态创建实例或动态创建的数组。” 【参考方案1】:

这是一个 Java 设计决定,有些人认为这是一个错误。容器需要对象,而原语不是从对象派生的。

这是 .NET 设计人员从 JVM 学习并实现值类型和泛型的一个地方,因此在许多情况下消除了装箱。在 CLR 中,泛型容器可以将值类型存储为底层容器结构的一部分。

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

装箱是一种技术上的妥协,我觉得它是泄漏到语言中的实现细节。自动装箱是很好的语法糖,但仍然是性能损失。如果有的话,我希望编译器在自动装箱时警告我。 (据我所知,现在可能,我在 2010 年写了这个答案)。

关于拳击的一个很好的解释:Why do some languages need Boxing and Unboxing?

以及对 Java 泛型的批评:Why do some claim that Java's implementation of generics is bad?

在 Java 的辩护中,很容易向后看和批评。 JVM经受住了时间的考验,在很多方面都是不错的设计。

【讨论】:

不是一个错误,一个精心挑选的折衷方案,我相信它确实为 Java 提供了很好的服务。 .NET 从中吸取了教训并从一开始就实现了自动装箱,并在 VM 级别实现了泛型而没有装箱开销,这已经够大的错误了。 Java 自己的修正尝试只是一个语法级别的解决方案,它仍然受到自动装箱与根本没有装箱的性能影响。 Java 的实现在处理大型数据结构时表现不佳。 @mrjoltcola:恕我直言,默认自动装箱是一个错误,但应该有一种方法来标记变量和参数,它应该自动装箱给它们的值。即使是现在,我认为应该添加一种方法来指定某些变量或参数应该接受新自动装箱的值[例如传递 Object.ReferenceEquals 类型的 Object 引用应该是合法的,它标识装箱的整数,但传递整数值应该是非法的]。恕我直言,Java 的自动拆箱很讨厌。【参考方案2】:

使实施更容易。由于 Java 原语不被视为对象,因此您需要为这些原语中的每一个创建一个单独的集合类(无需共享模板代码)。

您当然可以这样做,只需查看GNU Trove、Apache Commons Primitives 或HPPC。

除非您有非常大的集合,否则包装器的开销对于人们来说并不重要(当您确实有非常大的原始集合时,您可能希望花精力研究使用/构建专门的数据结构)。

【讨论】:

【参考方案3】:

这是两个事实的结合:

Java 原始类型不是引用类型(例如,int 不是 Object) Java 使用引用类型的类型擦除来执行泛型(例如,List<?> 在运行时实际上是 List<Object>

由于这两个都是真的,泛型 Java 集合不能直接存储原始类型。为方便起见,引入了自动装箱以允许将原始类型自动装箱为引用类型。但请不要误会,无论如何,集合仍然存储对象引用。

这可以避免吗?也许吧。

如果 intObject,则根本不需要框类型。 如果没有使用类型擦除来完成泛型,则可以将原语用于类型参数。

【讨论】:

【参考方案4】:

有auto-boxing 和自动拆箱的概念。如果您尝试将int 存储在List<Integer> 中,Java 编译器会自动将其转换为Integer

【讨论】:

自动装箱是在 Java 1.5 中与泛型一起引入的。 但它是编译时的事情;没有性能优势的语法糖。 Java 编译器自动装箱,因此与 .NET 等泛型不涉及装箱的 VM 实现相比,性能损失。 @mrjoltcola:你的意思是什么?我只是在分享事实,而不是在争论。 我的意思是指出语法和性能之间的区别很重要。我也考虑我的 cmets 事实分享,而不是争论。谢谢。【参考方案5】:

这不是真正的约束吗?

考虑是否要创建一个存储原始值的集合。您将如何编写一个可以存储 int、float 或 char 的集合?很可能你最终会得到多个集合,所以你需要一个 intlist 和一个 charlist 等。

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

【讨论】:

"你将如何编写一个可以存储 int、float 或 char 的集合?" - 使用正确实现的泛型/模板,就像其他语言一样,不会因为假装一切都是对象而付出代价。 在使用 Java 的六年里,我几乎从未想过要存储原语集合。即使在我可能想要使用参考对象的额外时间和空间成本的少数情况下,也可以忽略不计。特别是我发现很多人认为他们想要 Map,却忘记了数组可以很好地完成这个技巧。 @DJClayworth 仅当键值非稀疏时才有效。当然,您可以使用辅助数组来跟踪键,但这有其自身的问题:相对有效的访问将需要根据键顺序对两个数组进行排序以允许二进制搜索,这反过来会进行插入和删除效率低下,除非插入/删除的模式是这样的Java 本身。 @JAB 实际上,如果键是稀疏的,它可以正常工作,它只需要比它们不稀疏的更多的内存。如果它们的键是稀疏的,这意味着它们并不多,并且使用 Integer 作为键可以正常工作。使用需要最少内存的任何方法。或者,如果您不在乎,则随心所欲。【参考方案6】:

主要原因是java的设计策略。 ++ 1) 集合需要对象进行操作,而基元不是从对象派生的 所以这可能是另一个原因。 2) Java 原始数据类型不是 ex 的引用类型。 int 不是对象。

要克服:-

我们有自动装箱和自动拆箱的概念。因此,如果您尝试存储原始数据类型,编译器会自动将其转换为该原始数据类的对象。

【讨论】:

【参考方案7】:

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

如果您现在想避免在集合中装箱原语,有几种第三方替代方案。除了前面提到的第三方选项之外,还有Eclipse Collections、FastUtil 和Koloboke。

前一段时间还发布了原始映射的比较,标题为:Large HashMap 概述:JDK, FastUtil, Goldman Sachs, HPPC, Koloboke, Trove。 GS Collections (Goldman Sachs) 库已迁移到 Eclipse Foundation,现在更名为 Eclipse Collections。

【讨论】:

以上是关于为啥 Java Collections 不能直接存储 Primitives 类型?的主要内容,如果未能解决你的问题,请参考以下文章

Java——Map接口,可变参数,collections(集合实现类)

为啥我不能腌制一个 typing.NamedTuple 而我可以腌制一个 collections.namedtuple?

为啥我不能在我的 ArrayList<T> 上调用 Collections.sort()?

为啥我的idea不能新建php文件,是否需要安装php的插件

为啥 malloc 不能在 FreeBSD-x64 内核空间分配大内存?

█■为啥要用实现接口的类实例化接口呢?