Java类型转换是否会引入开销?为什么?

121

在我们将一个类型的对象转换为另一个类型时,是否存在额外开销?还是编译器会解决所有问题,没有运行时的成本?

这是一个普遍的问题吗?还是有不同的情况?

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

Java的类型信息是否仍然保留在运行时?还是在编译后就忘记了一切,如果我们执行(Double)elements [0],我们将只是跟随指针并将那8个字节解释为double,无论它是什么?

我非常不清楚Java中的类型是如何进行处理的。如果您有任何关于书籍或文章的推荐,谢谢。


instanceof和casting的性能非常好。我在Java7中发布了一些关于解决问题不同方法的时间记录,链接在这里:https://dev59.com/KWQo5IYBdhLWcg3wGsPN#28858680 - Wheezil
这个问题有非常好的答案https://dev59.com/PHLYa4cB1Zd3GeqPYoW0 - user454322
5个回答

91

有两种类型的转换:

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

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

显式转换是指从更宽的类型到更窄的类型。对于这种情况,您必须明确地使用转换,如下所示:

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

在第二种情况下,运行时会有额外的开销,因为必须检查这两种类型,并在无法进行强制转换的情况下,JVM 必须抛出 ClassCastException 异常。
摘自 JavaWorld:转换的代价

强制转换 用于在类型之间进行转换,特别是在我们感兴趣的引用类型之间。

向上转型 操作(也称为 Java 语言规范中的扩展转换)将子类引用转换为祖先类引用。由于该转换操作始终安全且可以直接由编译器实现,因此通常是自动的。

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


110
这篇JavaWorld的文章已经超过10年了,所以我会对它关于性能方面的陈述持非常怀疑的态度。 - skaffman
1
@skaffman,实际上,无论它发表的任何声明(无论涉及性能与否),我都会持保留态度。 - Pacerier
如果我不将转换后的对象分配给引用并直接调用它的方法,那么情况会是一样的吗?例如 ((String)o).someMethodOfCastedClass() - Parth Vishvajit
5
现在这篇文章已经接近20年了,回答也很早以前的了。这个问题需要一个现代的回答。 - Raslanove
1
原始类型呢?我的意思是,例如从int到short的强制转换是否会导致类似的开销? - luke1985
@luke1985 这可能是一个单独的问题,但简要地说:没有检查,但数据被缩小了,因此仍然存在开销(不同类型的开销)。 - Mark

43

针对Java的合理实现:

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

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

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

然后只需要将读取的值与强制转换的期望静态类型进行比较。根据指令集体系结构,另一个指令需要在不正确的分支上进行分支(或错误)。32位ARM等ISA具有条件指令并且可能能够使悲惨路径通过幸福路径。

由于接口的多重继承,所以接口更难处理。通常,最后两个转换为接口的转换在运行时类型中被缓存。在很早的日子里(十多年前),接口有点慢,但现在已经不再相关了。

希望您可以看到这种情况在性能上基本上是无关紧要的。您的源代码更重要。在性能方面,在您的场景中最大的影响可能是从所有地方追踪对象指针导致的缓存未命中(类型信息当然是共同的)。


1
有趣 - 这是否意味着对于非接口类,如果我编写Superclass sc =(Superclass)subclass; (jit即:加载时间)编译器将在Superclass和Subclass的“Class”头中“静态”放置从Object的偏移量,然后通过简单的加法+比较就能够解决问题?-这很好也很快:) 对于接口,我认为不会比小哈希表或btree更糟糕? - peterk
@peterk 对于类之间的转换,对象地址和“vtbl”(方法指针表,加上类层次结构、接口缓存等表)都不会改变。因此,[class]转换检查类型,如果匹配,则无需执行其他操作。 - Tom Hawtin - tackline

8
例如,假设我们有一个Object[]数组,其中每个元素可能具有不同的类型。但是我们总是确信,比如说,元素0是Double,元素1是String。(我知道这是一种错误的设计,但让我们假设我必须这样做。)
编译器不会记录数组中各个元素的类型。它只需检查每个元素表达式的类型是否可以分配给数组元素类型。
Java的类型信息在运行时仍然保留吗?或者编译后所有信息都被遗忘了,如果我们执行(Double)elements[0],我们将只是跟随指针并将这8个字节解释为double,而不管它是什么?
某些信息在运行时保留,但不包括各个元素的静态类型。您可以通过查看类文件格式来确定这一点。
理论上,JIT编译器可以使用“逃逸分析”来消除一些赋值中不必要的类型检查。然而,按照您所建议的程度进行此操作超出了现实优化的范围。分析各个元素的类型的回报太小了。
此外,人们不应该以这种方式编写应用程序代码。

1
原始类型怎么办?(float) Math.toDegrees(theta) 这里也会有显著的开销吗? - S.D.
2
某些基本类型的强制转换会产生开销。它是否显著取决于上下文。 - Stephen C

7

运行时执行类型转换的字节码指令称为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 是有效的,因为 ColoredPointPoint 的子类,但是 pa[0] = new Point() 是无效的。

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

数组和泛型类型之间的这种类型差异使得它们经常不适合混合使用。


1
理论上,引入了开销。 然而,现代JVM很聪明。 每个实现都不同,但可以假设存在一种实现,当它可以保证永远不会发生冲突时,就可以JIT优化掉类型转换检查。 至于哪些具体的JVM提供此功能,我无法告诉您。我必须承认,我自己也想了解JIT优化的细节,但这是JVM工程师需要担心的事情。
故事的寓意是首先编写易懂的代码。如果您遇到减速问题,请进行分析并确定问题所在。 很可能不是由于类型转换引起的。 在您知道需要进行优化之前,永远不要为了优化而牺牲清洁、安全的代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接