接口与类相比
由编译器强制的一个模块间协作的合约(Contract):
接口是一个由编译器强制的模块间协作的合约。它定义了一组方法的契约,所有实现该接口的类都必须提供这些方法的具体实现。这种强制性保证了不同模块之间的协作方式的一致性和可靠性。举例来说,Java 中的 List
接口定义了一组列表操作的方法,如 add
、get
、remove
等,所有实现了 List
接口的类都必须提供这些方法的具体实现。
无成员变量:
接口不能包含任何成员变量,它只能包含方法的声明。这是因为接口的目的是定义一组方法的契约,而不关心具体的实现和状态。因此,接口中只能声明方法,而不能包含任何与状态相关的成员变量。相比之下,类可以包含成员变量,因为类除了定义方法外,还可以包含状态和行为的具体实现。
成员函数只有申明不能有实现:
接口中的方法只能有声明而不能有具体的实现。这是因为接口是一种纯粹的抽象,它只定义了一组方法的契约,而不关心具体的实现。因此,接口中的方法只能有方法的签名,而不能包含方法的具体实现。相比之下,类中的方法可以有具体的实现,因为类除了定义方法签名外,还可以提供方法的具体实现。
接口的声明
静态语言角度(Java、TypeScript)依赖关键字:
在静态类型语言中,如Java和TypeScript,接口的声明通过关键字来实现。具体来说:
- Java 中使用
interface
关键字声明接口,然后在类中通过implements
关键字实现接口。 - TypeScript 中也使用
interface
关键字声明接口,然后可以通过implements
关键字或者直接在类中定义相同的结构来实现接口。
通过关键字的方式,编译器可以在编译时检查类是否实现了接口中定义的所有方法,以及方法的参数和返回值类型是否匹配。
动态语言角度(JavaScript、Python)依赖注释文档约束:
在动态类型语言中,如JavaScript和Python,接口的约束主要依赖于注释文档和约定。具体来说:
- JavaScript 中通常使用 JSDoc 注释来描述接口和方法的结构和用法,然后通过开发者的遵循来保证类的实现符合接口的契约。
- Python 中没有内置的接口机制,但通常通过注释文档或者约定来定义接口,然后由开发者自觉遵守这些约定。
在动态语言中,接口的实现没有严格的语法规定,更多地依赖于开发者的自觉和约定。虽然没有编译器在编译时检查,但可以通过代码审查和单元测试等方式来确保类的实现符合接口的要求。
Go 用鸭子类型实现类时接口功能:
Go 语言中没有传统意义上的接口,但通过鸭子类型(Duck Typing)来实现接口的功能。鸭子类型是一种动态类型系统中的一种策略,它关注对象的行为而不是对象的类型。
在 Go 中,一个类型只要实现了某个接口所定义的所有方法,就可以被视为实现了该接口。这种方式类似于动态语言中的接口约定,但更加灵活,没有明确的接口声明和实现关系。
通过鸭子类型,Go 语言可以实现接口的功能,但在语法上与静态类型语言中的接口有所不同。
面试题:接口和抽象类的不同
抽象类可以有成员变量:
在 Java 中,抽象类可以包含成员变量,而接口不能包含任何成员变量。这意味着抽象类可以包含具体的状态和行为,而接口只能包含方法的声明。举例来说,AbstractList
类就包含了一些成员变量来管理列表的状态,比如 modCount
和 size
。
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
protected transient int modCount = 0;
protected int size;
}
抽象类可以有部分实现:
抽象类可以有部分方法的实现,而接口中的方法都是抽象的,没有具体的实现。这意味着在抽象类中,可以将一些通用的方法实现在抽象类中,而在子类中实现特定的行为。举例来说,AbstractList
类中实现了 toString()
方法和 isEmpty()
方法。
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
public boolean isEmpty() {
return size() == 0;
}
}
抽象类不可以多重继承:
Java 中,抽象类不支持多重继承,一个类只能继承一个抽象类。但是,一个类可以实现多个接口。这意味着如果一个类已经继承了一个抽象类,就无法再继承其他抽象类了,但可以实现多个接口。举例来说,ArrayList
类继承了 AbstractList
抽象类,但同时实现了 List
接口。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
}
总的来说,接口和抽象类在 Java 中有着不同的使用场景和特性,需要根据具体的需求来选择合适的方式。
接口的多重实现举例
为什么有抽象类了还要有接口这个概念
从用户角度看问题:
接口对于用户来说是一个约定或者合约,定义了一组方法的契约,用户可以通过接口来了解一个类提供了哪些功能,而不需要关心具体的实现细节。这种抽象的设计使得用户可以更加专注于如何使用接口提供的功能,而不需要了解背后的具体实现。
强调合约:
接口强调了类与类之间的契约或者约定。通过接口,类之间建立了一种规范化的交流方式,类的设计者向用户承诺了一组特定的行为和功能。这种明确的约定使得不同类之间可以更加灵活地协作,而无需关心具体的实现细节。
强制协作双方无法犯错:
接口强制了类的实现者必须遵循接口定义的契约,提供接口中定义的所有方法的具体实现。这样一来,无论是类的设计者还是类的用户,在使用接口时都可以放心,不会因为接口的实现方式不同而出现错误。接口的存在使得类之间的协作更加可靠,避免了潜在的错误和bug。
通过以上三个角度的解读,可以看出接口在面向对象编程中的重要性。它不仅可以帮助用户更好地理解和使用类的功能,还可以建立类之间的规范化交流方式,提高代码的可靠性和可维护性。
扩展阅读
面向对象主题 | 链接 |
---|---|
类与对象 | 链接 |
接口与抽象类 | 链接 |
不可变性 | 链接 |