为什么对象的声明类型在运行时很重要?

4
在像Java这样的静态类型语言中,据我所知,类型声明基本上是为了在编译时捕获错误,这是与动态类型语言相比的明显优势。但是当Java进行后期绑定时,我们会遇到诸如“ClassCastException”之类的错误,显示了声明的类型在运行时某种程度上是相关的。但是为什么声明类型实际上很重要呢?
例如:
public class TestClass 
{
   public static void main(String[] args) 
   {
       Animal d = new Animal();
      ((Dog)d).bark(); //ClassCastException because an Animal is not a dog, which would make sense to throw at compile-time, but not at runtime.
   }
}

class Dog extends Animal{}

class Animal
{
   void bark()
   {
       System.out.println("Woof");
   }
}

我知道这只是一个不必要的强制类型转换的超级糟糕的例子,但我只是举了一个例子。我们始终试图避免运行时异常,那么为什么Java不能忽略强制类型转换并在实际对象类型上调用bark方法,这种情况下是Animal?我一直在阅读关于鸭子类型的文章,似乎可以在Java(即如果这个对象吠叫,那么让我们把它当作一只狗!)或任何静态类型语言中使用类似的适应,因为在运行时,Java似乎表现得很动态。

编辑:现在我想更多地思考它,这个问题涉及到运行时类型检查的需要。为什么需要发生这种情况?静态类型的语言在强制类型转换时会出现运行时异常,这似乎是不好的。


在这种情况下,您不需要转换,因为bark()Animal中定义。 - Ramanlfc
@Ramanifc,我知道,我想不出一个好的例子 - 我的问题是如果我遇到这样的类转换异常,运行时不会继续,但我想知道为什么它不会 - 这是可靠性问题还是更一般的问题? - rb612
你是指忽略了显式转换,对吗? - Dioxin
@VinceEmigh没错 - 我不明白为什么运行时声明类型甚至会有影响 - 多态和方法作用于实际对象类型,那么为什么无效的转换在运行时甚至会有影响呢?在编译时这是有意义的,因为Java不知道实际类型。 - rb612
@11thdimension - 你说得很对。运行时类型检查的整个概念以及为什么运行时对象的引用类型甚至很重要,对我来说都不是很清楚。 - rb612
显示剩余2条评论
5个回答

5
为什么Java不能忽略强制转换并在实际对象类型上调用bark方法,即在这种情况下是Animal?
这样做从语义上讲是错误的。如果Dog和Animal有不同版本的Bark方法会怎么样?
假设您在基类中有一个非虚方法Bark。当您在Dog上调用Bark时,它应该调用Dog.Bark;同样,当您在Animal上调用Bark时,它应该调用Animal.Bark。
如果编译器像您所说的那样忽略强制转换,它将最终调用错误的方法。
请注意,在C#中,默认情况下所有方法都是非虚拟的,与Java相反。因此,在C#的上下文中,这个论点更有意义。

1
但是使用多态,它总是在运行时调用实际类型,对吗? - rb612
@rb612 是的,但我说的是非虚拟方法。你错过了吗? - Sriram Sakthivel
1
@SriramSakthivel 但是在Java中所有方法都是虚拟的。方法总是分派到运行时类型。 - resueman
1
@resueman 嗯,可能会有final方法,对吧?那算是非虚方法吗?如果不是的话,考虑到问题标记了c#,那么我的回答就是关于c#的。 - Sriram Sakthivel
@SriramSakthivel 你是对的 - 我漏掉了那部分。我现在明白你的意思了。那是一个很好的论点。我正在尝试看到一个更多态化的 Java 等价物。 - rb612
然而,在转换异常的情况下,Java运行时已经知道Dog不是Animal的兼容类型。 - 11thdimension

3
当然,鸭子类型理论在Java中也可以使用。虽然很多东西都可以在Java中使用,但Java是按照特定的方式设计的,因此声明的类型就显得非常重要,这与其静态类型有关,而不是其他一些原因。
静态类型并不仅仅存在于编译时期。当然,它的大部分作用就是验证代码是否能够正确运行。但这还不够。由于不可能始终验证所有类型是否正确(例如将Object参数转换为特定类),因此还需要进行运行时类型检查。如果在编译时是“静态的”,而在运行时是“鸭子类型”,那么会产生非常混乱且难以调试的情况。

这很有帮助,谢谢。你能解释一下为什么静态类型语言必须进行运行时类型检查吗?我认为回答这个问题实际上就是回答我的问题了。 - rb612
Wikipedia中有一个很好的解释。正如你所看到的,有不止一种方法来完成任务,但它几乎总是涉及(至少某种形式的)运行时类型检查。 - Kayaman
这是一个很好的解释 - 谢谢。因此,需要进行动态检查以验证操作是否安全 - 好的,所以在这里,我们正在进行不安全的向下转换,但不像在整数对象上调用字符串方法那样不可能。但它是致命的。这没有意义。唯一让我觉得它在编译时失败有意义的原因是Java只知道引用类型,但在运行时,Java知道实际类型。 - rb612
你是对的,在编译时这种非法转换类型的错误是有意义的,因为如果将Animal视为Dog,则应该能够在Animal上调用Dog方法,如果您尝试执行此操作,则编译器无法捕获。但是在这里,Java使用有关实际对象类型的信息,因此我甚至不明白为什么转换会有任何区别或在运行时点是非法的。我一直学习转换和类型声明只是为了进行编译时类型检查。 - rb612
你说得对,如果在运行时有“duck”而在编译时有“static”,那将会是一场混乱。因此,如果鸭子类型不可选的实际原因是为了保持一致性,那么我很满意。感谢您的帮助@Kayaman - 我知道这是一次长时间的讨论。 - rb612
显示剩余7条评论

1

你能解释一下为什么在静态类型语言中需要进行运行时类型检查吗?

是的,这里有几个原因:

  1. 编译后的 Dog 类被其他项目中的某个人替换,并且新类甚至没有继承 Animal。
  2. 通过 Dserialization 加载了编译后的 Dog 类,但是被打包的某个错误版本的 Dog。

0

正如您所看到的错误信息, 编译器无法检测到类型转换错误,因为向下转型 "((Dog)d)" 是有效的转型,如果引用 Dog 是一个 Animal 对象。因此,它会抛出运行时错误而不是编译时错误。

异常线程 "main" java.lang.ClassCastException: com.example.com.example.Animal 无法转换为 com.example.com.example.Dog at com.example.com.example.Test.main(Test.java:12)


感谢@bmt - 我给出了一个糟糕的例子,但我是在询问ClassCastException的一般行为。 - rb612

0
编译器不知道引用的是哪个实际对象,它只知道引用类型。实际对象在运行时才会出现。因此,编译器不会为此给出编译器错误。

我知道它会抛出一个ClassCastException,这是在运行时而不是捕获编译时错误。我的问题就在于运行时而不是编译时。 - rb612

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