编译时和运行时出现的向下转型/向上转型错误?

4
请检查以下程序。
当编译器在编译时抛出转换异常,以及何时在运行时抛出转换异常?我有疑问。
例如,在下面的程序中,表达式(Redwood)new Tree()应该在编译时失败,因为Tree不是Redwood。但是它没有在编译时失败,而是在运行时失败!
public class Redwood extends Tree {
     public static void main(String[] args) {
         new Redwood().go();
     }
     void go() {
         go2(new Tree(), new Redwood());
         go2((Redwood) new Tree(), new Redwood());
     }
     void go2(Tree t1, Redwood r1) {
         Redwood r2 = (Redwood)t1;
         Tree t2 = (Tree)r1;
     }
 }
 class Tree { }

编译器不会“发出异常”,它们会打印错误消息。 - user207421
6个回答

6

一棵树不一定是红木,但它可能是,因此没有编译时错误。

在这种情况下,很明显它不是红木,但编译器倾向于抱怨绝对不正确的事物,而不是可能不正确的事物。在这种情况下,逻辑是不正确的,而不是代码本身,逻辑是你的问题,而不是编译器的问题。

Tree t = new Redwood();
Redwood r = (Redwood) t;

编译时和运行时都完全有效。

(Redwood) t; 可以正常运行,但现在 (Redwood) new Tree() 不行,因为要转换的对象不是引用,而是一个 Tree 对象。为什么编译器不能在编译时决定呢? - sHAILU
4
new Tree() 返回一个指向 Tree 对象的引用。在 Java 中你从来不会有对象本身,只会有指向对象的引用。 - JB Nizet
JB Nizet,如果我们这样考虑的话,那么它就可以通过。 - sHAILU
1
编译器的工作不是检查你的逻辑,逻辑有问题,而不是类型转换。 - Mike

6
我在解释中添加了一个子类。
       Tree
      /    \
     /      \
    /        \ 
Redwood       Blackwood  

在这个层次结构中:

向上转型: 当我们沿着类层次结构将引用从子类向根方向转换时。 在这种情况下,我们不需要使用转换运算符

向上转型示例:

RedwoodBlackwood都是tree:因此

Tree t1;
Tree t2;
Redwood r = new Redwood() ;
Blackwood  b = new Blackwood() ; 

t1 = r;    // Valid 
t2 = b;    // Valid   

Downcast: 向类层次结构中从根类(父类)指向子类(或者子类的子类)的方向进行引用转换时,我们需要进行显式类型转换。

Downcast Example:

Redwood r = new Tree();  //compiler error, because Tree is not a Redwood 
Blackwood r = new Tree();  //compiler error, because Tree is not a Blackwood  

如果一个 Tree 对象 真的指向了 Redwood 或者 Blackwood 对象,那么你需要显式地进行类型转换,否则会在运行时出错。例如:

在这个例子中:

Tree t1;
Tree t2;
Redwood r = new Redwood() ;
Blackwood  b = new Blackwood() ; 

t1 = r;    // Valid 
t2 = r;    // Valid     

Redwood r2 = (Redwood)t1; 
Blackwood  b2 = (Blackwood)t2  

[答案]

为什么没有编译器错误?这是向下转型的示例:

源代码Redwood r = (Redwood) new Tree();首先创建Tree对象,然后将其强制类型转换为Redwood

您可以这样考虑:

