学习Java中有关接口的概念。
参考资料
《Java核心技术 卷1(第十一版)》
《Java基础核心总结》
接口的概念
在 Java 中,接口不是类,而是对希望符合这个接口的类的一组需求。下面以 Comparable 接口为例。
1 | public interface Comparable |
Array 类中的 sort 方法承诺可以对对象数组进行排序,要是排序对象所属的类必须实现 Comparable 接口。任何实现 Comparable 的类都必须包含一个 compareTo 方法。
如果给 sort 传递了一个没实现接口的类的对象数组,sort会将其强制类型转换;如果这个对象数组成员不属于任何实现了 Comparable 的类,编译时就会报错。
接口中的方法默认均为 public 方法,声明时不需要提供关键字(接口中的方法修饰符仅限于 public, private, abstract, default, static and strictfp)。
接口中不能有实例字段,但是 Java 8 之后可以在接口中实现简单的方法(前提是不引用任何实例字段)。
将类声明为实现某个接口的语句为 class 类名 implements 接口1(,接口2,接口3,...)
。在接口声明中,需要被实现的方法可以不写 public 修饰符,但是在实现接口的类中声明这些方法时,必须写上 public。
接口的属性
接口不是类,不能使用 new 实例化一个接口。
1 | Comparable x = new Comparable(..); //ERROR |
但是可以声明接口的变量,接口类型的变量必须引用实现了这个接口的类对象。
1 | Comparable x; |
instanceof 可以检测一个对象是否实现了某个特定的接口。
1 | if(anObject instanceof Comparable) {...} |
接口可以扩展接口,语法同类继承。
接口中不能包含实例字段,但是可以包含常量。与接口中的方法都默认是 public 一样,接口中的字段默认是 public static final。
1 | public interface A {int ACONSTANT = 1; } |
尽管每个类只能有一个超类,但是可以实现多个接口。
1 | class Employee implements Comparable, Cloneable {...} |
虽然类不能多继承,但是接口可以同时继承多个接口。
1 | interface C extends A, B {...} // OK |
接口方法
静态和私有方法
Java 8 中允许在接口中添加静态方法(虽然通常情况下相关的静态方法会被存放在接口的伴随类中,例如 Collection/Collections 和 Path/Paths)。
在Java 9 中,可以为接口添加私有方法,私有方法可以是静态或者实例方法,一般作为接口中的辅助方法来使用。
默认方法
可以为接口中的方法提供一个默认的实现,例如:
1 | public interface Comparable |
默认方法可以调用其他方法,例如:
1 | public interface Collection |
默认方法的一个重要作用是 “接口演化”。
以 Collection 接口为例,假如之前你为其提供了一个 Bag 类。
1 | public class Bag implements Collection {...} |
后来,在 Java 8 中,又为其提供了一个 stream 方法。如果该方法不是一个默认方法,那么此时 Bag 类将不能编译成功,因为它没有实现这个方法。如果将方法实现为一个默认方法的话,Bag 就能正常编译了。另外,如果没有重新编译 Bag 类,而是直接加载并在一个 Bag 实例上调用 stream 方法,将会调用 Collection.stream 默认方法。
解决默认方法的冲突
如果先在一个接口中定义了一个默认方法,然后又在另一个超类或者接口中定义了一个同样的方法,某个类在继承超类且实现接口时可能会产生二义性的问题。Java 中对应的规则如下:
- 超类优先。
如果超类中提供了一个具体的方法,且与实现接口中的某个默认方法同名同参数,默认方法会被覆盖。 - 接口冲突。
如果一个接口中提供了一个默认方法,另一个接口也定义了同名同参数类型的方法(无论是不是默认方法),必须要覆盖这个方法来解决冲突。
以下面的 getName 方法为例,Student 类会继承两个接口提供的不一致的 getName 方法。并不是在其中任选一个,Java 编译器会报错。解决方法是在 Student 类中再提供一个 getName 方法进行覆盖,可以选择两个冲突方法中的其中一个,例如,1
2
3
4
5
6
7
8
9
10
11interface Person
{
default String getName() {return ""; }
}
interface Named
{
default String getName() {return getClass().getName() + "_" + hashCode(); }
}
class Student implements Person, Named {...}1
public String getName() {return Person.super.getName();}
接口如何产生冲突并不重要,只要两个接口中提供了同名同参数类型的方法,且至少一个接口中为其提供了默认实现,Java 编译器就会报错。比如,就算 Named 接口没有提供 getName 的默认实现,还是会产生冲突问题。
相反,如果两个接口都没有为共享方法提供默认实现,就不会发生冲突。实现类可以选择实现这个方法或者不实现,不实现的时候这个类就是一个抽象类。
假如上述代码中的 Person 是 Student 继承的超类,那么就会采取规则 1, Student 会继承超类中的方法,Named 中的相同默认方法会被忽略。
Cloneable接口与对象克隆
Java 中对引用类型变量作等号赋值的操作是浅拷贝,如果需要对对象进行深拷贝,就需要用到类的 clone 方法。
clone 是类 Object 中的一个 protected 方法,只对包 java.lang. 以及其子类中可见,直接使用的话,就只有对应类中可以克隆对应类的对象。
Object 中的克隆方法的操作是对对象中的字段逐一拷贝,对于只包含基本数据类型或者不可变类的对象的类来说没有问题;但是如果类中有其他子对象的引用,该部分字段就仍是浅拷贝。所以一般情况下自定义的类需要重新定义 clone 方法来实现完全的深拷贝。
重定义时需要注意两点:
- 类必须注明实现 Cloneable 接口。
这个接口仅作为标记——毕竟 clone 方法是从 Object 中继承来的。如果一个对象请求克隆,但是没有实现这个接口,编译器会生成一个 CloneNotSupportedException 异常。
Cloneable 接口是 Java 提供的少数标记接口之一,这种接口中没有定义任何方法,只是用来允许在类查询中使用关键字 instanceof。一般建议自己的程序中不要使用标记接口。
- 重定义 clone 方法时应声明为 public 方法。
由于 clone 原来是 protected 方法,重新定义时就需要定义其为 public 才能保证自己类以外的方法可以克隆自己类的对象。
当然,即使 clone 的默认实现(浅拷贝)能够满足要求,韩式需要实现 Cloneable 接口,然后将其重定义为 public 方法包装一下,在方法中调用 super.clone()
。例如:
1 | class Employee implements Cloneable |
要重定义深拷贝的克隆,需要对类实例中的对应引用类型字段也进行深拷贝,如果包含没有实现深拷贝版 clone 的类,同时就需要为其又定义一次 clone();如果当前的类有被其它类继承,就还得考虑子类中的克隆。
听起来很麻烦,但是实际上克隆确实并没有想象中那么常用,标准库中只有不到 5% 的类实现了 clone …… 尽量使用默认 clone 或者不使用为好。