当我们将一种类型的对象转换为另一种类型时,是否有任何开销?或者编译器只解析所有内容并且在运行时没有成本?
这是一般事情,还是有不同的情况?
例如,假设我们有一个Object []数组,其中每个元素可能有不同的类型。但我们总是知道,例如,元素0是一个Double,元素1是一个String。 (我知道这是一个错误的设计,但我们假设我必须这样做。)
Java的类型信息是否仍然在运行时保留?或者编译后遗忘一切,如果我们做(Double)元素[0],我们只需跟随指针并将这8个字节解释为double,不管是什么?
我不清楚Java中的类型是如何完成的。如果您对书籍或文章有任何建议,那么也要感谢。
答案 0 :(得分:70)
有两种类型的铸造:
隐式转换,当你从一个类型转换为一个更宽的类型时,这是自动完成的并且没有开销:
String s = "Cast";
Object o = s; // implicit casting
当你从更宽的类型转到更窄的类型时,显式转换。对于这种情况,您必须明确使用类似的转换:
Object o = someObject;
String s = (String) o; // explicit casting
在第二种情况下,运行时会产生开销,因为必须检查这两种类型,并且如果转换不可行,JVM必须抛出ClassCastException。
取自JavaWorld: The cost of casting
Casting 用于转换 类型 - 在引用类型之间 特别是对于铸造类型 我们感兴趣的操作 这里。
Upcast 操作(也称为 扩展Java中的转换 语言规范)转换a 子类引用祖先 课堂参考。这个铸造 操作通常是自动的,因为 它总是安全的,可以 由编译器直接实现。
Downcast 操作(也称为 缩小Java中的转换 语言规范)转换 祖先类引用子类 参考。这种铸造操作 从Java开始创建执行开销 要求检查演员表 运行时以确保它是有效的。 如果引用的对象不是 目标类型的实例 演员或该类型的子类, 不允许尝试演员表演 并且必须抛出一个 java.lang.ClassCastException。
答案 1 :(得分:41)
对于Java的合理实现:
每个对象都有一个标题,其中包含指向运行时类型的指针(例如Double
或String
,但它永远不会是CharSequence
或{{1} })。假设运行时编译器(通常是Sun的HotSpot)无法静态确定类型,则需要通过生成的机器代码执行某些检查。
首先需要读取指向运行时类型的指针。无论如何,这对于在类似情况下调用虚拟方法是必要的。
为了转换为类类型,在确定AbstractList
之前确切知道有多少个超类,因此可以在类型指针的常量偏移处读取类型(实际上是HotSpot中的前八个) 。这类似于读取虚方法的方法指针。
然后,读取值只需要与演员的预期静态类型进行比较。根据指令集架构,另一条指令需要在不正确的分支上分支(或故障)。诸如32位ARM之类的ISA具有条件指令,并且可能能够让悲伤路径通过快乐路径。
由于接口的多重继承,接口更加困难。通常,接口的最后两个强制类型转换为运行时类型。在很早的时候(十多年前),界面有点慢,但这已经不再适用了。
希望你能看到这种事情在很大程度上与性能无关。您的源代码更重要。在性能方面,你的场景中最大的打击可能是在整个地方追逐对象指针的缓存未命中(类型信息当然是常见的)。
答案 2 :(得分:7)
例如,假设我们有一个Object []数组,其中每个元素可能有不同的类型。但我们总是知道,例如,元素0是一个Double,元素1是一个String。 (我知道这是一个错误的设计,但我们假设我必须这样做。)
编译器不会记录数组中各个元素的类型。它只是检查每个元素表达式的类型是否可分配给数组元素类型。
Java的类型信息是否仍然在运行时保留?或者编译后遗忘一切,如果我们做(Double)元素[0],我们只需跟随指针并将这8个字节解释为double,不管是什么?
某些信息在运行时保留,但不是单个元素的静态类型。您可以通过查看类文件格式来判断这一点。
理论上,JIT编译器可以使用“转义分析”来消除某些赋值中不必要的类型检查。但是,按照您建议的程度执行此操作将超出实际优化的范围。分析单个元素类型的收益太小。
此外,人们不应该编写类似的应用程序代码。
答案 3 :(得分:5)
用于在运行时执行转换的字节代码指令称为checkcast
。您可以使用javap
反汇编Java代码,以查看生成的指令。
对于数组,Java在运行时保留类型信息。大多数情况下,编译器会为您捕获类型错误,但在某些情况下,当您尝试将对象存储在数组中时会遇到ArrayStoreException
,但类型不匹配(并且编译器没有抓住它。 Java language spec给出了以下示例:
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
指令。
泛型类型和数组的类型差异使得它通常不适合混合数组和泛型类型。
答案 4 :(得分:1)
理论上,引入了开销。 但是,现代JVM很聪明。 每个实现都是不同的,但假设可以存在一个JIT优化离开转换检查的实现,它可以保证永远不会发生冲突,这是不合理的。 至于哪些特定的JVM提供这个,我无法告诉你。我必须承认我自己想知道JIT优化的具体细节,但这些都是让JVM工程师担心的。
故事的寓意是首先编写可理解的代码。如果您遇到速度下降,请说明并确定问题。 很可能因为铸造而无法获胜。 在您知道自己需要的情况下,永远不要牺牲干净,安全的代码来优化它。