十一、接口
对于面向对象编程来说,抽象是一个极具魅力的特征。如果一个程序员的抽象思维很差,那他在编程中就会遇到很多困难,无法把业务变成具体的代码。在 Java 中,可以通过两种形式来达到抽象的目的: 抽象类、接口。
11.1 概述
-
接口:是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量、构造方法和成员方法,那么接口的内部主要就是封装了方法,包含抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法 (JDK 9)。
-
接口的定义:它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。
小贴士:
引用数据类型:数组、类、接口 -
接口的使用:它不能创建对象(不能被实例化,但是使用匿名接口时可以),但是可以被实现( implements ,类似于被继承)。一个实现接口的类(可以看做是接口的子类),需要实现接口中所有的抽象方法,创建该类对象,就可以调用方法了,否则它必须是一个抽象类。
小贴士:
接口中的方法都是抽象方法,不允许有普通方法!
11.2 接口定义
接口通过 interface
关键字来定义,它可以包含一些常量和方法:
/**
* @author QHJ
* @date 2022/8/31 14:36
* @description: 接口
*/
public interface MyInterface {
// 常量
String LED = "LED";
// 抽象方法
int getElectricityUse();
// 静态方法
static boolean isEnergyEfficient(String electtronicType) {
return electtronicType.equals(LED);
}
// 默认方法
default void printDescription() {
System.out.println("电子");
}
}
查看上面代码的反编译结果:
public interface MyInterface
{
public abstract int getElectricityUse();
public static boolean isEnergyEfficient(String electtronicType)
{
return electtronicType.equals("LED");
}
public void printDescription()
{
System.out.println("\u7535\u5B50");
}
public static final String LED = "LED";
}
由此可见,接口中定义的所有变量或者方法,都会自动添加上 public
关键字。
01、MyInterface 接口中的核心知识点
-
接口中定义的变量会在编译的时候自动加上
public static final
修饰符(这个注意看一下反编译后的字节码),也就是说 LED 变量其实就是一个常量。Java 官方文档上有这样的声明:
Every field declaration in the body of an interface is implicitly public, static, and final.( 接口主体中的每个字段声明都是隐式公共的、静态的和最终的。)
换句话说,接口可以用来作为常量类使用,还能省略掉
public static final
,这看似是一种不错的选择,但是这种选择并不可取。因为接口的本意是对方法进行抽象,而常量接口会对子类中的变量造成命名空间上的 “污染”。 -
没有使用
private
、default
、static
关键字修饰的方法是隐士抽象的,在编译的时候会自动加上public abstract
修饰符。也就是说上面getElectricityUse()
其实是一个抽象方法,没有方法体——这是定义接口的本意。 -
从 Java 8 开始,接口中允许有静态方法。静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口名来调用:
MyInterface.isEnergyEfficient("LED")
。接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。
-
接口中允许定义
default
方法也是从 Java 8 开始的,它始终由一个代码块组成,为实现该接口而不覆盖该方法的类提供默认实现
。既然要提供默认实现,就要有方法体,换句话说,默认方法后面不能直接使用 “;” 号来结束——编译器会报错。
允许在接口中定义默认方法的理由很充分,因为一个接口可能有多个实现类,这些类就必须实现接口中定义的抽象类,否则编译器就会报错。假如我们需要在所有的实现类中追加某个具体的方法,在没有default
方法的帮助下,我们就必须挨个对实现类进行修改。
02、结论
- 接口中允许定义变量
- 接口中允许定义抽象方法
- 接口中允许定义静态方法(Java 8 之后)
- 接口中允许定义默认方法(Java 8 之后)
03、扩展
-
接口中没有构造方法,不允许直接实例化,否则编译器会报错。
需要定义一个类去实现接口,然后再实例化:/** * @author QHJ * @date 2022/8/31 15:16 * @description: 接口的实现类 */ public class MyInterfaceImpl implements MyInterface{ public static void main(String[] args) { new MyInterfaceImpl(); } @Override public int getElectricityUse() { return 0; } } /** * @author QHJ * @date 2022/8/31 14:41 * @description: */ public class InterfaceTest { public static void main(String[] args) { new MyInterfaceImpl(); } }
-
接口可以是空的,既可以不定义变量,也可以不定义方法。其中最典型的例子就是 Serializable 接口:
public interface Serializable { }
Serializable 接口用来为序列化的具体实现提供一个标记,也就是说,只要某个类实现了 Serializable 接口,那么它就可以用来序列化了。
-
不要在定义接口的时候使用
final
关键字,否则会报编译错误,因为接口就是为了让子类实现的,而 final 阻止了这种行为。
-
接口的抽象方法不能是
private、protected、final
,否则编译器都会报错:
-
接口中无法定义成员变量,但是可以定义变量,变量其实是隐式
public static final
(常量),所以其值无法改变。//默认是一个静态常量,不可以被修改 (static final) int age = 20;
-
接口中没有静态代码块。
11.3 接口可以做什么?
01、使某些类具有某些功能
使某些实现类具有我们想要的功能。比如:实现了 Cloneable 接口的类具有拷贝的功能,实现了 Comparable 或 Comparator 的类具有比较功能。
Cloneable 和 Serializable 一样都属于标记性接口,它们内部都是空的。实现了 Cloneable 接口的类可以使用 Object.clone()
方法,否则会抛出 CloneNotSupportedException。
/**
* @author QHJ
* @date 2022/8/31 15:37
* @description: 使实现类具有某些功能
*/
public class CloneableTest implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneableTest test1 = new CloneableTest();
CloneableTest test2 = (CloneableTest) test1.clone();
}
}
02、通过接口实现多继承
Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的。
如果有两个类共同继承一个父类,那么父类的方法就会被两个子类重写。然后,如果有一个新类同时继承了这两个子类,那么在调用重写方法的时候编译器就不能识别要调用哪个类的方法了。这也正是著名的菱形问题:
ClassC 同时继承了 ClassA 和 ClassB,ClassC 的对象在调用 ClassA 和 ClassB 中重写的方法时,就不知道该调用 ClassA 的方法还是 ClassB 的方法。
但是接口就没有这方面的困扰。定义两个接口 Fly 和 Run,一个实现类 Dog:
/**
* @author QHJ
* @date 2022/8/31 16:37
* @description: Fly接口
*/
public interface Fly {
void fly();
}
/**
* @author QHJ
* @date 2022/8/31 16:38
* @description: Run接口
*/
public interface Run {
void run();
}
Dog 类同时实现这两个接口:
/**
* @author QHJ
* @date 2022/8/31 16:39
* @description: 狗类
*/
public class Dog implements Fly, Run {
@Override
public void fly() {
System.out.println("会飞的狗");
}
@Override
public void run() {
System.out.println("会跑的狗");
}
}
在某种形式上,接口实现了多重继承的目的:在现实世界里,狗的确只会跑,但是在某些虚幻的梦境中,狗是会飞的。这就意味着需要赋予这只狗更多的能力,通过抽象类是无法实现的,只能通过接口。
03、实现多态
多态,通俗的理解就是同一件事发生在不同的对象上会产生不同的结果
。就好比鼠标的左键可以点击 × 来关闭一个页面,也可以点击超链接打开新的页面。
多态可以通过继承的关系实现,也可以通过接口的形式实现:
/**
* @author QHJ
* @date 2022/8/31 16:46
* @description: 形状接口
*/
public interface Shape {
String name();
}
Circle 类和 Square 类分别实现了 Shape 接口,并重写了 name() 方法:
/**
* @author QHJ
* @date 2022/8/31 16:47
* @description:
*/
public class Circle implements Shape{
@Override
public String name() {
return "圆形";
}
}
/**
* @author QHJ
* @date 2022/8/31 16:47
* @description:
*/
public class Square implements Shape{
@Override
public String name() {
return "正方形";
}
}
测试类:
/**
* @author QHJ
* @date 2022/8/31 14:41
* @description: 接口测试类
*/
public class InterfaceTest {
public static void main(String[] args) {
List<Shape> shapes = new ArrayList<>();
Shape circleShape = new Circle();
Shape squareShap = new Square();
shapes.add(circleShape);
shapes.add(squareShap);
for (Shape shape : shapes){
System.out.println(shape.name()); // 圆形 正方形
}
}
}
这就实现了多态,变量 circleShape 和 squareShap 的引用类型都是 Shape,但执行 shape.name() 方法的时候,Java 虚拟机知道该去调用 Circle 的 name() 方法还是 Square 的 name() 方法。
多态存在的 3 个前提:
- 要有继承关系,比如 Circle 类和 Square 类都实现了 Shape 接口;
- 子类要重写父类的方法,Circle 和 Square 都重写了 name() 方法;
- 父类引用指向子类对象,circleShape 和 squareShape 的类型都为 Shape,但前者指向的是 Circle 对象,后者指向的是 Square 对象。
也就意味着,尽管在 for 循环中,shape 的类型都为 Shape,但在调用 name() 方法的时候,它知道 Circle 对象应该调用 Circle 类的 name() 方法,Square 对象应该调用 Square 类的 name() 方法。
11.4 基本的实现
01、实现
类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类 似继承,格式相仿,只是关键字不同,实现使用 implements 关键字。
非抽象子类实现接口:
- 必须重写接口中所有抽象方法。
- 继承了接口的默认方法,既可以直接调用,也可以重写。
02、实现格式
class 类名 implements 接口名 {
// 重写接口中抽象方法【必须】
// 重写接口中默认方法【可选】
}
03、使用
-
抽象方法的使用
所有抽象方法必须全部实现。
-
默认方法的使用
可以继承、可以重写
,二选一,但是只能通过实现类的对象来调用。- 继承默认方法
//定义接口 public interface IUSB { /** * 默认方法 */ public default void service2(){ System.out.println("默认方法输出service2"); } } //定义实现类 public class UDISK implements IUSB{ @Override public void service2() { //继承,什么都不用写,直接调用 } } //测试类 public class test { public static void main(String[] args) { IUSB iusb = new UDISK(); iusb.service2(); } } 输出结果: 默认方法输出service2
- 重写默认方法
//定义接口 public interface IUSB { /** * 默认方法 */ public default void service2(){ System.out.println("默认方法输出service2"); } } //定义实现类 public class UDISK implements IUSB{ @Override public void service2() { //方法重写 System.out.println("重写默认方法输出service2"); } } //测试类 public class test { public static void main(String[] args) { IUSB iusb = new UDISK(); //调用重写的默认方法 iusb.service2(); } } 输出结果: 重写默认方法输出service2
-
静态方法的使用
静态与.class 文件相关,只能使用接口名调用,不可以通过实现类的类名或者实现类的对象调用。
//定义接口 public interface IUSB { /** * 静态方法 */ public static void service3(){ System.out.println("静态方法输出service3"); } } //定义实现类 public class UDISK implements IUSB{ //无法重写静态方法 } //测试类 public class test { public static void main(String[] args) { //UDISK.service3();错误,无法继承方法,也无法调用。 IUSB.service3(); } } 输出结果: 静态方法输出service3
11.5 接口的多实现
01、多实现
在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。
02、实现格式:
class 类名 [extends 父类名] implements 接口名1,接口名2,接口名3... {
// 重写接口中抽象方法【必须】
// 重写接口中默认方法【不重名时可选】
}
[ ]: 表示可选操作。
03、使用
-
抽象方法
接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。
//定义多个接口 interface A { public abstract void showA(); public abstract void show(); } interface B { public abstract void showB(); public abstract void show(); } //定义实现类 public void C implements A,B{ @Override public void showA() { System.out.println("showA"); } @Override public void showB() { System.out.println("showB"); } @Override public void show() { System.out.println("show"); //重名抽象方法只用重写一次 } }
-
默认方法
接口中,有多个默认方法时,实现类都可继承使用。如果默认方法有重名的,必须重写一次。
//定义多个接口 interface A { public default void showA(); public default void show(); } interface B { public default void showB(); public default void show(); } //定义实现类 public void C implements A,B{ @Override public void showA() { System.out.println("showA"); } @Override public void showB() { System.out.println("showB"); } @Override public void show() { System.out.println("show"); //重名默认方法必须重写 } }
04、优先级问题
当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。
//定义接口
interface A {
public default void methodA(){
System.out.println("AAAAAAAAAAAA");
}
}
//定义父类
class D {
public void methodA(){
System.out.println("DDDDDDDDDDDD");
}
}
//定义子类
class C extends D implements A {
// 未重写methodA方法
}
//定义测试类
public class Test {
public static void main(String[] args) {
C c = new C();
c.methodA();
}
}
输出结果:
DDDDDDDDDDDD
11.6 接口的多继承
一个接口能继承另一个或者多个接口
,这和类之间的继承比较相似。接口的继承使用 extends 关键字,子接口继承父接口的方法。如果父接口中的默认方法有重名的,那么子接口需要重写一次。
//定义父接口
interface A {
public default void method(){
System.out.println("AAAAAAAAAAAAAAAAAAA");
}
}
interface B {
public default void method(){
System.out.println("BBBBBBBBBBBBBBBBBBB");
}
}
//定义子接口
interface D extends A,B{
@Override
public default void method() {
System.out.println("DDDDDDDDDDDDDD");
}
}
小贴士:
子接口重写默认方法时,default 关键字可以保留。
子类重写默认方法时,default 关键字不可以保留。
11.7 接口在应用中常见的三种模式
在编程领域,好的设计模式能够让我们的代码事半功倍。在使用接口的时候,经常会用到三种模式:策略模式、适配器模式和工厂模式。
01、策略模式
策略模式的思想是,针对一组算法,将每一种算法封装到具有公共接口的实现类中,接口的设计者可以在不影响调用者的情况下对算法做出改变:
// 接口:教练
interface Coach {
// 方法:防守
void defend();
}
// 何塞·穆里尼奥
class Hesai implements Coach {
@Override
public void defend() {
System.out.println("防守赢得冠军");
}
}
// 德普·瓜迪奥拉
class Guatu implements Coach {
@Override
public void defend() {
System.out.println("进攻就是最好的防守");
}
}
public class Demo {
// 参数为接口
public static void defend(Coach coach) {
coach.defend();
}
public static void main(String[] args) {
// 为同一个方法传递不同的对象
defend(new Hesai());
defend(new Guatu());
}
}
Demo.defend() 方法可以接受不同风格的 Coach,并根据所传递的参数对象的不同而产生不同的行为,这被称为“策略模式”。
02、适配器模式
适配器模式的思想是,针对调用者的需求对原有的接口进行转接。生活当中最常见的适配器就是HDMI(英语:High Definition Multimedia Interface,中文:高清多媒体接口)线,可以同时发送音频和视频信号。
interface Coach {
void defend();
void attack();
}
// 抽象类实现接口,并置空方法
abstract class AdapterCoach implements Coach {
public void defend() {};
public void attack() {};
}
// 新类继承适配器
class Hesai extends AdapterCoach {
public void defend() {
System.out.println("防守赢得冠军");
}
}
public class Demo {
public static void main(String[] args) {
Coach coach = new Hesai();
coach.defend();
}
}
Coach 接口中定义了两个方法(defend() 和 attack()),如果类直接实现该接口的话,就需要对两个方法进行实现。
如果我们只需要对其中一个方法进行实现的话,就可以使用一个抽象类作为中间件,即适配器(AdapterCoach),用这个抽象类实现接口,并对抽象类中的方法置空(方法体只有一对花括号),这时候,新类就可以绕过接口,继承抽象类,我们就可以只对需要的方法进行覆盖,而不是接口中的所有方法。
03、工厂模式
所谓的工厂模式理解起来也不难,就是什么工厂生产什么,比如说宝马工厂生产宝马,奔驰工厂生产奔驰,A 级学院毕业 A 级教练,C 级学院毕业 C 级教练。
// 教练
interface Coach {
void command();
}
// 教练学院
interface CoachFactory {
Coach createCoach();
}
// A级教练
class ACoach implements Coach {
@Override
public void command() {
System.out.println("我是A级证书教练");
}
}
// A级教练学院
class ACoachFactory implements CoachFactory {
@Override
public Coach createCoach() {
return new ACoach();
}
}
// C级教练
class CCoach implements Coach {
@Override
public void command() {
System.out.println("我是C级证书教练");
}
}
// C级教练学院
class CCoachFactory implements CoachFactory {
@Override
public Coach createCoach() {
return new CCoach();
}
}
public class Demo {
public static void create(CoachFactory factory) {
factory.createCoach().command();
}
public static void main(String[] args) {
// 对于一支球队来说,需要什么样的教练就去找什么样的学院
// 学院会介绍球队对应水平的教练。
create(new ACoachFactory());
create(new CCoachFactory());
}
}
有两个接口,一个是 Coach(教练),可以 command()(指挥球队);另外一个是 CoachFactory(教练学院),能 createCoach()(教出一名优秀的教练)。然后 ACoach 类实现 Coach 接口,ACoachFactory 类实现 CoachFactory 接口;CCoach 类实现 Coach 接口,CCoachFactory 类实现 CoachFactory 接口。当需要 A 级教练时,就去找 A 级教练学院;当需要 C 级教练时,就去找 C 级教练学院。
依次类推,我们还可以用 BCoach 类实现 Coach 接口,BCoachFactory 类实现 CoachFactory 接口,从而不断地丰富教练的梯队。