Java 转换会引起开销吗? 为什么?

当我们将一种类型的对象强制转换为另一种类型时,是否存在开销?或者编译器只是解决所有问题,并且在运行时没有成本?

这是一般的事情,还是有不同的情况?

例如,假设我们有一个 Object []数组,其中每个元素可能有不同的类型。但是我们总是确切地知道,比如说,元素0是一个 Double,元素1是一个 String。(我知道这是一个错误的设计,但让我们假设我必须这样做。)

Java 的类型信息在运行时仍然存在吗?或者在编译之后所有的东西都被遗忘了,如果我们做(Double)元素[0] ,我们只需要跟随指针并将这8个字节解释为 Double,不管它是什么?

我不太清楚类型是如何在 Java 中实现的。如果你有任何关于书籍或文章的推荐,那么也谢谢你。

45923 次浏览

有两种类型的铸造:

隐式 转换,当您从一个类型转换到一个更广泛的类型时,这是自动完成的,没有开销:

String s = "Cast";
Object o = s; // implicit casting

显式 强制转换,当您从一个更宽的类型转换到一个更窄的类型时。对于这种情况,必须像这样明确地使用强制转换:

Object o = someObject;
String s = (String) o; // explicit casting

在第二种情况下,运行时会有开销,因为必须检查这两种类型,而且如果强制转换不可行,JVM 必须抛出 ClassCastException。

取自 JavaWorld: 转换的成本

铸造 用于在 中的引用类型之间 particular, for the type of casting 我们感兴趣的行动 给你。

Upcast 操作(也称为 在 Java 中扩展转换 语言规范)转换为 对祖先的子类引用 类引用。这个铸造 操作通常是自动的,因为 它总是安全的,可以是 由编译器直接实现。

Downcast 操作(也称为 在 Java 中缩小转换 语言规范)转换为 对子类的祖先类引用 参考。这个铸造操作 创建执行开销,因为 Java requires that the cast be checked at 运行时来确保它是有效的。 如果引用的对象不是 的目标类型的实例 该类型的强制转换或子类, the attempted cast is not permitted 必须抛出一个 ClassCastException.

例如,假设我们有一个 Object []数组,其中每个元素可能有不同的类型。但是我们总是确切地知道,比如说,元素0是一个 Double,元素1是一个 String。(我知道这是一个错误的设计,但让我们假设我必须这样做。)

编译器不记录数组中单个元素的类型。它只是检查每个元素表达式的类型是否可赋值给数组元素类型。

Java 的类型信息在运行时仍然存在吗?或者在编译之后所有的东西都被遗忘了,如果我们做(Double)元素[0] ,我们只需要跟随指针并将这8个字节解释为 Double,不管它是什么?

有些信息在运行时保留,但不保留单个元素的静态类型。您可以通过查看类文件格式来判断这一点。

从理论上讲,JIT 编译器可以使用“转义分析”来消除某些赋值中不必要的类型检查。然而,这样做的程度,你建议将超出了现实的优化界限。分析单个元素类型的回报太小。

此外,人们不应该编写这样的应用程序代码。

在运行时执行强制转换的字节码指令称为 checkcast。您可以使用 javap反汇编 Java 代码,以查看生成了什么指令。

对于数组,Java 在运行时保留类型信息。大多数情况下,编译器会为您捕获类型错误,但是在有些情况下,当您试图在数组中存储对象时,会遇到 ArrayStoreException,但是类型不匹配(编译器也没有捕捉到它)。Java 语言规范给出了以下例子:

class Point { int x, y; }
class ColoredPoint extends Point { int color; }
class Test {
public static void main(String[] args) {
ColoredPoint[] cpa = new ColoredPoint[10];
Point[] pa = cpa;
System.out.println(pa[1] == null);
try {
pa[0] = new Point();
} catch (ArrayStoreException e) {
System.out.println(e);
}
}
}

Point[] pa = cpa是有效的,因为 ColoredPoint是 Point 的一个子类,但是 pa[0] = new Point()是无效的。

这与泛型类型相反,泛型类型在运行时没有保存类型信息。编译器在必要时插入 checkcast指令。

泛型类型和数组类型的这种差异使得混合使用数组和泛型类型通常不合适。

对于 Java 的合理实现:

每个对象都有一个头,其中包含一个指向运行时类型的指针(例如 DoubleString,但它永远不可能是 CharSequenceAbstractList)。假设运行时编译器(在 Sun 的例子中通常是 HotSpot)不能静态地确定类型,那么生成的机器代码需要执行一些检查。

首先,需要读取指向运行时类型的指针。无论如何,这对于在类似的情况下调用虚方法是必要的。

对于类型转换,在到达 java.lang.Object之前,确切地知道有多少个超类,因此类型可以从类型指针(实际上是 HotSpot 中的前八个)以常量偏移量读取。这同样类似于读取虚方法的方法指针。

然后读取值只需要与预期的强制转换静态类型进行比较。根据不同的指令集架构,另一个指令将需要分支(或错误)到不正确的分支上。32位 ARM 等 ISA 有条件指令,可以有悲欢离合的路径通过。

由于接口的多重继承,接口变得更加困难。通常,对接口的最后两个强制转换缓存在运行时类型中。在非常早期的时候(十多年前) ,接口有点慢,但是现在已经不重要了。

希望您可以看到,这类事情在很大程度上与性能无关。你的源代码更重要。就性能而言,在您的场景中,最大的问题可能是到处追逐对象指针而导致缓存丢失(类型信息当然是常见的)。

In theory, there is overhead introduced. However, modern JVMs are smart. 每个实现都是不同的,但是假设可能存在一个实现,JIT 优化了转换检查,而且可以保证永远不会出现冲突,这种假设并非不合理。 至于哪些具体的 JVM 提供了这一点,我无法告诉您。我必须承认,我自己也想知道 JIT 优化的具体细节,但这些都是 JVM 工程师需要担心的问题。

这个故事的寓意是首先编写可以理解的代码。 很有可能不是因为选角。 永远不要牺牲干净、安全的代码来优化它,直到你知道你需要这样做。