定义
Interfaces, is a way of describing what classes should do, without specifying how they should do it.
和大多数有接口这项技术的语言里面一样,接口只负责定义行为(方法),具体行为的实现由类完成。
另外要说明的就是Java 8(Java SE 8/JDK 1.8)引入的函数式接口(functional interface):只包含一个抽象方法的接口。这里要注意有两个条件:只包含一个,且是抽象的方法(所以只限定了抽象方法,可以包含多个非抽象方法)。函数式接口可以选择性使用@FunctionalInterface
注解修饰,如果使用了该注解,但接口又不符合条件,编译的时候编译器就会报错。函数式接口主要是配合lambda表达式使用的(以后再讨论)。
一些特性
- 实现一个接口使用
implements
关键字;实现接口的类必须实现接口声明的所有抽象方法(当然如果是抽象类的话,可以只定义不实现)。 不能new一个接口实例;但可以声明一个接口类型的变量;接口类型的变量只能引用实现了该接口的类的对象。我们可以使用
instanceof
关键字检查一个对象是否实现了某个接口。// 错误,List是一个接口,不能new一个接口实例 X = new List<String>; // OK,可以声明一个接口类型的变量 List<String> stringList; // OK,ArrayList实现了List接口 List<String> strList = new ArrayList<>(1); // 错误,HashMap没有实现List接口 List<String> strList = new HashMap<String, String>(); // 使用instanceof关键字检测一个对象是否实现了一个接口 if (strList instanceof Map) { }
- 接口之间可以继承,比如A接口可以继承B、C接口。
- 接口内的方法默认都是
public
的,字段默认是public static final
的,所以接口内只能定义方法和常量。 - 类只能继承一个类,但可以实现多个方法。Java为了简单,不像C++那样允许多继承,这也是引入接口这项技术的一个主要原因。
静态方法
从Java 8开始,接口里面可以定义静态方法了(static methods)。这个主要会涉及一个编程习惯的改变。比如在Java 8之前,为了OOP,定义一个接口的同时,会定义一个对应的类(这个类一般是抽象的或者不允许继承的),然后在接口中声明方法,在抽象类中定义一些静态方法。标准库中就有许多类似的技术,比如Path
是一个接口,声明了一些方法,与之对应的是一个Paths
类,该类中定义了一些静态方法:
// Path接口
public interface Path
extends Comparable<Path>, Iterable<Path>, Watchable
{
...
FileSystem getFileSystem();
...
}
// 对一个的Paths类
public final class Paths {
...
public static Path get(String first, String... more) {
return FileSystems.getDefault().getPath(first, more);
}
...
}
但从Java 8开始,如果有该类需求,无需再额外定义一个类了,直接在接口中实现静态方法即可(这种方式也是比较推荐的)。比如上面的Paths类实现的功能可以挪到Path接口里面去:
public interface Path
extends Comparable<Path>, Iterable<Path>, Watchable
{
...
FileSystem getFileSystem();
...
public static Path get(String first, String... more) {
return FileSystems.getDefault().getPath(first, more);
}
}
当然,JDK中的标准库因为数量庞大不太会重构了。
默认方法
Java 8开始,可以给接口中的方法提供一个默认的实现,实现方式是在方法前添加default
修饰符,并给方法提供一个方法体。这样如果实现该接口的类覆写(override)了该方法则以类中的实现为准,如果没有则使用接口中默认的实现。而且默认方法可以调用接口中的所有其它的方法(包括抽象方法)。
这种技术有一个非常重要的用途就是“接口进化(interface evolution)”,比如标准库中的Collection接口已经存在很多年了(since 1.2),已经有很多类实现了该方法。如果现在我们在该接口中增加几个方法,那原来的那些类就会编译不过了,因为它们没有实现新增的方法。如果我们不重新编译原来那些类,使用旧的Jar包,那如果有新的类使用了新增的方法,那又会报AbstractMethodError
错误。这个时候默认方法就非常有用了,我们把新增的方法全部定义为默认方法,这样原来的类也不用必须实现这些方法,新的类可以覆写这些方法,两边都不影响。标准库也的确是这样做的,比如Collection接口在1.8中引入的新的方法都是定义成默认方法的:
...
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
...
最后要讨论的就是相同的默认方法冲突的问题:如果相同的默认方法同时出现在了实现接口的类里面或者多个接口里面,会发生什么?
这里要注意:
- 方法相同指的是方法名和参数都相同。
- 这里说的是默认方法,而非常规的抽象方法,如果都是抽象方法,因为没有具体的实现,即使接口和类里面都有,抑或是多个接口里面都有,也是不存在任何冲突的。
然后我们看Java里面有默认方法相同时的冲突处理规则:
- 类优先原则。即如果是类和接口冲突了,那以类里面的实现为准,接口里面的默认方法就被自动忽略了。
- 如果多个接口里面有相同的方法,且其中有至少一个方法是默认方法,那编译就会报错。程序员必须在实现接口的类中通过重写方法来解决冲突。
第1个规则很好理解,第2个举个例子。比如类A同时实现了M和N两个接口,而M和N中呢都有一个方法String getName()
,这个时候就要分情况了:
- 如果M和N中的
getName
方法都是抽象的,那就像前面说的,不会有任何冲突; - 如果M和N中至少有一个
getName
方法是默认方法,那编译就会报错,此时,程序员必须在类A中覆写getName
方法来解决冲突。比如可以自己实现,也可以使用M.super.getName()
的方式来显式的说明使用哪个接口的默认方法。
评论已关闭