接口
1. 概念引入
抽象类是从多个类中抽象出来的模板,如果将这种抽象更进一步,则可以提炼出一种更加特殊的“抽象类”—— 接口(interface) 。
接口定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这也意味着接口里通常是定义一组公用方法。
接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。基于这个原则,很多软件架构设计理论都倡导“面向接口编程”,而不是面向实现类编程。面向接口编程的常用场景可参照“简单工厂模式”和“命令模式”。
继承一个类,既是设计的重用,又是代码的重用;实现一个接口,纯粹是设计的重用。
2. 语法
定义接口不再是使用class
关键字,而是使用interface
关键字:
[public] interface 接口名 [extends 父接口1 父接口2 …]
{
0~N个静态常量定义…
0~N个抽象方法定义…
0~N个类方法、默认方法或私有方法定义…
0~N个内部类、内部接口、内部枚举定义…
}
- 修饰符可以是
public
或省略,若省略,则默认采用包权限访问控制符(default
级别); - 接口名的命名像类名那样命名即可,不过接口名通常也使用形容词;
- 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
有的接口只定义常量,而没有定义方法,例如标准库中的SwingConstants
接口。不过,这样使用接口更像是退化,所以建议最好不要这样使用。
3. 接口的特性
3.1 综述
由于接口定义的是一种公共的抽象行为规范,这种设计哲学意味着:
- 接口里的所有成员全部是
public
权限; - 接口里不能包含构造器和初始化块;
- 接口里的成员字段只能是静态常量,即接口里的字段都自动是
public static final
修饰的; - 接口中的定义的内部类、内部接口、内部枚举都是
public static
的; - 接口中的普通方法(只能是实例方法)也是隐式抽象的,即它们自动是
public abstract
的,不能有方法体;
然而,特殊情况是,为了更好地服务接口的内涵,除了上述的抽象方法,接口中的方法还可以定义为类方法、默认方法、私有方法,这些方法必须有方法体,故而它们不会隐式地被abstract
修饰。
- 类方法:必须使用
static
修饰,毫无疑问,加上隐式的public
,它的修饰符是public static
的。 - 默认方法:必须使用
default
修饰,default
方法不能使用static
修饰,综合而言是public default
的。 - 私有方法:Java 9引入,必须使用
private
修饰,作用是为接口中的默认方法或类方法提供支持,其不能使用default
修饰,但可以用static
修饰,即私有方法既可以是类方法又可以是实例方法。
在Java 8允许接口中定义静态方法之前,通常的做法是将静态方法放在伴随类中,如标准库中成对出现的接口和实用工具类Collection/Collections, Path/Paths
。而现在,实现你自己的接口时,另一种做法是在接口中直接定义相关的静态工具方法,而不必要再提供一个伴随类。
一个接口示例:
package lee;
public interface Output {
// 接口里的成员变量只能是常量
int MAX_CACHE_LINE = 50;
// 接口里的普通方法只能是public的抽象方法
void out();
void getData(String msg);
// 在接口中定义默认方法,需要使用default修饰
default void print(String... msgs) {
for (String msg : msgs) {
System.out.println(msg);
}
}
// 在接口中定义默认方法,需要使用default修饰
default void test() {
System.out.println("默认的test()方法");
}
// 在接口中定义类方法,需要使用static修饰
static String staticTest() {
return "接口里的类方法";
}
// 定义私有方法
private void foo() {
System.out.println("foo私有方法");
}
// 定义私有静态方法
private static void bar() {
System.out.println("bar私有静态方法");
}
}
3.2 默认方法
接口的默认方法其实就是实例方法,只是由于早期Java的设计是“接口中的实例方法不能有方法体”,Java 8也不能直接推倒以前的规则,故而只好重新定义了一个所谓的“默认方法”,说白了,默认方法就是有方法体的实例方法。
有些情况下,默认方法很有用。如Iterator
接口中,声明了一个remove
方法,其定义如下:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
默认方法的一个重要用法是“接口演化”。
默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样签名的方法,此时子类面临了方法冲突。Java 规定的解决规则如下:
超类优先:如果超类提供了一个具体方法,相同函数签名的默认方法会被忽略;
接口冲突:如果一个接口提供了一个默认方法,另一个接口只要有一个方法的签名与其相同(无论是抽象方法还是默认方法),则子类中必须覆盖这个方法来解决冲突,否则编译不通过。当然,在覆盖时可以选择直接调用某一个父接口中的方法:
interface Person { default String getName() { return ""; } } interface Named { default String getName() { return getClass().getName() + "_" + hashCode(); } } class Student implements Person, Named { public String getName() { return Person.super.getName(); } }
3.3 接口的继承
接口的继承和类不一样,接口完全支持多继承。和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量。
interface InterfaceA {
int PROP_A = 5;
void testA();
}
interface InterfaceB {
int PROP_B = 6;
void testB();
}
interface InterfaceC extends InterfaceA, InterfaceB {
int PROP_C = 7;
void testC();
}
public class InterfaceExtendsTest {
public static void main(String[] args) {
System.out.println(InterfaceC.PROP_A);
System.out.println(InterfaceC.PROP_B);
System.out.println(InterfaceC.PROP_C);
}
}
3.4 使用接口
接口不能用于创建实例,但接口可以用于声明引用类型变量,此时这个引用类型变量必须引用到其实现类的对象;除此之外,接口的主要用途就是被实现类实现。
一个类可以实现一个或多个接口,其中继承使用extends
关键字,实现使用implements
关键字,这也是 Java 为单继承灵活性不足所做的补充:
[修饰符] class 类名 extends 父类 implements 接口1, 接口2…
{
// 类体部分
}
一个类实现了一个或多个接口后,这个类必须完全实现这些接口里所定义的全部抽象方法;否则,这个类将保留从父接口那里继承到的抽象方法,导致该类也必须定义成抽象类。
实现接口方法时,必须使用public
访问控制修饰符,因为接口里的方法都是public
的,而子类(实现类)重写父类方法时访问权限只能更大或者相等,所以实现类实现接口里的方法时只能使用public
访问权限。
接口不能显式继承任何类,但所有接口类型的引用变量都可以直接赋给Object
类型的引用变量。
4. 接口和抽象类的比较
- 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承;
- 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法;
- 接口和抽象类的差别主要体现在二者的设计目的上:接口体现的是一种规范,抽象类体现的是一种模板式设计。
- 以及一些用法上的差别……