37

这不是什么是装箱和拆箱的问题,而是为什么像 Java 和 C# 这样的语言需要它?

我非常熟悉 C++、STL 和 Boost。

在 C++ 中,我可以很容易地写出这样的东西,

std::vector<double> dummy;

我对Java有一些经验,但我真的很惊讶,因为我不得不写这样的东西,

ArrayList<Double> dummy = new ArrayList<Double>();

我的问题,为什么它应该是一个对象,在谈论泛型时,在技术上包含原始类型有什么困难?

4

6 回答 6

58

在谈论泛型时,在技术上包含原始类型有什么难的?

在 Java 的情况下,这是因为泛型的工作方式。在 Java 中,泛型是一种编译时技巧,它可以防止您将Image对象放入ArrayList<String>. 但是,Java 的泛型是通过类型擦除实现的:泛型类型信息在运行时丢失。这是出于兼容性原因,因为泛型是在 Java 生命周期的后期添加的。这意味着,在运行时,anArrayList<String>实际上是一个ArrayList<Object>(或更好:只是在其所有方法ArrayList中期望并返回),当您检索一个值时会Object自动转换为。String

但是由于int不是从 派生的Object,因此您不能将其放入期望(在运行时)的 ArrayList 中,Object也不能将其Object转换int为。这意味着原语int必须包装成一个继承自 的类型Object,例如Integer.

例如,C# 的工作方式不同。C# 中的泛型也在运行时强制执行,并且不需要对List<int>. C# 中的装箱仅在您尝试将值类型存储int在引用类型变量(如object. 由于int在 C# 中继承自ObjectC#,因此编写object obj = 2是完全有效的,但是 int 将被装箱,这是由编译器自动完成的(不会Integer向用户或任何东西公开引用类型)。

于 2009-06-24T19:47:40.947 回答
12

装箱和拆箱是语言(如 C# 和 Java)实现其内存分配策略的一种必然。

某些类型分配在堆栈上,而其他类型则分配在堆上。为了将堆栈分配的类型视为堆分配的类型,需要装箱以将堆栈分配的类型移动到堆上。拆箱是相反的过程。

在 C# 中,堆栈分配的类型称为值类型(例如System.Int32System.DateTime),而堆分配的类型称为引用类型 (例如System.StreamSystem.String)。

在某些情况下,将值类型视为引用类型是有利的(反射就是一个例子),但在大多数情况下,最好避免装箱和拆箱。

于 2009-06-24T19:21:24.150 回答
2

我相信这也是因为原语不继承自 Object。假设您有一个方法希望能够接受任何东西作为参数,例如。

class Printer {
    public void print(Object o) {
        ...
    }
}

您可能需要将一个简单的原始值传递给该方法,例如:

printer.print(5);

您可以在不装箱/拆箱的情况下做到这一点,因为 5 是原语而不是对象。您可以为每种原始类型重载 print 方法以启用此类功能,但这很痛苦。

于 2009-06-24T19:29:14.353 回答
2

我只能告诉你 Java 为什么它不支持泛型中的原始类型。

首先存在的问题是,每次支持这一点的问题都会引发讨论,如果 java 甚至应该有原始类型。这当然阻碍了对实际问题的讨论。

其次,不包括它的主要原因是他们想要二进制向后兼容性,因此它可以在不知道泛型的 VM 上未经修改地运行。这种向后兼容性/迁移兼容性的原因也是为什么现在 Collections API 支持泛型并保持不变并且没有(如在 C# 中引入泛型时)一套全新的泛型感知 Collection API。

兼容性是使用 ersure 完成的(在编译时删除了通用类型参数信息),这也是您在 java 中收到如此多未经检查的强制转换警告的原因。

您仍然可以添加具体化的泛型,但这并不容易。仅添加类型信息添加运行时而不是删除它是行不通的,因为它破坏了源代码和二进制兼容性(您不能继续使用原始类型,也不能调用现有的编译代码,因为它们没有相应的方法)。

另一种方法是 C# 选择的方法:见上文

此用例不支持自动装箱/拆箱,因为自动装箱成本太高。

Java 理论与实践:泛型陷阱

于 2009-06-24T20:09:11.420 回答
2

每个存储在堆上的非数组非字符串对象都包含一个 8 或 16 字节的标头(32/64 位系统的大小),后跟该对象的公共和私有字段的内容。数组和字符串具有上述标头,再加上一些定义数组长度和每个元素大小的字节(可能还有维数、每个额外维的长度等),然后是第一个的所有字段元素,然后是第二个的所有字段,等等。给定一个对象的引用,系统可以很容易地检查标题并确定它是什么类型。

引用类型的存储位置包含一个 4 或 8 字节的值,用于唯一标识存储在堆上的对象。在目前的实现中,该值是一个指针,但更容易(并且在语义上等效)将其视为“对象 ID”。

值类型存储位置保存值类型字段的内容,但没有任何关联的标头。如果代码声明了一个 type 变量Int32,则无需存储Int32说明它是什么的信息。该位置包含一个的事实Int32被有效地存储为程序的一部分,因此它不必存储在该位置本身中。例如,如果一个对象有一百万个对象,每个对象都有一个 type 字段,那么这代表了很大的节省Int32。每个持有 的对象Int32都有一个标头,用于标识可以操作它的类。由于该类代码的一个副本可以对数百万个实例中的任何一个进行操作,因此该字段是Int32成为代码的一部分比让每个字段的存储都包含有关其内容的信息要高效得多。

当请求将值类型存储位置的内容传递给不知道期望该特定值类型的代码时,装箱是必要的。期望未知类型对象的代码可以接受对存储在堆上的对象的引用。由于存储在堆上的每个对象都有一个标头来标识它是什么类型的对象,因此代码可以在需要以需要知道其类型的方式使用对象时使用该标头。

请注意,在 .net 中,可以声明所谓的泛型类和方法。每个这样的声明都会自动生成一系列相同的类或方法,除了它们期望作用的对象类型之外。如果将 an 传递Int32给例程DoSomething<T>(T param),它将自动生成例程的一个版本,其中每个类型的实例都T被有效地替换为Int32. 该版本的例程将知道声明为类型的每个存储位置都T包含一个Int32,因此就像在例程被硬编码以使用Int32存储位置的情况下一样,没有必要将类型信息与这些位置本身一起存储。

于 2012-06-09T00:38:49.417 回答
1

在 Java 和 C#(与 C++ 不同)中,一切都扩展了 Object,因此像 ArrayList 这样的集合类可以包含 Object 或其任何后代(基本上是任何东西)。

然而,出于性能原因,Java 中的原语或 C# 中的值类型被赋予了特殊的地位。他们不是对象。你不能做类似(在Java中)的事情:

 7.toString()

即使 toString 是 Object 的一个方法。为了将这种点头与性能联系起来,创建了等效的对象。AutoBoxing 删除了必须将原语放在其包装类中并再次取出的样板代码,从而使代码更具可读性。

C# 中值类型和对象之间的区别更加灰色。请参阅此处了解它们的不同之处。

于 2009-06-24T19:29:31.653 回答