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

IT小君   2021-09-25T03:42:15

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

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

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

Java 的类型信息在运行时是否仍然保留?或者在编译后一切都被遗忘了,如果我们做 (Double)elements[0],我们只会跟随指针并将这 8 个字节解释为双精度值,不管是什么?

我不太清楚 Java 中的类型是如何完成的。如果您对书籍或文章有任何推荐,也谢谢。

评论(5)
IT小君

有两种类型的铸造:

隐式转换,当您从一个类型转换为更宽的类型时,这是自动完成的并且没有开销:

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

显式转换,当您从较宽的类型转换为较窄的类型时。对于这种情况,您必须像这样显式使用强制转换:

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

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

摘自JavaWorld:铸造成本

转换用于类型之间的转换——特别是引用类型之间的转换,对于我们在这里感兴趣的转换操作的类型。

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

向下转换操作(在 Java 语言规范中也称为缩小转换)将祖先类引用转换为子类引用。此转换操作会产生执行开销,因为 Java 要求在运行时检查转换以确保其有效。如果引用的对象既不是转型的目标类型的实例,也不是该类型的子类,则不允许尝试转型,并且必须抛出 java.lang.ClassCastException。

2021-09-25T03:42:16   回复
IT小君

对于 Java 的合理实现:

每个对象都有一个标头,其中包含一个指向运行时类型的指针(例如Doubleor String,但它永远不会是CharSequenceor AbstractList)。假设运行时编译器(在 Sun 的情况下通常是 HotSpot)无法静态确定类型,因此生成的机器代码需要执行一些检查。

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

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

然后读取值只需要与强制转换的预期静态类型进行比较。根据指令集架构,另一条指令需要在不正确的分支上分支(或出错)。诸如 32 位 ARM 之类的 ISA 具有条件指令,并且可能能够让悲伤路径通过快乐路径。

由于接口的多重继承,接口更加困难。通常,对接口的最后两个强制转换缓存在运行时类型中。在早期(十多年前),接口有点慢,但这不再相关。

希望你能看到这种事情在很大程度上与性能无关。你的源代码更重要。在性能方面,您的场景中最大的打击可能是由于到处追逐对象指针而导致的缓存未命中(类型信息当然很常见)。

2021-09-25T03:42:16   回复
IT小君

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

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

Java 的类型信息在运行时是否仍然保留?或者在编译后一切都被遗忘了,如果我们做 (Double)elements[0],我们只会跟随指针并将这 8 个字节解释为双精度值,不管是什么?

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

理论上,JIT 编译器可以使用“转义分析”来消除某些赋值中不必要的类型检查。但是,按照您建议的程度执行此操作将超出实际优化的范围。分析单个元素的类型的收益太小。

此外,人们无论如何都不应该编写这样的应用程序代码。

2021-09-25T03:42:16   回复
IT小君

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

对于数组,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在必要时插入指令。

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

2021-09-25T03:42:17   回复
IT小君

理论上,引入了开销。但是,现代 JVM 很智能。每个实现都是不同的,但假设可能存在一个实现,当它可以保证永远不会发生冲突时,JIT 优化了强制转换检查并不是没有道理的。至于哪个特定的 JVM 提供了这个,我不能告诉你。我必须承认我想自己了解 JIT 优化的细节,但这些是 JVM 工程师需要担心的。

这个故事的寓意是首先编写可理解的代码。如果您遇到速度变慢的情况,请分析并确定您的问题。很可能不是因为铸造。永远不要为了优化代码而牺牲干净、安全的代码,直到您知道自己需要这样做。

2021-09-25T03:42:17   回复