为啥有些语言需要装箱和拆箱?
Posted
技术标签:
【中文标题】为啥有些语言需要装箱和拆箱?【英文标题】:Why do some languages need Boxing and Unboxing?为什么有些语言需要装箱和拆箱? 【发布时间】:2009-06-24 19:19:24 【问题描述】:这不是什么是装箱和拆箱的问题, 而是为什么像 Java 和 C# 这样的语言需要它?
我非常熟悉 C++、STL 和 Boost。
在 C++ 中我可以很容易地写出这样的东西,
std::vector<double> dummy;
我对Java有一些经验,但我真的很惊讶,因为我不得不写这样的东西,
ArrayList<Double> dummy = new ArrayList<Double>();
我的问题,为什么它应该是一个对象,在谈论泛型时,在技术上包含原始类型有什么困难?
【问题讨论】:
从 C++ 到 Java,当我发现这个事实时,我完全震惊了…… 【参考方案1】:在谈论泛型时,在技术上包含原始类型有什么困难?
在 Java 中,这是因为泛型的工作方式。在 Java 中,泛型是一种编译时技巧,它会阻止您将 Image
对象放入 ArrayList<String>
。但是,Java 的泛型是通过类型擦除实现的:泛型类型信息在运行时丢失。这是出于兼容性原因,因为泛型是在 Java 生命周期的后期添加的。这意味着,在运行时,ArrayList<String>
实际上是一个ArrayList<Object>
(或者更好的是:只是ArrayList
,它在其所有方法中都期望并返回Object
),当您检索到时自动转换为String
价值。
但由于int
不是从Object
派生的,因此您不能将它放在期望(在运行时)Object
的 ArrayList 中,也不能将Object
转换为int
.这意味着原语int
必须包装成一个继承自Object
的类型,例如Integer
。
例如,C# 的工作方式不同。 C# 中的泛型也在运行时强制执行,List<int>
不需要装箱。只有当您尝试将 int
之类的值类型存储在 object
之类的引用类型变量中时,C# 中的装箱才会发生。由于 C# 中的 int
继承自 C# 中的 Object
,因此编写 object obj = 2
是完全有效的,但是 int 将被装箱,这是由编译器自动完成的(没有 Integer
引用类型暴露给用户或任何东西)。
【讨论】:
只是希望我的问题会被注意到我敢问:为什么java没有通过自动装箱实现泛型的原语?我的意思是如果 List装箱和拆箱是语言(如 C# 和 Java)实现其内存分配策略的方式所必需的。
某些类型分配在堆栈上,其他类型分配在堆上。为了将堆栈分配的类型视为堆分配的类型,需要装箱以将堆栈分配的类型移动到堆上。拆箱是相反的过程。
在 C# 中,堆栈分配的类型称为值类型(例如System.Int32
和System.DateTime
),堆分配的类型称为引用类型(例如@987654326 @ 和 System.String
)。
在某些情况下,将值类型视为引用类型是有利的(反射就是一个例子),但在大多数情况下,最好避免装箱和拆箱。
【讨论】:
小心值类型和基于堆栈的分配。 Eric Lippert 有两篇关于该主题的精彩博文:blogs.msdn.com/ericlippert/archive/2009/04/27/… 和 blogs.msdn.com/ericlippert/archive/2009/05/04/… AFAIK,除非您指定 List【参考方案3】:我相信这也是因为原语不继承自 Object。假设您有一个方法希望能够接受任何东西作为参数,例如。
class Printer
public void print(Object o)
...
您可能需要向该方法传递一个简单的原始值,例如:
printer.print(5);
你可以在不装箱/拆箱的情况下做到这一点,因为 5 是一个原始的而不是一个对象。您可以为每种原始类型重载 print 方法以启用此类功能,但这很痛苦。
【讨论】:
【参考方案4】:我只能告诉你为什么 Java 不支持泛型中的原始类型。
首先存在的问题是,每次支持这一点的问题都会引发讨论,如果 java 甚至应该有原始类型。这当然阻碍了对实际问题的讨论。
不包含它的第二个主要原因是他们想要二进制向后兼容性,因此它可以在不知道泛型的 VM 上运行而无需修改。这种向后兼容性/迁移兼容性的原因也是为什么现在 Collections API 支持泛型并保持不变并且没有(如在 C# 中引入泛型时)一套全新的泛型感知 Collection API。
兼容性是使用 ersure 完成的(在编译时删除了通用类型参数信息),这也是您在 java 中收到如此多未经检查的强制转换警告的原因。
您仍然可以添加具体化的泛型,但这并不容易。仅添加类型信息添加运行时而不是删除它是行不通的,因为它破坏了源代码和二进制兼容性(您不能继续使用原始类型,也不能调用现有的编译代码,因为它们没有相应的方法)。
另一种方法是 C# 选择的方法:见上文
并且此用例不支持自动装箱/拆箱,因为自动装箱成本太高。
Java theory and practice: Generics gotchas
【讨论】:
【参考方案5】:堆中存储的每个非数组非字符串对象都包含一个 8 或 16 字节的标头(32/64 位系统的大小),后面是该对象的公共和私有字段的内容。数组和字符串具有上述标头,再加上一些定义数组长度和每个元素大小的字节(可能还有维数、每个额外维的长度等),然后是第一个的所有字段元素,然后是第二个元素的所有字段,等等。给定一个对象的引用,系统可以轻松检查标题并确定它是什么类型。
引用类型的存储位置包含一个 4 或 8 字节的值,用于唯一标识存储在堆上的对象。在目前的实现中,该值是一个指针,但更容易(并且在语义上等效)将其视为“对象 ID”。
值类型存储位置保存值类型字段的内容,但没有任何关联的标头。如果代码声明了一个Int32
类型的变量,则无需存储Int32
说明它是什么的信息。该位置拥有Int32
的事实被有效地存储为程序的一部分,因此不必将其存储在该位置本身中。这代表了很大的节省,例如,如果一个对象有一百万个对象,每个对象都有一个Int32
类型的字段。每个持有Int32
的对象都有一个标头,用于标识可以操作它的类。由于该类代码的一个副本可以对数百万个实例中的任何一个进行操作,因此将字段是Int32
作为代码的一部分这一事实比让每个字段的存储都包含有关内容的信息要高效得多是的。
当请求将值类型存储位置的内容传递给不知道期望该特定值类型的代码时,装箱是必要的。期望未知类型对象的代码可以接受对存储在堆上的对象的引用。由于存储在堆上的每个对象都有一个标头来标识它是什么类型的对象,因此代码可以在需要以需要知道其类型的方式使用对象时使用该标头。
请注意,在 .net 中,可以声明所谓的泛型类和方法。每个这样的声明都会自动生成一系列相同的类或方法,除了它们期望作用的对象类型。如果将Int32
传递给例程DoSomething<T>(T param)
,则会自动生成一个例程版本,其中T
类型的每个实例都被Int32
有效替换。该版本的例程将知道声明为T
类型的每个存储位置都包含一个Int32
,因此就像例程被硬编码为使用Int32
存储位置的情况一样,它不是必需的将类型信息与这些位置本身一起存储。
【讨论】:
【参考方案6】:在 Java 和 C#(与 C++ 不同)中,一切都扩展了 Object,因此像 ArrayList 这样的集合类可以包含 Object 或其任何后代(基本上是任何东西)。
然而,出于性能原因,Java 中的原语或 C# 中的值类型被赋予了特殊的地位。他们不是对象。你不能这样做(在 Java 中):
7.toString()
尽管 toString 是 Object.为了将这种点头与性能联系起来,创建了等效的对象。 AutoBoxing 删除了必须将原语放入其包装类并再次取出的样板代码,使代码更具可读性。
C# 中值类型和对象之间的区别更加灰色。请参阅 here 了解它们有何不同。
【讨论】:
在 C# 中,原语被实现为具有方法的结构(例如 int 在 System 命名空间中的 Int32 中定义),因此 7.ToString() 可以正常工作。虽然 C# 中的所有内容都派生自 Object,但 Java 并非如此。 @JulianR:更具体地说,所有 CLR 值类型都继承自 System.ValueType,而 System.ValueType 又继承自 System.Object。 System.ValueType 覆盖了许多 System.Object 的虚拟方法,因此在调用这些方法时避免了装箱。导致值类型装箱的唯一方法是 Object.GetType 和 Object.MemberwiseClone。以上是关于为啥有些语言需要装箱和拆箱?的主要内容,如果未能解决你的问题,请参考以下文章