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

leo*_*loy 8 java duck-typing static-typing interface structural-typing

有时我们有几个类具有相同签名的某些方法,但是它们与声明的Java接口不对应.例如,两者JTextFieldJButton(在其他几个中 javax.swing.*)都有一种方法

public void addActionListener(ActionListener l)
Run Code Online (Sandbox Code Playgroud)

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

  public interface CanAddActionListener {
      public void addActionListener(ActionListener l);
  }
Run Code Online (Sandbox Code Playgroud)

所以我可以写:

  public void myMethod(CanAddActionListener aaa, ActionListener li) {
         aaa.addActionListener(li);
         ....
Run Code Online (Sandbox Code Playgroud)

但是,遗憾的是,我不能:

     JButton button;
     ActionListener li;
     ...
     this.myMethod((CanAddActionListener)button,li);
Run Code Online (Sandbox Code Playgroud)

这个演员是非法的.编译器知道JButton 不是 a CanAddActionListener,因为类没有声明实现该接口...... 但是它"实际"实现了它.

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

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

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

WRe*_*ach 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);
    ...
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

这些只是许多可能的设计选择中的三个.这三个可以如下概括地总结:

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

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

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

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

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


Ste*_*n C 5

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

这是一个更基本的问题.

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

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

(后一种方法的名称是"duck typing"...... Java不支持它.)


关于类型系统的维基百科页面说鸭子打字既不是"主格打字",也不是"结构打字".相比之下,皮尔斯甚至没有提到"鸭子打字",但他定义了主格(或称之为"名义上的")打字和结构打字如下:

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

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

参考:

  • "类型和编程语言" - Benjamin C Pierce,MIT Press,2002,ISBN 0-26216209-1.