老问题。我很惊讶没有人引用规范的资料:Java: an Overview by James Gosling,Design Patterns: Elements of Reusable Object-Oriented Software by the Gang of Four 或 Effective Java 作者 Joshua Bloch(以及其他来源)。
我将从引用开始:
接口只是对象响应的一组方法的规范。它不包括任何实例变量或实现。接口可以被多重继承(与类不同),并且它们可以以比通常的刚性类更灵活的方式使用
继承结构。 (高斯林,第 8 页)
现在,让我们一一提出您的假设和问题(我将自愿忽略 Java 8 的特性)。
假设
接口是仅抽象方法和最终字段的集合。
您在 Java 接口中看到关键字abstract 了吗?不,那么您不应该将接口视为抽象方法的集合。也许你被 C++ 所谓的接口误导了,这些接口是只有纯虚方法的类。 C++ 在设计上没有(也不需要)接口,因为它具有多重继承。
正如 Gosling 所解释的,您应该将接口视为“对象响应的一组方法”。我喜欢将界面和相关文档视为服务合同。它描述了您可以从实现该接口的对象中获得什么。文档应指定前置条件和后置条件(例如,参数不应为空,输出始终为正,...)和不变量(不修改对象内部状态的方法)。我认为,这份合同是 OOP 的核心。
Java 中没有多重继承。
确实。
JAVA 省略了 C++ 中许多很少使用、难以理解、令人困惑的特性,根据我们的经验,这些特性带来的痛苦多于好处。这主要包括运算符重载(尽管它确实有方法重载)、多重继承和广泛的自动强制。 (高斯林,第 2 页)
没什么可补充的。
接口可用于在Java中实现多重继承。
不,simlpy,因为 Java 中没有多重继承。见上文。
继承的一个优点是我们可以在派生类中使用基类的代码,而无需重新编写。可能这是继承存在的最重要的事情。
这就是所谓的“实现继承”。正如您所写,这是一种重用代码的便捷方式。
但它有一个重要的对应物:
父类通常至少定义其子类的部分物理表示。因为继承将子类暴露给其父类的实现细节,所以人们常说“继承破坏了封装”[Sny86]。子类的实现与其父类的实现如此紧密地联系在一起,以至于父类实现的任何改变都会迫使子类改变。 (GOF,1.6)
(在 Bloch 第 16 条中有类似的引用。)
其实继承还有另一个目的:
类继承结合了接口继承和实现继承。接口继承定义了一个新的接口
或更多现有接口。实现继承根据一个或多个现有实现定义了一个新的实现。 (GOF,附录 A)
两者都在 Java 中使用关键字 extends。您可能有类的层次结构和接口的层次结构。第一个分担实施,第二个分担义务。
问题
Q1。由于接口只有抽象方法(没有代码),所以我们怎么能说如果我们正在实现任何接口,那么它就是继承?我们没有使用它的代码。**
接口的实现不是继承。是执行。因此关键字implements。
Q2。如果实现一个接口不是继承,那么接口是如何实现多重继承的呢?**
Java 中没有多重继承。见上文。
Q3。无论如何,使用接口有什么好处?他们没有任何代码。我们需要在我们实现它的所有类中一次又一次地编写代码。/那为什么要制作接口?/使用接口的确切好处是什么?我们使用接口实现的真的是多重继承吗?
最重要的问题是:你为什么想要多重继承?我可以想到两个答案:1.给一个对象多个类型; 2. 重用代码。
为一个对象赋予多种类型
在 OOP 中,一个对象可能有不同的类型。例如在 Java 中,ArrayList<E> 具有以下类型:Serializable、Cloneable、Iterable<E>、Collection<E>、List<E>、RandomAccess、AbstractList<E>、AbstractCollection<E> 和 @9876543我希望我没有忘记任何人)。如果一个对象有不同的类型,那么不同的消费者将能够在不知道它的特殊性的情况下使用它。我需要一个Iterable<E> 而你给我一个ArrayList<E>?没关系。但如果我现在需要List<E> 而你给我ArrayList<E>,也可以。等等。
如何在 OOP 中键入对象?你以Runnable接口为例,这个例子完美的说明了这个问题的答案。我引用官方 Java 文档:
此外,Runnable 提供了使类处于活动状态而不是子类化 Thread 的方法。
重点是:继承是键入对象的便捷方式。您想创建一个线程吗?让我们继承Thread 类。您希望一个对象具有不同的类型,让我们使用多重继承。啊。它在 Java 中不存在。 (在 C++ 中,如果你想让一个对象有不同的类型,多继承是要走的路。)
那么如何为一个对象赋予多种类型呢?在Java 中,您可以直接 键入您的对象。这就是当你的类implementsRunnable 接口时你所做的。如果您喜欢继承,为什么要使用Runnable?也许是因为你的类已经是另一个类的子类,比如说A。现在你的班级有两种类型:A 和 Runnable。
通过多个接口,您可以为一个对象赋予多种类型。你只需要创建一个implements 多个接口的类。只要您遵守合同,就可以。
重用代码
这是一个困难的主题;我已经引用了 GOF 关于打破封装的内容。其他答案提到了钻石问题。你也可以想到单一职责原则:
一个班级应该只有一个改变的理由。 (Robert C. Martin,敏捷软件开发、原则、模式和实践)
拥有一个父类可能会给一个类一个改变的理由,除了它自己的责任:
超类的实现可能会随着版本的变化而变化,如果发生这种情况,子类可能会中断,即使它的代码没有被触及。因此,子类必须与其超类同步发展(Bloch,第 16 条)。
我要补充一个更平淡的问题:当我试图在一个类中找到一个方法的源代码时,我总是有一种奇怪的感觉,但我找不到它。然后我记得:它必须在父类的某个地方定义。或者在祖父母班。或者甚至更高。在这种情况下,一个好的 IDE 是一项宝贵的资产,但在我看来,它仍然是一种神奇的东西。与接口层次结构没有什么相似之处,因为 javadoc 是我唯一需要的东西:IDE 中的一个键盘快捷键,我就明白了。
继承方式有优势:
在包中使用继承是安全的,其中子类和超类的实现都在同一个程序员的控制之下。在扩展专门为扩展设计和记录的类时,使用继承也是安全的(第 17 条:为继承设计和记录,否则禁止它)。 (布洛赫,第 16 条)
AbstractList 是 Java 中“专门为扩展而设计和记录的”类的一个示例。
但是 Bloch 和 GOF 坚持这一点:“偏好组合胜于继承”:
委托是一种让组合像继承一样强大的复用方式 [Lie86, JZ91]。在委托中,处理请求涉及两个对象:接收对象将操作委托给它的委托。这类似于子类将请求推迟到父类。 (GOF 第 32 页)
如果您使用组合,则不必一次又一次地编写相同的代码。您只需创建一个处理重复的类,然后将此类的一个实例传递给这些类实现接口。这是重用代码的一种非常简单的方法。这有助于您遵循单一职责原则并使代码更具可测试性。 Rust 和 Go 没有继承(它们也没有类),但我不认为代码比其他 OOP 语言更冗余。
此外,如果您使用组合,您会发现自己很自然地使用接口来为您的代码提供所需的结构和灵活性(请参阅有关接口用例的其他答案)。
注意:您可以使用 Java 8 接口共享代码
最后,最后一句:
在令人难忘的问答环节中,有人问他 [James Gosling]:“如果你可以重做 Java,你会改变什么?” “我会遗漏课程”(网络上的任何地方,不知道这是不是真的)