概述
接口就是规范,定义的是一组规则,体现了现实世界中”如果你是 / 要…则必须能…”的思想。继承是一个"是不是"的 is-a 关系,而接口实现则是 “能不能"的 has-a 关系。
例如:Java 程序是否能够连接使用某种数据库产品,那么要看该数据库产品能否实现 Java 设计的 JDBC 规范
接口的本质是契约、标准、规范,就像我们的法律一样。制定好后大家都要遵守。
接口
接口的定义
接口的定义与类的定义方式相似,只是将 class 关键字换成了 interface 关键字。它同样也会被编译成 .class 文件,但一定要明确它并不是类,而是另外一种引用数据类型。
1
2
3
4
5
6
7
8
9
10
|
package uskg.kotoriforest.api;
public interface Interface {
}
class Test1 implements Interface {
public static void main(String[] args) {
}
}
|
在 interface 内部,我们可以声明属性和方法。其中属性必须使用 public static final 修饰,但其实interface中的属性默认就是 public static final 的,所以可以不写😓;而方法则需要声明为 public abstract,同样的,因为接口中的方法的默认修饰就为 public abstract,所以也可以不写。
上述是 jdk8 之前的规范,在 jdk8 及以后,接口内可以声明静态方法和默认方法。在 jdk9 及以后,接口内还可以声明私有方法。但我们主要还是使用 public abstract 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public interface Interface {
public static final int SIZE = 100; // int SIZE = 100; 这样也是可以的
public abstract void method(); // void method(); OK!
// 默认方法 其中 public 可以省略 但建议保留
public default void method2() {
System.out.println("method2");
}
// 静态方法 其中 public 可以省略 但建议保留
public static void method3() {
System.out.println("method3");
}
// 静态方法 可以用 static 修饰
// 如果是非 static 就是给接口内部的默认方法调用的
// 如果是 static 就是给接口内部的默认方法和静态方法调用的
private void method4() {
System.out.println("method4");
}
// public 可以省略
// 接口也能像类一样 拥有自己的内部接口😋
public interface Inner {
}
}
|
说完可以在接口内声明的,还需要提一嘴不能在接口内声明的,构造器、代码块都是不能出现于接口内部的,因为接口并不能被实例化,且接口中没有成员变量需要动态初始化。
1
2
3
4
5
|
public interface Interface {
Interface() {} // error
{} // error
static {} // error
}
|
接口的使用
类实现接口
接口不能创建对象,但是可以被类实现。类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements 关键字。
1
2
3
4
5
6
7
8
9
|
[修饰符] class [class name] implements [interface name] {
// [必须] 实现接口中的抽象方法 但如果本类是抽象类的话 就可以不实现
// [可选] 重写接口中的默认方法
}
[修饰符] class [class name] extends [super class name] implements [interface name] {
// [必须] 实现接口中的抽象方法 如果父类也是抽象类也需要实现父类中的抽象方法 但如果本类是抽象类的话 就可以不实现
// [可选] 重写接口中的默认方法
}
|
如果接口的实现类是非抽象类,那么必须重写接口中所有抽象方法。
默认方法可以选择保留原实现,也可以重写。
重写时,default单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了。
接口中的静态方法和私有方法不能被继承也不能被重写。
接口的多实现
在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。所以,一个类能继承一个父类,同时实现多个接口。
1
2
3
4
5
6
7
8
9
|
[修饰符] class [class name] implements [interface name1], [interface name2], [interface name3]... {
// [必须] 实现接口中的抽象方法 但如果本类是抽象类的话 就可以不实现
// [可选] 重写接口中的默认方法
}
[修饰符] class [class name] extends [super class name] [interface name1], [interface name2], [interface name3]... {
// [必须] 实现接口中的抽象方法 如果父类也是抽象类也需要实现父类中的抽象方法 但如果本类是抽象类的话 就可以不实现
// [可选] 重写接口中的默认方法
}
|
接口中有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public interface Interface {
void method1();
void method2();
}
interface Api {
void method1();
void method3();
}
class Test implements Interface, Api{
@Override
public void method1() { // 同时重写了 Interface 和 Api 中的 method1
System.out.println("method1");
}
@Override
public void method3() {
System.out.println("method2");
}
@Override
public void method2() {
System.out.println("method3");
}
}
|
类针对于接口的多实现,在一定程度上弥补了单继承的局限性(只能拿到声明,不能拿到实现)。
接口的多继承
一个接口能继承另一个或者多个接口,接口的继承也使用 extends 关键字,子接口会继承父接口的方法。
一个子类只能有一个父类,但谁说一个子接口只能有一个父接口了😎。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
public interface Interface {
void method1();
default void method2(){}
}
interface Api {
static void method3() {}
private static void method4() {}
}
interface Outer extends Interface, Api {
default void method5() {
method1();
method2();
Api.method3();
// 子接口不能继承父接口的 static 和 private 方法
}
}
class Test implements Outer {
@Override
public void method1() {
}
@Override
public void method2() {
Outer.super.method2();
}
@Override
public void method5() {
Outer.super.method5();
}
}
|
接口与实现类对象构成多态引用
实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间,也可以构成多态引用。通过接口类型的变量调用方法,最终执行的是你 new 的实现类对象实现的方法体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public interface IOut {
void goOut();
}
class TheRich implements IOut {
public void goOut() {
System.out.println("坐飞机头等舱");
}
}
class ThePoor implements IOut {
public void goOut() {
System.out.println("坐快车硬座");
}
}
class Test {
public static void main(String[] args) {
IOut theRich = new TheRich();
theRich.goOut();
System.out.println("--------------------");
IOut thePoor = new ThePoor();
thePoor.goOut();
}
}
|
1
2
3
|
坐飞机头等舱
--------------------
坐快车硬座
|
接口在 jdk8 和 jdk9 中的新特性
使用接口的静态成员
接口不能直接创建对象,但是可以通过接口名直接调用接口的静态方法和静态常量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public interface Interface {
int NUMBER = 0X1234;
static void method(){
System.out.println("method");
};
}
class Test implements Interface{
public static void main(String[] args) {
System.out.println(Interface.NUMBER); // OK
System.out.println(Test.NUMBER); // OK
Interface.method(); // OK
Test.method(); // error: static 方法通过其的定义接口调用 不能通过实现类调用
}
}
|
在 Java 中,接口不能继承父接口的 static 方法。这是因为 static 方法属于接口本身,而不是它的实现类或子接口。
- static 方法在 Java 接口中只能通过接口名直接调用,不能通过实现类或子接口进行调用。
- 子接口或实现类不会继承父接口的 static 方法。
- 这是 Java 的设计决定:static 方法属于定义它的接口,不能通过继承传播。
回忆一下 Java 中的继承对 static 方法的处理方式:
在 Java 中,子类不能继承父类的 static 方法,但子类可以通过类名直接调用父类的 static 方法。这是因为 static 方法是属于类本身的,而不是属于某个实例或子类。
- static 方法属于类,而不是对象:
static
方法在类加载时就存在,属于类本身,不依赖于类的实例。
- 不能通过继承来重写 static 方法:子类不会真正“继承”父类的 static 方法,而是拥有它自己的 static 方法空间。如果子类定义了一个相同名字的 static 方法,这种行为被称为“隐藏”而不是“重写”。
- 调用方式:static 方法应通过类名调用,即使子类定义了相同的方法,也只能通过类名调用,不应通过实例调用。
使用接口的默认方法
- 对于接口的静态方法,直接使用接口名进行调用即可,也只能使用接口名进行调用,不能通过实现类的对象进行调用。
- 对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用,接口不能直接创建对象,只能创建实现类的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public interface Interface {
// default 可以看作是用来顶替 abstract 的位置的
// 如果什么也不写会默认是 abstract 而 default 就是为了声明该函数不是抽象函数,具有函数体
public default void method1() {
System.out.println("method1");
}
default void method2() {
System.out.println("method2");
}
default void method3() {
// default 函数可以调用其他的default、static、private、private static 方法
method1();
method2();
System.out.println("method3");
}
}
class Test implements Interface {
@Override
public void method1() {
// Interface.super.method1();
System.out.println("Test::method1");
}
public static void main(String[] args) {
Test test = new Test();
test.method3();
}
}
|
1
2
3
|
Test::method1
method2
method3
|
接口中声明的默认方法可以被实现类继承,实现类在没有重写此方法的情况下,默认调用接口中声明的默认方法。如果实现类重写了此方法,则调用的是自己重写的方法。
使用接口的私有方法
接口的私有方法非常简单,你可以简单的认为它是私有的默认方法,但是不能使用 default 关键字。
1
2
3
4
|
interface Interface {
// 可以带上 static
private void method() {}
}
|
私有方法的主要作用就是将静态方法和默认方法中重复出现的代码提取出来,然后进行复用。
jdk8 中相关冲突问题
默认方法冲突
接口冲突
当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?
无论你多难抉择,最终都是要做出选择的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public interface Interface {
default void method() {
System.out.println("Interface method");
}
}
interface Api {
default void method() {
System.out.println("Api method");
}
}
class Test implements Interface, Api {
// error: Test 从类型 Interface 和 Api 继承 method() 的不相关默认值
}
|
类实现了两个接口,而两个接口定义了同名同参数(相同签名)的默认方法。则实现类在没有重写这两个接口默认方法的条件下,会报错。我们把这个叫做接口冲突。(如果是抽象方法就不会有这个问题,因为抽象方法一定要重写😋)
1
2
3
4
5
6
7
8
9
10
11
|
class Test implements Interface, Api {
@Override
public void method() {
// 解决方案1: 保留其中一个父接口的实现 下面二选一
// Interface.super.method();
// Api.super.method();
// 解决方案2: 完全重写
System.out.println("Test method");
}
}
|
当一个子接口同时继承了多个接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?
当然也是需要重写的了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public interface Interface {
default void method() {
System.out.println("Interface method");
}
}
interface Api {
default void method() {
System.out.println("Api method");
}
}
interface IProgram extends Interface, Api {
@Override
default void method() {
// Interface.super.method();
// Api.super.method();
System.out.println("IProgram method");
}
}
|
子接口重写默认方法时,default 关键字必须保留。
子类重写默认方法时,default 关键字不可以保留。
类优先原则
子类(或实现类)继承了父类并实现了接口,并且父类和接口中声明了签名相同的方法。默认的,子类(或实现类)在没有重写此方法的情况下,调用的是父类中的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public interface Interface {
default void method1() {
System.out.println("Interface method1");
}
void method2();
}
class SuperClass {
public void method1() {
System.out.println("SuperClass method1");
}
public void method2() {
System.out.println("SuperClass method2");
}
}
class SubClass extends SuperClass implements Interface {
public static void main(String[] args) {
SubClass subClass = new SubClass();
subClass.method1();
subClass.method2();
System.out.println("-----------------------------");
new SubClass() {
@Override
public void method1() {
System.out.println("SubClass method1");
}
}.method1();
}
}
|
1
2
3
4
|
SuperClass method1
SuperClass method2
-----------------------------
SubClass method1
|
如果现在我们有一个实现类重写了父类和接口中的签名相同的方法,那么我们如何分别调用实现类自己的、父类的、父接口中的签名相同的方法呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class SubClass extends SuperClass implements Interface {
@Override
public void method1() {
System.out.println("SubClass method1");
}
public void exec() {
method1(); // 调用自己的
super.method1(); // 调用父类的
Interface.super.method1(); // 调用父接口的
}
public static void main(String[] args) {
new SubClass().exec();
}
}
|
1
2
3
|
SubClass method1
SuperClass method1
Interface method1
|
常量冲突问题
问题出现的原因:
- 当子类既继承父类又实现父接口,而父类中存在与父接口常量同名的成员变量,并且该成员变量名在子类中仍然可见。
- 子类同时实现多个接口,而多个接口存在相同同名常量。
上述条件只要满足其一,在子类中想要引用父类或父接口的同名的常量或成员变量时,就会有冲突。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class SuperClass { int x = 1;}
interface SuperInterface { int x = 2; int y = 2; }
interface MotherInterface { int x = 3; }
class SubClass extends SuperClass implements SuperInterface, MotherInterface {
public void method(){
// System.out.println("x = " + x);//模糊不清
System.out.println("super.x = " + super.x);
System.out.println("SuperInterface.x = " + SuperInterface.x);
System.out.println("MotherInterface.x = " + MotherInterface.x);
System.out.println("y = " + y);//没有重名问题,可以直接访问
}
public static void main(String[] args) {
new SubClass().method();
}
}
|
1
2
3
4
|
super.x = 1
SuperInterface.x = 2
MotherInterface.x = 3
y = 2
|
接口的总结与面试题
总结
-
接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。
-
声明接口用 interface ,接口的成员声明有限制:
① 公共的静态常量
② 公共的抽象方法
③ 公共的默认方法 ( JDK8.0 及以上)
④ 公共的静态方法 ( JDK8.0 及以上)
⑤ 私有方法 ( JDK9.0 及以上)
-
类可以实现接口,关键字是 implements ,并且支持多实现。如果实现类不是抽象类就必须实现接口中所有的抽象方法。如果实现类既要继承父类又要实现父接口,那么继承( extends )在前,实现 ( implements )在后。
-
接口可以继承接口,关键字是 extends ,而且支持多继承。
-
接口的默认方法可以选择重写或不重写。如果有冲突问题就需要另行处理。子类重写父接口的默认方法,要去掉 default ,子接口重写父接口的默认方法,不要去掉 default。
-
接口的静态方法不能被继承,也不能被重写。接口的静态方法只能通过 “ 接口名.静态方法名 ” 进行调用。
面试题
-
为什么接口中只能声明公共的静态的常量?
因为接口是标准规范,那么在规范中需要声明一些底线边界值,当实现者在实现这些规范时,不能去随意修改和触碰这些底线,否则就有“危险”。
-
为什么 JDK 8.0 之后允许接口定义静态方法和默认方法呢?它违反了接口作为一个抽象标准定义的概念。
- 静态方法:因为之前的标准类库设计中,有很多 Collection / Colletions 或者 Path / Paths 这样成对的接口和类,后面的类中是静态方法,而这些静态方法都是为前面的接口服务的,那么这样设计一对 API,不如把静态方法直接定义到接口中使用和维护更方便。
- 默认方法:(1)我们要在已有的老版接口中提供新方法时,如果添加抽象方法,就会涉及到原来使用这些接口的类就会有题,那么为了保持与旧版本代码的兼容性,只能允许在接口中定义默认方法实现。比如:Java8 中对 Collection、List、Comparator等接口提供了丰富的默认方法。(2)当我们接口的某个抽象方法,在很多实现类中的实现代码是一样的,此时将这个抽象方法设计为默认方法更为合适,那么实现类就可以选择重写,也可以选择不重写。
-
为什么JDK1.9 要允许接口定义私有方法呢?我们说接口是规范,规范是需要公开让大家遵守的。
因为有了默认方法和静态方法这样具有具体实现的方法,那么就可能出现多个方法由共同的代码可以抽取,而这些共同的代码抽取出来的方法又只希望在接口内部使用,所以就增加了私有方法。
接口与抽象类之间的对比