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

| 有时,我们有几个类,这些类的某些方法具有相同的签名,但是与声明的Java接口不对应。例如,
JTextField
JButton
(在ѭ2several中的其他几个)中都有一个方法
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
不是
CanAddActionListener
,因为该类尚未声明实现该接口……但是,它“实际上”实现了该接口。 有时这很不方便-Java本身已经修改了几个核心类来实现由旧方法(例如,ѭ9)组成的新接口。 我的问题是:为什么会这样呢?我了解声明类实现接口的实用程序。但是无论如何,以我的示例为例,为什么编译器不能推断出类
JButton
\“ satisfies \”接口声明(在其中查找)并接受强制转换?是编译器效率问题还是更多基本问题? 我对答案的总结:在这种情况下,Java可能允许某些“结构类型”(某种鸭子类型-但在编译时检查了)。没有。除了一些(对我来说尚不清楚)性能和实现方面的困难之外,这里还有一个更基本的概念:在Java中,接口(以及所有事物的声明)的声明并不仅仅意味着结构化(具有这些签名),但在语义上:方法应该实现某些特定的行为/意图。因此,在结构上满足某些接口的类(即,它具有带有所需签名的方法)不一定在语义上满足它(一个极端的示例:回想一下甚至没有方法的““ marker interfaces \”)。因此,Java可以断言一个类实现了一个接口,因为(并且仅因为)该接口已被明确声明。其他语言(Go,Scala)也有其他哲学。     
已邀请:
Java的使设计类明确声明其实现的接口的设计选择就是设计选择。可以肯定的是,JVM已经针对此选择进行了优化,并且除非添加了一些新的JVM指令,否则现在可​​能要付出额外的代价来实现另一个选择(例如Scala的结构化类型)。 那么设计选择到底是什么呢?一切都归结为方法的语义。考虑:以下方法在语义上是否相同? draw(String graphicShapeName) draw(String handgunName) draw(String playingCardName) 这三种方法都具有签名“ 11”。一个人可能会推断出他们与参数名称有不同的语义,或者通过阅读一些文档来推断。机器有什么方法可以告诉他们它们不同吗? Java的设计选择是要求类的开发人员明确声明方法符合预定义接口的语义:
interface GraphicalDisplay {
    ...
    void draw(String graphicalShapeName);
    ...
}

