为什么有些语言需要拳击和拆箱?

时间:2009-06-24 19:19:25

标签: java c++ generics boxing unboxing

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

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

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

std::vector<double> dummy;

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

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

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

6 个答案:

答案 0 :(得分:47)

  

在谈论泛型时,在技术上难以包含原始类型吗?

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

但由于int不是从Object派生的,所以你不能把它放在期望(在运行时)Object的ArrayList中,你不能强制转换Object 1}}到int。这意味着必须将原始int包装到继承自Object的类型中,例如Integer

例如,C#的工作方式不同。 C#中的泛型也在运行时强制执行,List<int>不需要装箱。只有当您尝试在int等引用类型变量中存储object之类的值类型时,才会在C#中进行装箱。由于C#中的int继承自C#中的Object,因此编写object obj = 2完全有效,但int将被装箱,这由编译器自动完成(无Integer引用类型暴露给用户或任何东西)。

答案 1 :(得分:12)

拳击和拆箱是必不可少的,因为语言(如C#和Java)实现了他们的内存分配策略。

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

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

在某些情况下,能够像处理参考类型一样处理值类型(反射就是一个例子)是有利的,但在大多数情况下,最好避免装箱和拆箱。

答案 2 :(得分:2)

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

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

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

printer.print(5);

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

答案 3 :(得分:2)

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

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

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

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

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

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

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

Java theory and practice: Generics gotchas

答案 4 :(得分:1)

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

但是出于性能原因,java中的原语或C#中的值类型被赋予了特殊状态。他们不是对象。你不能做(在Java中):

 7.toString()

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

C#中值类型和对象之间的差异更加灰色。请参阅here,了解它们的不同之处。

答案 5 :(得分:1)

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

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

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

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

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