Tree t = new Tree();`
Redwood r = (Redwood)t;  

所以在编译时没问题,但是为什么会出现运行时错误呢?实际上,Redwood 子类不能指向 Tree 父类对象。因此你的代码在运行时失败了。

Tree t = new Tree();

t 指向的是 Tree() 对象而不是 Redwood()。这就是运行时错误的原因。

编译器不知道 t 中的值,在语法上一切都正确。但是在运行时由于 Redwood r = (Redwood)t;,其中 tTree class 的一个对象,它变得有缺陷了。

[建议:]

我想建议您使用 instanceof 运算符:

强制转换操作用于在类型之间进行转换,而 instanceof 运算符用于在运行时检查类型信息。*

描述:

instanceof 运算符允许您确定对象的类型。它将对象放在运算符的左侧,类型放在运算符的右侧,并返回一个布尔值,指示对象是否属于该类型。以下是最清晰的示例:

if (t instanceof Redwood)
{
    Redwood r = (Redwood)t;
    // rest of your code
}

但我想补充一点:将基本类型转换为派生类型是不好的事情。

参考资料: 小心instanceof运算符


1
如果您想这样做,可以编写编译器,但这不被认为是编译器职责的一部分。编译器的工作是编译代码。如果代码可以编译,它将被编译。该代码符合Java规范,因此将被编译。这就像从修理工那里取回你的汽车并说:“他为什么没有加满油箱?油快要用完了!”答案是,他本来可以把油箱加满,但这不是他的工作。 - Mike
@Subin - 同意。可以添加许多额外功能,但本质上编译器只是编译代码。其余的都是锦上添花,不总是能得到你想要的所有锦上添花。 - Mike
@Subin 编译时规则的存在是为了捕捉那些根本不可能发生的转换尝试。 - Grijesh Chauhan
@Subin,你看的是逻辑,而不是语法。编译器将其视为(Redwood) (refernce_to_Tree),而不是(Redwood) new Tree(),这是有效的语法。 - Mike
它可能会更智能,但编译器的目的不是变得更智能,而是编译符合Java规范的代码。该代码符合Java规范,因此被编译。正如我之前提到的,这就像从修理工那里取回你的汽车并说:“嗯,他为什么没有加满油箱呢?它几乎空了!”答案是,他本来可以加满油箱,但那不是他的工作。 - Mike
显示剩余10条评论

5
编译器只会看表达式的编译时类型,不会对表达式的运行时类型做出任何假设。 new Tree() 的编译时类型是 Tree,因此 (Redwood)new Tree()(Redwood)myTreeVariable 没有区别。

同意。但是假设new Tree()的运行时类型永远不会是Redwood并且编译器会发出警告,这样做是否安全? - Subin Sebastian
1
@Subin。虽然编程语言可能有这样的规则,但实际上并没有。这很好,因为这种无意义的情况只会使语言变得混乱,而在现实世界中你永远不会遇到它。 - ignis

2
我有疑问,编译器什么时候会在编译级别引发强制转换异常,什么时候会在运行时引发?

严格来说,编译器不会“引发强制转换异常”。您将获得:

  • 编译错误或
  • 运行时异常。

(Redwood) new Tree()引发运行时异常而不是编译错误的原因是JLS(第5.5.1节)规定了这种情况应该发生。

具体来说,JLS说:

"给定一个编译时引用类型S(源)和一个编译时引用类型T(目标),如果由于以下规则而没有编译时错误,则从S到T存在强制转换。"

"如果S是类类型" AND "如果T是类类型,则| S | <:| T |或| T | <:| S |。否则,将出现编译时错误。"

其中 "| S | <: | T |" 表示类型 S 是类型 T 或类型 T 的子类型。

在这种情况下,S是Tree,T是Redwood,两者都是类,RedwoodTree的子类型...因此没有编译错误。

这是“错误”的事实并不相关。 JLS说这是合法的Java代码,因此不应该出现编译错误。(聪明的编译器可能会发出编译警告,表明表达式总是会引发异常...但这是另一个问题。)


JLS规则背后的推理在规范中没有详细说明,但我想它是这样的:

比较以下三个片段:

Redwood r = (Redwood) new Tree();

Tree t = new Tree();
Redwood r = (Redwood) t;

Tree t1 = new Tree();  Tree t2 = new Redwood();
Redwood r = (Redwood) (someCondition ? t1 : t2);

Tree t = gardenStore.purchaseTree();
Redwood r = (Redwood) t;

假设他们将第一个片段定义为编译错误:
第二个呢?那很容易证明。
第三个呢?这可能很容易......或者非常困难。
第四个呢?现在,片段的合法性取决于我们甚至可能没有源代码的方法的语义!
关键是,一旦您开始要求编译器证明有关表达式动态值的事情,您就会陷入导致停机问题的棘手境地。如果您使编译错误变成可选项,则会出现可怕的情况,其中一个编译器可以说程序有效,而另一个编译器可以说它有错误!

0

引用的树可能之前是一棵红杉树,这就是为什么代码能够编译成功的原因。

     Tree t1 =  new Redwood(); 
     Redwood r1 = (Redwood)t1;
     Tree t2 = (Tree)r1;

你可以看到,Tree()可能已经是一棵红杉树了。

0
需要补充的是,没有必要进行向下转型:由于红木也是一种树,因此您可以将其分配给树变量。
Redwood r;
Tree t = r; // no casting needed

你可以在Java类型系统中始终寻找替换原则。我相信有大量的资料可供参考。


(Redwood) new Tree() 在编译时和运行时都无法有效,因为要转换的对象不是引用,而是一个Tree对象。为什么编译器不能在编译时决定它呢? - sHAILU
编译器不执行代码,它只是检查代码是否有效。您提供了一个树(Tree),这是一个可能是红杉树(Redwood)的类。编译器的工作不是去检查您的逻辑是否合理。即使您编译int a = 1/0;,尽管它保证会给您一个ArithmeticException,但也可以通过编译。 - Mike
我向你展示相反的情况。请注意,我将Redwood分配给Tree,而不是将Tree分配给Redwood。其次,你的代码失败并不是因为引用和对象之间存在冲突。在Java中,对象=引用。你需要学习一些基础课程。 - Val
对象不等于引用。但这与OP的问题无关。 - ignis

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