接口的概念与定义
Java中的接口(interface)技术主要用来描述类具有什么功能,而并不给出每个功能的具体实现。一个类可以实现(implements)一个或者多个接口,并且在需要接口的地方,随时使用实现类相应接口的对象。也就是说,如果接口A
规定了C
、D
方法(只是声明,在A
中并没有具体实现),那么实现了接口A
的类B
就必须提供C
、D
方法的具体实现,除非B
是抽象类。
一般来说,接口类中不会包含方法的具体实现,但是Java8有一个新特性,可以使用default
关键字在接口类中实现一些默认的方法,这些方法称为default
方法或者Defender
方法,也可以称为虚拟扩展方法(Virtual extension method)。
接口中不能含有实例域,其default
方法也不能引用实例域;另外,接口中的方法不需要加关键字,默认都是public
。
定义一个接口的方法示例如下,使用interface
关键字即可,可以根据需要加方法的具体实现:
public interface Iterator<T> { boolean hasNext(); E next(); // Iterator接口中的 default 方法, // 如果迭代器不支持remove操作,则抛出UnsupportedOperationException // 这个方法的作用是 删除在迭代器指向的Collection中,迭代器返回的最后一个元素 // remove()去掉的是当前it.next()返回的元素 default void remove() { throw new UnsupportedOperationException("remove"); } // 这个方法的作用是 对每个剩余的元素执行action操作,知道所有的元素被执行完毕或者因为抛出异常而终止。 // 如果指定了顺序,则按迭代的顺序执行操作,抛出的异常将会转发给调用这个函数的上层函数 default void forEachReamining(Consumer<? super E> action) { Objects.requiredNonNull(action); while (hasNext()) action.accept(next()); } } |
再给一个Java源码中List
接口的示例,它也只是对一些实现该接口的类必须实现的方法进行了声明,由于本身就是一个接口,因此没有对方法的具体实现,除了default
方法:
public interface List<E> extends Collection<E> { int size(); boolean isEmpty(); boolean contains(Object 0); Iterator<E> iterator(); } |
实现接口
为了让类实现某个接口,通常需要以下两个步骤:
- 将类声明为实现给定的接口(implements)关键字
- 对接口中的所有方法进行定义或者给出具体实现
参考Java中的集合ArrayList
类,就是实现了List
接口,所以ArrayList
中有对List
所规定的方法的具体实现:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private int size; // 对size()方法的具体实现 public int size() { return size; } } |
接口的继承
在Java中,一个接口不能实现(implements)另外一个接口,但是可以继承(extends)其他接口。和类的继承不同的是,一个接口可以继承多个接口(多继承),而一个类只能继承一个父类(单继承)。
Java中不允许类的多继承的原因是,如果A
同时继承B
和C
,而B
和C
同时有一个方法D
,就会导致编译器不知道A
到底要继承B
还是C
的D
方法。
但是接口就不存在这样的问题,因为接口中一般只有对方法的声明,但是并没有方法的具体实现。这些全部都是抽象方法,因此继承哪一个都无所谓,所以接口可以继承多个接口。
举个例子,JDK的源码中,BlockingDeque
接口是这么定义的:
public interface BlockingDeque<E> extends BLockingQueue<E> Deque<E> { // .... } |
需要注意的是以下几点:
- 一个类如果实现了一个接口,则必须要实现该接口的所有方法
- 方法的名字、返回类型、参数必须与接口中的完全一致
- 因为接口的方法默认都是
public
,因此在这些方法具体实现的时候,必须使用public
修饰符
接口的特性
- 接口不是类,不能用
new
操作符实例化一个接口 - 接口可以被声明为变量,比如
final List<String> list;
- 接口变量必须引用实现了该接口的类对象,比如
final List<String> list = new ArrayList();
,变量list
必须被赋值为实现了List
接口的类对象的实例。在这里就是ArrayList
。 - 接口可以被扩展,使用
extends
关键字,比如List
接口扩展自Collection
接口:public interface List<E> extends Collection<E> {}
- 接口中不能包含实例域或静态方法(Java8中已经支持接口中增加静态方法),但是可以包含常量
- 接口中的方法会被自动设置为
public
,接口中的域也会被自动设置为public static final
- 每个类只能有一个超类,但是却能实现多个接口,比如
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}
就实现了多个接口。
为什么要使用接口
对于接口,初学的时候有这两个疑问:
- 为什么要实现接口,直接在这个类中写实现方法不是更便捷?这样还省去了定义接口的时间
- 为什么不用抽象类来代替
对于第一个问题,实际上Java中的接口是规范,一般接口就是在很多地方都会被用到,大家需要统一标准,所以使用了接口可以为不同的类进行顺利交互提供标准。
那么我们在什么时候需要通过接口建立规范呢?答案是在为了抽象系统的某种公共行为, 或者封装变化性,进行系统设计的时候需要抽取出接口,这样将来系统会变得更加灵活。
对于第二个问题,在Java中一个类只能有一个超类,这就大大的限制了这个类的通用属性,比如类A
继承了B
之后,就只能有B
的通用属性,却不能有C
的通用属性。而一个类却能实现多个接口,这样就可以使得一个类A
既能有B
接口的通用属性,也能有C
接口的通用属性。
在一些编程语言当中,比如C++就允许继承多个类,但是这样会增加多重继承的复杂性和低效性,使用接口就可以大大减少多重继承带来的缺点。
静态方法
在Java8中已经支持接口中增加静态方法,但是这有违于接口作为抽象规范的本质。
所以目前的通常做法是将静态方法放在伴随类中,比如标准库中的Collection
和Collections
或Path
和Paths
。
默认方法
Java8支持为接口里的方法提供一个默认实现,使用default
关键字声明,然而这并没有太大用处,因为实现了这个接口的类中这个方法的具体实现一定会覆盖这个默认方法。不过,在某些时候,默认方法也能带来便利。
比如接口A
提供2个方法:
public interface A { void method1(); void method2(); } |
而这两个方法都不是默认方法,所以当B
实现了接口A
时,你需要在B
中同时实现两个方法,但实际上你需要的可能只是方法一,这个时候如果方法二有默认实现,你就不需要再次去实现对你并没啥用的方法二了。
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或者另一个接口中定义了同样的法国法,就会出现默认方法冲突,在Java中解决这种冲突有两种原则:
- 超类优先,如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被覆盖
- 接口冲突,如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,编译器会给提示并且报错,必须由编程人员自己来解决这个冲突。