Java转换会带来开销吗?为什么?


103

当我们将一种类型的对象转换为另一种类型时,是否会有开销?还是编译器可以解决所有问题,并且在运行时没有成本?

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

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

Java的类型信息是否仍在运行时保留?还是编译后一切都被遗忘了,如果我们执行(Double)elements [0],我们将仅遵循指针并将这8个字节解释为double,无论是什么?

我不清楚在Java中如何完成类型。如果您对书籍或文章有任何建议,那么也谢谢。


instanceof和cast的性能相当不错。我张贴在Java7各地不同的做法问题的一些时间在这里:stackoverflow.com/questions/16320014/...
Wheezil

这另一个问题有很好的答案stackoverflow.com/questions/16741323/…–
user454322

Answers:


77

有2种类型的转换:

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

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。


100
那篇JavaWorld文章已有10多年的历史了,所以我会说任何关于使用最优质盐的性能的陈述。
skaffman 2010年

@skaffman,事实上,我会接受一言不发的声明(无论是否执行)。
Pacerier,2014年

如果我不将强制转换的对象分配给引用,而只是对其调用方法,会是同样的情况吗?喜欢((String)o).someMethodOfCastedClass()
Parth Vishvajit

2
现在这篇文章已经快20年了。答案也有很多年了。这个问题需要一个现代的答案。
拉斯拉诺夫

原始类型呢?我的意思是,例如-从int转换为short会导致类似的开销吗?
luke1985 '19

44

对于Java的合理实现:

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

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

为了强制转换为类类型,在您命中之前java.lang.Object,确切知道有多少个超类,因此可以从类型指针(实际上是HotSpot中的前八个)偏移一定的距离来读取类型。同样,这类似于读取虚拟方法的方法指针。

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

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

希望您能看到这种情况与性能无关。您的源代码更为重要。在性能方面,您的方案中最大的问题很可能是在各处跟踪对象指针而导致的高速缓存未命中(类型信息当然很常见)。


1
有趣-这是否意味着对于非接口类,如果我编写Superclass sc =(Superclass)子类;(jit即:加载时间)编译器将“静态地”在其“类”标头中的每个超类和子类的Object中添加偏移量,然后通过简单的添加+比较就能解决问题?-很好又快:)对于接口,我认为不比一个小的哈希表或btree更糟?
彼得2012年

@peterk对于类之间的转换,对象地址和“ vtbl”(方法指针表,以及类层次结构表,接口高速缓存等)都不会改变。因此,[class]强制类型检查类型,如果适合,则无需进行其他任何操作。
Tom Hawtin-大头钉

8

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

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

Java的类型信息是否仍在运行时保留?还是编译后一切都被遗忘了,如果我们执行(Double)elements [0],我们将仅遵循指针并将这8个字节解释为double,无论是什么?

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

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

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


1
那原语呢? (float) Math.toDegrees(theta)这里还会有大量的开销吗?
SD

2
一些原始类型的转换会产生开销。它是否重要取决于上下文。
Stephen C

6

用于在运行时执行转换的字节码指令称为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在必要时插入指令。

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


2

从理论上讲,引入了开销。但是,现代JVM很聪明。每个实现都是不同的,但是可以假设存在一个JIT优化了强制转换检查的实现,因为它可以保证永远不会发生冲突。至于哪些特定的JVM提供此功能,我无法告诉您。我必须承认我想自己了解JIT优化的细节,但这是JVM工程师所担心的。

故事的寓意是首先编写可理解的代码。如果您遇到速度变慢的情况,请分析并确定问题所在。不太可能不是由于转换造成的。除非您知道需要,否则请不要牺牲干净,安全的代码来尝试对其进行优化。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.