为什么接口必须用Java声明?

时间:2011-04-17 04:24:07

标签: java interface static-typing duck-typing structural-typing

有时我们有几个类具有相同签名的某些方法,但这些类与声明的Java接口不对应。例如,JTextFieldJButtonjavax.swing.*中的其他几个)都有方法

public void addActionListener(ActionListener l)

现在,假设我希望对具有该方法的对象执行某些操作;那么,我想要一个界面(或者自己定义它),例如。

  public interface CanAddActionListener {
      public void addActionListener(ActionListener l);
  }

所以我可以写:

  public void myMethod(CanAddActionListener aaa, ActionListener li) {
         aaa.addActionListener(li);
         ....

但遗憾的是,我不能:

     JButton button;
     ActionListener li;
     ...
     this.myMethod((CanAddActionListener)button,li);

此演员表是非法的。编译器知道 JButton 不是 a CanAddActionListener,因为该类尚未声明实现该接口... 然而它“实际上”实现它

这有时带来不便 - 而且Java本身已经修改了几个核心类来实现由旧方法(例如String implements CharSequence)构成的新接口。

我的问题是:为什么会这样?我理解声明一个类实现接口的实用程序。但无论如何,看看我的例子,为什么编译器不能推断类JButton“满足”接口声明(查看它内部)并接受转换?这是编译器效率的问题还是存在更多基本问题?

我对答案的总结:这是一个Java可以允许某些“结构类型”(一种鸭子打字 - 但在编译时检查)的情况。它没有。除了一些(我不清楚)性能和实现困难之外,还有一个更为基本的概念:在Java中,接口的声明(以及一般来说,所有内容)并不仅仅是结构(具有这些签名的方法)但是语义:这些方法应该实现某些特定的行为/意图。因此,结构满足某些接口的类(即,它具有所需签名的方法)并不一定满足它语义(一个极端的例子:回忆“标记”接口“,甚至没有方法!)。因此,Java可以声明一个类实现了一个接口,因为(并且只是因为)已经显式声明了它。其他语言(Go,Scala)有其他哲学。

6 个答案:

答案 0 :(得分:8)

Java的设计选择使得实现类明确地声明它们实现的接口就是 - 设计选择。可以肯定的是,JVM已针对此选择进行了优化,并且实现了另一种选择(例如,Scala的结构类型)可能现在需要额外的成本,除非并且直到添加了一些新的JVM指令。

那么究竟是的设计选择呢?这一切都归结为方法的语义。考虑一下:以下方法在语义上是一样的吗?

  • draw(String graphicalShapeName)
  • draw(String handgunName)
  • draw(String playingCardName)

这三种方法都有签名draw(String)。人类可能会推断出它们与参数名称有不同的语义,或者通过阅读一些文档。机器有什么办法可以说它们不同吗?

Java的设计选择是要求类的开发人员明确声明方法符合预定义接口的语义:

interface GraphicalDisplay {
    ...
    void draw(String graphicalShapeName);
    ...
}

class JavascriptCanvas implements GraphicalDisplay {
    ...
    public void draw(String shape);
    ...
}

毫无疑问,draw中的JavascriptCanvas方法旨在与图形显示的draw方法相匹配。如果有人试图传递要拔出手枪的物体,机器可以检测到错误。

Go的设计选择更加自由,允许在事后定义接口。具体类不需要声明它实现的接口。相反,新卡片游戏组件的设计者可以声明提供游戏卡的对象必须具有与签名draw(String)匹配的方法。这样做的好处是可以使用任何具有该方法的现有类而无需更改其源代码,但缺点是该类可能会拔出手枪而不是扑克牌。

鸭子类型语言的设计选择是完全省去形式接口并简单地匹配方法签名。接口(或“协议”)的任何概念都是纯粹的惯用语,没有直接的语言支持。

这些只是众多可能设计选择中的三种。这三者可以如下概括地总结:

Java:程序员必须明确声明他的意图,然后机器会检查它。假设程序员可能会犯一个语义错误(图形/手枪/卡)。

Go:程序员必须至少声明他的意图的一部分,但是在检查时机器的运行时间较少。假设程序员可能会犯一个文书错误(整数/字符串),但不太可能产生语义错误(图形/手枪/卡)。

鸭子打字:程序员不需要表达任何意图,机器也无需检查。假设程序员不太可能犯一个文书错误或语义错误。

解决接口和一般打字是否足以测试文书和语义错误超出了本答案的范围。完整的讨论必须考虑构建时编译器技术,自动化测试方法,运行时/热点编译以及许多其他问题。

人们承认draw(String)例子被故意夸大以说明问题。真实的例子将涉及更丰富的类型,这将提供更多线索来消除方法的歧义。

答案 1 :(得分:5)

  

为什么编译器不能推断JButton类“满足”接口声明(查看它内部)并接受转换?这是编译器效率的问题还是存在更多基本问题?

这是一个更基本的问题。

接口的要点是指定存在许多类支持的通用API /行为集。因此,当一个类声明为implements SomeInterface时,该类中符号与接口中的方法签名匹配的任何方法都假定是提供该行为的方法。

相比之下,如果语言简单地匹配基于签名的方法......无论接口如何......那么当具有相同签名的两个方法实际上意味着/做一些语义上不相关的事情时,我们就有可能得到错误的匹配。

(后一种方法的名称是“鸭子打字”...... Java不支持它。)


type systems上的维基百科页面说,鸭子打字既不是“主格打字”,也不是“结构打字”。相比之下,皮尔斯甚至没有提到“鸭子打字”,但他定义了主格(或称之为“名义”)打字和结构打字如下:

  

“输入像Java这样的系统,其中[类型]的名称是重要的,并且显式声明了子类型,称为 nominal 。类型系统就像本书中大多数名称不必要的系统一样和子类型直接在类型的结构上定义,称为结构。“

因此,根据皮尔斯的定义,鸭子打字是结构类型的一种形式,尽管通常使用运行时检查来实现。 (Pierce的定义与编译时与运行时检查无关。)

参考:

  • “类型和编程语言” - Benjamin C Pierce,麻省理工学院出版社,2002年,ISBN 0-26216209-1。

答案 2 :(得分:2)

我不能说我知道为什么某些设计决策是由Java开发团队做出的。我还要回答一下这样一个事实:这些人在软件开发和(特别是)语言设计方面远比我聪明得多。但是,这是试图回答你的问题的一个裂缝。

为了理解为什么他们可能没有选择使用像“CanAddActionListener”这样的界面,你必须看看NOT使用接口的优点,而是选择抽象(最终是具体的)类。

您可能知道,在声明抽象功能时,您可以为子类提供默认功能。好的......那又怎样?大不了吧?好吧,特别是在设计语言的情况下,这是一个大问题。在设计语言时,您需要在语言的整个生命周期内维护这些基类(并且您可以确定随着语言的发展会有变化)。如果您选择使用接口,而不是在抽象类中提供基本功能,则实现该接口的任何类都将中断。这在发布后尤其重要 - 一旦客户(本例中的开发人员)开始使用您的库,您无法随心所欲地更改接口,或者您会对开发人员产生很多愤怒!

所以,我的猜测是Java开发团队完全意识到他们的许多AbstractJ *类共享相同的方法名称,让它们共享一个公共接口是不利的,因为它会使他们的API变得僵硬和不灵活。

总结(thank you to this site here):

  • 通过添加新的(非抽象)方法可以轻松扩展抽象类。
  • 如果不破坏与实现它的类的合同,则无法修改接口。接口出厂后,其成员集将永久固定。基于接口的API只能通过添加新接口来扩展。

当然,这并不是说您可以在自己的代码中执行类似的操作(扩展AbstractJButton并实现CanAddActionListener接口),但要注意这样做的缺陷。

答案 3 :(得分:2)

可能它是一种性能特征。

由于Java是静态类型的,因此编译器可以断言类与已识别接口的一致性。验证后,该断言可以在编译类中表示为对符合接口定义的引用。

稍后,在运行时,当一个对象将其类强制转换为接口类型时,所有运行时需要检查的是类的元数据,以查看它所投射的类是否兼容(通过接口或继承层次结构)。

这是一个相当便宜的检查,因为编译器完成了大部分工作。

介意,它不具有权威性。类可以说它符合接口,但这并不意味着即将执行的实际方法实际上会起作用。符合要求的类可能已经过时,方法可能根本就不存在。

但是java性能的一个关键组成部分是,虽然它仍然必须在运行时实际执行一种动态方法调度,但是有一个合同,该方法不会在运行时间后突然消失。因此,一旦找到该方法,其位置可以在将来缓存。与动态语言相比,方法可能来来往往,并且每次调用方法时都必须继续尝试查找方法。显然,动态语言具有使其表现良好的机制。

现在,如果运行时通过完成所有工作来确定对象是否符合接口,那么您可以看到可能会花费多少,特别是对于大型接口。例如,JDBC ResultSet有超过140种方法,其中包括。

鸭子打字是有效的动态界面匹配。检查对象上调用的方法,并在运行时映射它。

所有这些信息都可以缓存,并在运行时构建等等。所有这些都可以(并且在其他语言中),但在编译时完成大部分工作实际上在运行时CPU上都非常有效和它的记忆。虽然我们将Java与多GB堆用于长时间运行的服务器,但它实际上非常适合小型部署和精简运行时。甚至在J2ME之外。因此,仍然有动力尝试尽可能地保持运行时足迹。

答案 4 :(得分:2)

由于斯蒂芬C讨论的原因,鸭子打字可能很危险,但并不一定是打破所有静态打字的邪恶。一个静态且更安全的鸭子打字版本位于Go类型系统的核心,Scala中有一个版本,称为“结构类型”。这些版本仍然执行编译时检查以确保对象服从需求,但是存在潜在的问题,因为它们破坏了实现接口的设计范例总是故意的决定。

有关Scala功能的详细信息,请参阅http://markthomas.info/blog/?p=66http://programming-scala.labs.oreilly.com/ch12.html以及http://beust.com/weblog/2008/02/11/structural-typing-vs-duck-typing/

答案 5 :(得分:0)

接口代表一种替代类。可以将实现或继承自特定接口的类型的引用传递给期望该接口类型的方法。接口通常不仅指定所有实现类必须具有具有特定名称和签名的方法,而且通常还具有关联的合同,该合同指定所有合法实现类必须具有具有特定名称的方法和签名,以某种指定的方式行事。完全有可能即使两个接口包含具有相同名称和签名的成员,实现也可以满足一个而不是另一个的合同。

作为一个简单的例子,如果一个人从头开始设计一个框架,可以从一个Enumerable<T>接口开始(可以根据需要经常使用它来创建一个输出T序列的枚举器,但是不同的请求可能会产生不同的序列),但是从它派生出一个接口ImmutableEnumerable<T>,其行为如上所述,但保证每个请求都返回相同的序列。可变集合类型将支持ImmutableEnumerable<T>所需的所有成员,但由于在突变之后收到的枚举请求将报告与之前发出的请求不同的序列,因此它不会遵守ImmutableEnumerable合同。

将接口视为封装超出其成员签名的契约的能力是使基于接口的编程在语义上比简单的鸭子类型更强大的能力之一。