【本文更新于2019年5月20日】
更新内容是将例子讲的更通俗易懂。
由于本文属于中高阶知识,其中涉及interface知识、类的继承、抽象类、方法重载等基础知识,同学可以先自行了解一下。
1. interface简介
interface是java中的一个关键字,用于定义接口类,它最主要的作用封装一定功能的集合,被定义为接口的类不能实例化。
它的另外一个常见的用法,是用于回调功能。
通俗点讲,接口类它本身不具备具体的功能,只定义一些模版或者规范,由子类去实现具体的功能。
Java 7 版本以前,接口类只能定义一些抽象方法与常量,子类必须重写接口类中定义的全部方法。
这样就有一个明显的弊端,不需要实现的方法,也要在子类重写。
在Java 8 的时候,JDK支持接口类定义静态方法和默认方法。
在Java 9 的时候,又进行了改进,支持私有方法和私有静态方法的定义。
注意,这里我说的是方法,而不是抽象方法。也就是说JDK8与JDK9可以在接口类中定义方法体了。
2. 三个JDK版本的接口设计功能对比
Java 7 | Java 8 | Java 9 |
---|---|---|
常量 | 常量 | 常量 |
抽象方法 | 抽象方法 | 抽象方法 |
默认方法 | 默认方法 | |
静态方法 | 静态方法 | |
私有方法 | ||
私有静态方法 |
2.1 常量与抽象方法
常量与抽象方法是接口类的基础功能。
接口中定义的变量默认是public static final 型,也就是一个常量,且必须给其初值,实现类中不能重新定义,也不能改变其值
抽象方法使用abstract修饰,通常abstract省略不写。
如下代码,是演示定义一个接口类Test以及它的常量与抽象方法。
public interface Test {
String str = "abc";
int a = 10;
abstract void test();
abstract int sum();
void test2();
}
2.2 默认方法
java 8开始支持,也称扩展方法。
默认方法使用default 关键字修饰、定义。
默认方法需要写方法体,实现具体的逻辑。
实现类可以不重写默认方法,在需要的时候进行重写。
以下是实现的示例代码:
//接口定义,并定一个默认方法
public interface Test {
//默认方法
default void testDefault(){
System.out.println("default method");
}
}
//第一个实现类,不重写默认方法
public class TestImpl implements Test {
}
//第二个实现类,重写默认方法
public class TestImpl2 implements Test {
@Override
public void testDefault() {
System.out.println("TestImpl2 method");
}
}
//测试运行
public class Run {
public static void main(String[] args) {
Test test = new TestImpl();
test.testDefault();
Test test2 = new TestImpl2();
test2.testDefault();
}
}
运行结果:
没有重写父类的方法:default method
重写了父类的方法:TestImpl2 method
【代码解释说明】
接口类Test中定义了一个默认的方法testDefault(),实现了一些具体的功能,示例代码中是打印一句话“default method”。
接着,TestImpl与TestImpl2都实现了接口类Test。
其中只有TestImpl2重写了默认方法并修改了它的功能,示例代码中是打印一句话“TestImpl2 method”。
运行后的结果,也正如所见,默认方法,可以不被子类重写,被子类重写后,会执行子类所实现的具体逻辑。
总结一下:
1.JDK后它允许我们在接口类里添加一个默认方法。
2.默认方法不会破坏实现这个接口的已有类的兼容性,也就是不会强迫接口的实现类去重写默认方法。
3.java.util.Collection包中添加的stream(), forEach()等方法就是最好的例子。
2.3 静态方法
JDK 8开始支持
静态方法使用static关键字修饰、定义。
静态方法需要写方法体,实现具体的逻辑。
静态方法不可以被子类实现或继承。
//定义接口、定义静态方法
public interface TestFactory {
static Test createTest(int type){
if(type == 1){
return new TestImpl();
}else{
return new TestImpl2();
}
}
}
//测试运行
public class Run {
public static void main(String[] args) {
Test test = TestFactory.createTest(1);
test.testDefault();
Test test2 = TestFactory.createTest(2);
test2.testDefault();
}
}
运行结果:
default method
TestImpl2 method
【代码解释说明】
接口类TestFactory 定义了一个静态方法 createTest(),功能是创建Test的对象实例并返回给调用者。Test类的代码定义见2.2节。
测试运行代码中,通过得到的Test类的对象实例,成功调用其内部的方法。
2.4 默认方法与静态方法的关系
- 他们都是一个完整的方法体,各自都有具体的功能实现。
- 静态方法使用static修饰,默认方法使用default修饰。
- 默认方法内部可以调用静态方法,静态方法内部不可以调用默认方法,因为静态方法只能调用静态方法,这是语法上的限制,很容易理解,下面是默认方法调用静态方法的示例。
public interface Test {
//调用静态方法
default void testDefault(){
testStatic();
System.out.println("default method");
}
static void testStatic(){
System.out.println("static method");
}
}
//测试运行
public class Run {
public static void main(String[] args) {
Test test = TestFactory.createTest(1);
test.testDefault();
}
}
运行结果:
static method
default method
2.5 私有方法与私有静态方法
- java 9开始支持
- 私有方法使用private关键字修饰、定义。
- 私有静态方法使用private static关键字修饰、定义。
- private与private static方法,只能接口自身内部调用,实现类或子类不可重写重载。
- 都需要写方法体,实现具体的逻辑。
他们的定义示例如下:
public interface Test {
private void test(){
System.out.println(" private method");
}
private static void test2(){
System.out.println(" private static method");
}
}
如果你问我,私有方法与私有静态方法只能自身内部调用,是不是没什么意义?
答案肯定是,不是的!
其实,它同class类型对象的私有方法功能是一样的。
- 可以提高代码的重用性
- 不让子类或实现类调用,也有很好的安全性。
就拿一个数字签名来说,有PKCS#1与PKCS#7签名。他们有共性就是签名与验签,这两个功能的实现都需要数字证书,其中数字证书的解析功能等,不需要子类去实现,也不希望他们调用,避免遭破坏。那么就可以定义为私有方法。
大致过程如下:
- 1.需要实现类实现抽象方法,设置签名的类型、证书等
- 2.接口将签名的具体逻辑使用私有方法达到保护以及可重用
下面拿代码说明,首先定义签名接口类
public interface Signature {
int SIGN_PKCS1 = 0;
int SIGN_PKCS7 = 1;
//设置签名类型 p1或者P7
int setType();
//设置签名的数字证书
String setCert();
//具体的签名方法,签名逻辑用私有方法
default String sign(){
if(!checkCert()) return null;
int type = setType();
if(type == SIGN_PKCS1){
return pkcs1();
}else {
return pkcs7();
}
}
private Object parseCert(){
//解析证书
return obj;
}
private boolean checkCert(){
//检验证书是否过期、是否吊销
parseCert();
return true;
}
private String pkcs1(){
return "P1 签名结果";
}
private String pkcs7(){
return "P7 签名结果";
}
}
接口类定义了签名类型、签名方法、检验证书等具体的私有方法, sign()方法为默认方法,实现类可以重写,也可不写。这样子类只需要设置类型和证书即可,没有额外的代码。看代码:
public class SignatureP1 implements Signature {
@Override
public int setType() {
return SIGN_PKCS1;
}
@Override
public String setCert() {
return null;
}
}
public class SignatureP7 implements Signature {
@Override
public int setType() {
return SIGN_PKCS7;
}
@Override
public String setCert() {
return null;
}
}
有了默认方法和私有方法,实现类不需要强制实现各自公共的逻辑。交由接口类来实现。最后使用的方式为:
public class Run {
public static void main(String[] args) {
Signature signature1 = new SignatureP1();
signature1.sign();
Signature signature7 = new SignatureP7();
signature7.sign();
}
}
运行结果:
P1 签名结果
P7 签名结果
有人说,这他喵的不就是抽象类吗!别说,还真像。
他们有什么区别呢?请看下面我总结的几点:
他们都是抽象类型,都可以定义抽象方法,都有默认方法,不强制实现类实现。
类是单继承,接口可以多实现。
设计理念的不同,抽象类所表现的关系是"is"关系,接口所表现的是"like"关系,有点像python中的鸭子类型,当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。而抽象类不行,必须是鸭子。
前文中提到过,接口中定义的变量公开的静态常量,且必须给其初值,实现类中不能重新定义,也不能改变其值;抽象类中的变量其值可以在子类中重新定义,也可以重新赋值。
说到这里,另外强调接口的两个问题,一个是自身方法调用以及多实现的规则。
3. 方法互相调用问题
默认方法(default)可以调用abstract/private/static/private static方法。
而static方法只能调用static/private static方法。
调用关系不对,编译会不过。
4. 多实现的冲突问题
因为一个类可以实现多个接口,若是这些接口定义的方法存在一样的,便会有冲突。因为这样,子类在调用的时候,不知道调用哪一个接口定义的方法。
像这样,有两种情况下会发生:
- 两个接口没有任何关系,有相同的方法,一个类同时实现了这两个接口
- 一个接口继承了另一个接口,一个类同时实现这两个接口
4.1 一个类实现多个没有任何关系的接口
如下代码,TestC实现了TestA,TestB,而TestA,TestB中存在相同的方法
public interface TestA {
default void test(){
System.out.println("TestA");
}
}
public interface TestB{
default void test(){
System.out.println("TestB");
}
}
public class TestC implements TestA,TestB {
}
此时,会出现编译错误:
inherits unrelated defaults for test() from TestA and TestB
也就是说TestA 和 TestB 都有这个 test()方法,不知道要实现哪一个。
解决方法是重写这个相同的方法,方法内部指定一个具体的接口作为实现方法,下面代码指定TestA 作为实现方法。
当然也可以不指定父类接口的实现,重写自己的逻辑。
public class TestC implements TestA,TestB {
@Override
public void test() {
TestA.super.test();
}
}
进行测试:
public class Run {
public static void main(String[] args) {
TestC testC = new TestC();
testC.test();
}
}
运行结果
TestA
4.2 同时实现继承关系的两个接口
这种冲突,不能指定一个父接口来解决了,要么重写实现逻辑,要么按照默认的规则来调用,默认规则如下:
- 声明在类里面的方法优先于任何默认的方法,也就是,在实现类中重写这个相同的方法,调用的时候,优先级最高。
- 如果默认方法没有在类中实现,优先选取最具体的实现
重写的优先级最高,我们来看优先选取最具体的实现,接口定义如下:
public interface TestA {
default void test(){
System.out.println("TestA");
}
}
public interface TestB extends TestA{
default void test(){
System.out.println("TestB");
}
}
public class TestC implements TestA,TestB {
}
TestC 实现了TestB,TestB并且没用重写test()方法,TestB继承了TestA。测试运行:
public class Run {
public static void main(String[] args) {
TestC testC = new TestC();
testC.test();
}
}
运行结果
TestB
鉴于此,接口不能提供对Object类的任何方法的默认实现,如接口里不能提供对equals,hashCode以及toString的默认实现。
因为,一个类实现了这个接口并且是Object的子类,已经有了equals/hashCode/toString等方法的实现,那么接口定义的就没有意义了。
在类里实现的方法,调用优先级最高。
5. 总结
- 掌握JDK7\8\9三个版本中接口类的变化
- JDK 9之后,接口中支持定义方法可以包括私有的、抽象的、默认的、静态的。
- 一个类实现了多接口时,若存在相同方法,注意他们的调用方式和冲突。