Java基础教程(17)--接口
就像类一样,接口也是引用类型。它可以包含常量,方法签名,默认方法,静态方法和嵌套类型。在接口中,只有默认方法和静态方法有方法体。但不同于类,接口无法实例化,它只能被类实现或被其他接口继承。
一.定义接口
接口的定义由修饰符、关键字interface、接口名、extends关键字(接口可以继承并且支持多继承)和若干继承的父接口(如果有的话)以及接口体组成,语法如下(方括号表示可选内容):
public interface DoIt [extends Interface1, Interface2, ...] [throws Exception1, Exception2, ...] {
void doSomething (int i, double x);
int doSomethingElse(String s);
}
public修饰符代表这个接口可以在任何地方使用。如果没有指定接口是public的,那么这个接口只能在定义这个接口的包中使用。
一个接口可以继承其他接口,就像一个类可以继承另外一个类一样。不同的是,一个类只能继承一个父类,而一个接口可以继承多个接口。
接口体包含了若干个抽象方法,从Java8之后,接口中还可以包含默认方法和静态方法。抽象方法后直接跟分号,没有大括号,因为抽象方法不包含实现。默认方法使用default关键字定义,静态方法使用static关键字定义。接口中的所有方法都默认且必须是public的,因此可以省略方法前的public。
此外,接口可以包含常量声明。在接口中定义的所有常量都默认是public,static和final的,因此可以省略这些修饰符。
二.实现接口
要实现接口,需要在定义类时在类名后面使用implements关键字,然后跟着以逗号分隔的要实现的接口列表。按照惯例,如果类既继承了父类,又实现了接口,implements语句应该放在extends语句后面。
下面定义了一个USB接口:
public interface USB {
void read();
void write();
}
如果一个类实现了某个接口,则它必须实现这个接口中所有的抽象方法。假设现在我们要定义一个U盘类,它需要实现USB接口,那么它必须实现USB接口中定义的read和write方法:
public class UDisk implements USB {
@Override
public void read() {
System.out.println("Reading UDisk...");
}
public void write() {
System.out.println("Writing UDisk...");
}
}
当定义了一个新的接口时,实际上就是定义了一个新的引用数据类型。如果定义了一个接口类型的变量,则这个变量只能引用实现了这个接口的类的实例。例如:
USB usb = new UDisk();
若接口的抽象方法中包含异常,那么在实现这个抽象方法时可以抛出异常列表中的全部或部分异常,以及这些异常的子类,甚至不抛出异常。有关异常的知识将会在后面的文章中进行介绍。
三.接口中的静态方法
和类一样,接口也可以有静态方法。不同的是,在类中,既可以通过定义静态方法的类及其子类调用静态方法,也可以通过该类和子类的实例调用静态方法(可以但不推荐);在接口中,则只能通过定义静态方法的接口去调用静态方法,而不能通过实现它的类及其实例调用静态方法。接口的静态方法仅对于接口本身可见。例如下面的接口:
public interface Bar {
static void foo() {
System.out.println("I'm a static method.");
}
}
可以通过Bar.foo()的形式调用这个静态方法。假设有个BarImpl实现了这个接口,那么无论是BarImpl.foo()或new BarImpl().foo()都会产生编译错误,因为这个静态方法对它们来说是不可见的。
四.默认方法
从Java8之后,可以为接口中的方法提供一个默认实现。使用关键字default来表示这是一个默认方法。例如:
public interface Comparable<T> {
default int compareTo(T other) {
return 0;
}
}
当然,这里的默认方法并没有太大用处,因为Comparable的每一个实现类都要覆盖这个方法。不过有些情况下,默认方法可能很有用。如果希望在发生鼠标点击事件时得到通知,就要实现一个包含5个方法的接口:
public interface MouseListener {
void mouseClicked(MouseEvent event);
void mousePressed(MouseEvent event);
void mouseReleaseed(MouseEvent event);
void mouseEntered(MouseEvent event);
void mouseExited(MouseEvent event);
}
大多数情况下,我们只关心其中的一两个事件。可以将所有方法声明为默认方法,这些默认方法什么也不做:
public interface MouseListener {
default void mouseClicked(MouseEvent event) {}
default void mousePressed(MouseEvent event) {}
default void mouseReleaseed(MouseEvent event) {}
default void mouseEntered(MouseEvent event) {}
default void mouseExited(MouseEvent event) {}
}
这样一来,实现这个接口时,只需要覆盖真正关心的事件方法就可以了。
默认方法可以调用其他的默认方法、静态方法和抽象方法。例如,Collection接口可以定义一个便利方法:
public interface Collection {
int size();
default boolean isEmpty() {
return size() == 0;
}
}
这样在实现Collection接口时就不用操心isEmpty方法了。
默认方法的一个重要用法是“接口演化”。以Collection接口为例,这个接口作为Java的一部分已经很多年了。后来,在Java8中,又为这个接口增加了一个stream方法。假设stream不是一个默认方法,那么其他已经实现了Collection接口的类将不能编译,因为要实现接口就必须实现接口所有的抽象方法。这样一来,就要修改其他所有已经实现了Collection接口的类的代码,这显然不是我们愿意看到的。
不过,如果我们为stream方法提供默认实现,那么问题就迎刃而解了。其他已经实现了Collection接口的类不用做任何修改,就可以通过编译,而且还可以在这些类的实例上调用stream方法。如果真的有部分类需要重新定义stream方法而不是使用默认的stream方法,那么工作量也会小很多。
下面来讨论默认方法的继承问题。当一个接口继承一个包含默认方法的接口时,可以执行以下操作:
- 不做任何修改,完全继承父接口的默认方法;
- 重新声明这个默认方法,使它变成抽象方法;
- 重写这个默认方法。
执行第1种和第3种操作时,接口的实现类既可以选择使用默认的实现,也可以覆盖这个默认的方法。但执行第2种操作时,则必须实现这个抽象方法。