class JavascriptCanvas implements GraphicalDisplay {
    ...
    public void draw(String shape);
    ...
}
毫无疑问,
JavascriptCanvas
中的
draw
方法旨在与图形显示中的
draw
方法匹配。如果有人试图通过将要拔出手枪的物体,则机器可以检测到错误。 Go \的设计选择更为自由,并允许在事实之后定义接口。具体的类无需声明其实现的接口。而是,新纸牌游戏组件的设计者可以声明提供纸牌的对象必须具有与签名“ 11”匹配的方法。这样做的好处是,可以使用具有该方法的任何现有类,而无需更改其源代码,但是缺点是该类可能会拔出手枪而不是纸牌。 鸭式语言的设计选择是完全放弃形式化接口,并简单地匹配方法签名。接口的任何概念(或“协议”)都是纯惯用语,没有直接的语言支持。 这些只是许多可能的设计选择中的三个。这三个可以概括如下: Java:程序员必须明确声明他的意图,然后机器会对其进行检查。假设程序员可能会犯语义错误(图形/手枪/卡)。 继续:程序员必须声明其意图的至少一部分,但是检查机器时机器可以继续运行。假设程序员可能会犯一个笔误(整数/字符串),而不会犯语义上的错误(图形/手枪/卡)。 鸭式打字:程序员无需表达任何意图,机器无需检查。假定程序员不太可能犯笔误或语义错误。 解决接口和一般类型是否足以测试笔误和语义错误超出了此答案的范围。完整的讨论将不得不考虑构建时编译器技术,自动化测试方法,运行时/热点编译以及许多其他问题。 公认的是,故意夸大了“ 11”的例子。实际的示例将涉及更丰富的类型,这些类型将提供更多线索来消除方法的歧义。     
为什么编译器不能推断类JButton“满足”接口声明(在内部查看)并接受强制转换?是编译器效率问题还是更多基本问题? 这是一个更根本的问题。 接口的重点是指定存在许多类支持的通用API /一组行为。因此,当一个类声明为“ 18”时,该类中其签名与接口中的方法签名匹配的任何方法都被认为是提供该行为的方法。 相比之下,如果该语言只是简单地基于签名匹配方法……而与接口无关……那么当两个具有相同签名的方法实际上意味着/做一些与语义无关的事情时,我们很可能会得到错误的匹配。 (后一种方法的名称是“鸭子键入”……而Java不支持它。) 在类型系统的Wikipedia页面上,鸭子类型既不是\“名词性键入\”也不是\“结构性键入\”。相比之下,皮尔斯甚至没有提到“鸭式打字”,但他定义了主语(或称“名词”)和结构式打字,如下所示:   \“ Java之类的类型系统,其中[类型]的名称是有效的,并且显式声明了子类型,被称为标称系统。像本书中大多数类型系统一样,其中名称是非必需的并且子类型是直接在其上定义的类型系统类型的结构称为结构。” 因此,按照Pierce的定义,鸭子类型是结构类型的一种形式,尽管通常使用运行时检查来实现。 (Pierce的定义独立于编译时还是运行时检查。) 参考: “类型和编程语言”-本杰明·皮尔斯(Benjamin C Pierce),麻省理工学院出版社,2002年,ISBN 0-26216209-1。     
这可能是一项性能功能。 由于Java是静态类型的,因此编译器可以断言类与已标识接口的一致性。验证后,该断言可以在编译的类中表示为对符合接口定义的引用。 稍后,在运行时,将对象的Class强制转换为接口类型时,运行时所需要做的就是检查该类的元数据,以查看是否也强制转换了该类(通过接口或继承层次结构)。 由于编译器已经完成了大部分工作,因此这是一项相当便宜的检查。 请注意,这不是权威。一个类可以说它符合接口,但这并不意味着要执行的实际方法可以实际工作。符合条件的类可能已过时,并且该方法可能根本不存在。 但是,Java性能的一个关键组成部分是,尽管它仍然必须在运行时实际执行某种形式的动态方法分派,但是有一个约定,即该方法不会在运行时后面突然消失。因此,一旦找到方法,就可以在以后缓存其位置。与动态语言相反,动态语言中的方法可能会来来去去,并且每次调用方法时,它们都必须继续尝试并寻找各种方法。显然,动态语言具有使之良好运行的机制。 现在,如果运行时要通过自己完成所有工作来确定对象符合接口,则可以看到昂贵得多,尤其是对于大型接口。例如,JDBC ResultSet具有140多种方法。 鸭子类型有效地是动态接口匹配。检查在对象上调用了哪些方法,并在运行时将其映射。 所有这些信息都可以在运行时进行缓存和构建,等等。所有这些信息都可以(并且使用其他语言),但是在编译时完成许多操作实际上对运行时CPU及其内存都非常有效。 。虽然我们将Java与具有多个GB堆的Java用于运行时间较长的服务器,但实际上非常适合小型部署和精益运行时。即使在J2ME之外。因此,仍然有动机尝试使运行时占用的内存尽可能少。     
由于斯蒂芬·C(Stephen C)讨论的原因,鸭子打字可能很危险,但是打破所有静态打字的方法不一定是邪恶的。 Go类型系统的核心是静态的,更安全的鸭子类型版本,Scala中提供了一个称为“结构类型”的版本。这些版本仍会执行编译时检查以确保该对象符合要求,但是存在潜在的问题,因为它们破坏了实现接口的设计范式始终是一个有意的决定。 参见http://markthomas.info/blog/?p=66和http://programming-scala.labs.oreilly.com/ch12.html和http://beust.com/weblog/2008/02/11/关于Scala功能的讨论。     
我不能说我知道为什么Java开发团队会做出某些设计决策。我也要回答我一个事实,那就是这些人在软件开发和(特别是)语言设计方面比我以前聪明得多。但是,这很难回答您的问题。 为了理解为什么他们可能没有选择使用\“ CanAddActionListener \”这样的接口,您必须查看不使用接口的优点,而是首选抽象(最终是具体的)类。 如您所知,在声明抽象功能时,可以为子类提供默认功能。好吧...那又怎样?没关系吧?好吧,特别是在设计语言的情况下,这很重要。在设计语言时,您将需要在语言的整个生命周期中维护这些基类(并且可以确保随着语言的发展而有所变化)。如果您选择使用接口,而不是在抽象类中提供基本功能,则实现该接口的任何类都会中断。这在发布后特别重要-一旦客户(在这种情况下为开发人员)开始使用您的库,就无法一时兴起地更改接口,否则您会生气很多开发人员! 因此,我的猜测是Java开发团队完全意识到他们的许多AbstractJ *类共享相同的方法名称,因为它们使API僵化和僵化,所以共享它们的公共接口并不会带来任何好处。 总结一下(感谢您在此访问此站点): 通过添加新的(非抽象)方法,可以轻松扩展抽象类。 在不破坏接口与实现接口的类的约定的情况下,不能对其进行修改。接口出厂后,其成员集将永久固定。基于接口的API只能通过添加新接口来扩展。 当然,这并不是说您可以在自己的代码中做类似的事情(扩展AbstractJButton并实现CanAddActionListener接口),但是要意识到这样做的陷阱。     
接口表示替代类的一种形式。实现或从特定接口继承的类型的引用可以传递给需要该接口类型的方法。接口通常不仅会指定所有实现类必须具有带有特定名称和签名的方法,而且通常还将具有关联的协定,该合约规定所有合法的实现类必须具有具有特定名称和签名的方法,这些方法在某些指定的行为下运行。方法。即使两个接口包含具有相同名称和签名的成员也是完全有可能的,但实现可能满足一个协议的约定,而不满足另一个约定。 举一个简单的例子,如果一个人从头开始设计一个框架,可能会以一个
Enumerable<T>
接口开始(可以根据需要经常使用它来创建一个枚举器,该枚举器将输出一系列T \,但是可能会有不同的请求产生不同的序列),然后从中派生出接口
ImmutableEnumerable<T>
,该接口的行为如上,但保证每个请求都返回相同的序列。可变的收集类型可以支持
ImmutableEnumerable<T>
所需的所有成员,但是由于突变后收到的枚举请求将报告与之前提出的请求不同的顺序,因此它不遵守
ImmutableEnumerable
合同。 接口被视为封装了超出其成员签名范围的合同的能力是使基于接口的编程在语义上比简单的鸭子键入更强大的功能之一。     

要回复问题请先登录注册