【Java面向对象】5-9 接口

§5-9 接口

在 [继承](5 - 4 继承.md) 一节的拷贝对象部分中,介绍了克隆对象的方法 clone() 以及重写步骤。其中提及到,类需要实现 Cloneable 接口的同时重写 clone() 方法,这是笔记最早提及接口的一节。上一节介绍了抽象类,本节将重点介绍与之非常相似的接口以及它的特性。

5-9.1 接口的定义

接口的定义:使用关键字 interface 声明接口。

public interface InterfaceName {
    //接口成员
}

实现接口:使用关键字 implements 实现接口。

public class ClassName implements InterfaceName {
    //需要重写接口中的抽象方法
}

在前面的章节已经提过,类只能单继承,但是接口可以多继承、多实现。接口,类似于抽象类,但又有别于抽象类。抽象类中可以有具体实现和抽象方法,但是接口只能含有抽象方法。这样来看,接口可以使得约束和实现分离

由于接口无具体方法实现,因此它往往用于抽象,将某一事物的特性抽象到接口当中。这需要强大的抽象思维能力。接口常常也是设计模式所研究的,实际上就是如何合理地抽象。

5-9.2 接口中的方法

定义好接口后,就可以往接口中放入所需内容。接口含有抽象方法(Java 8 后可以使用 default 关键字修饰的非抽象方法)、staticfinal 变量。

5-9.2.1 抽象方法

现在我们来定义一个接口:

public interface TestInterface {
    void say();
    void run();
}

默认情况下,接口中的方法为 public abstract 类型。

当一个非抽象类实现了该接口,则需要重写其中所有的抽象方法以提供实现:

public class TestInterfaceImpl implements TestInterface {
    @Override
    public void say() {
        System.out.println("Say something.");
    }
    
    @Override
    public void run() {
        System.out.println("Run");
    }
}

但若该类为抽象类,则可以选择不实现接口中的抽象方法。

5-9.2.2 默认方法

自 Java 8 后,接口可以拥有默认方法,用 default 关键字修饰。接口中的默认方法可以有方法的实现。

public interface TestInterface {
    default void say() {
        System.out.println("Say something.");
    }
    default void run() {
        System.out.println("Run");
    }
}

接口中的默认方法在实现类中可以视情况考虑是否重写,若不重写,则默认调用接口中的默认方法。

default 方法只能通过接口的实现类对象来调用。

但是当类继承了多个接口,而接口中有同名的默认方法时,则会报错:

public interface Test {
    default void say() {
        System.out.println("Test interface");
    }
}

public interface Test2 {
    default void say() {
        System.out.println("Test 2 interface");
    }
}

image

这种情况下,有两种解决方法:

解决方法一:在实现类中自行重写该方法:

public class TestImpl implements Test, Test2 {
    public void say() {
        System.out.println("Test interface implementation");
    }
}

解决方法二:利用 super 关键字调用接口的默认方法:

public class TestImpl implements Test, Test2 {
    public void say() {
        Test.super.say();
        //Test2.super.say();
    }
}

5-9.2.3 静态默认方法

接口中另外一种自带方法体的方法称为静态默认方法,由关键字 static 修饰,且必须带有方法体。

public interface Test {
    static void say() {
        System.out.println("Static default method");
    }
}

调用时,只可以通过 接口名.方法名(形参列表) 的方式调用。如:

public class TestImpl implements Test {
    public static void main(String args[]) {
        Test.say();
    }
}

运行结果:

Static default method

5-9.2.4 私有(静态)方法

接口中的方法还可以被 private 修饰符修饰,设置为私有方法。接口中的私有方法必须有方法实现:

public interface Test {
    private void say() {
        System.out.println("Private method of Test interface.");
    }
    private static void print() {
        System.out.println("Private static method of Test interface.");
    }
}

由于 private 访问控制,接口中的私有方法只能由接口中的其他方法调用。而私有静态方法可以由接口中的静态方法调用。

5-9.3 接口中的成员变量

接口中还可以定义一些成员变量:

public interface Test {
    double PI = 3.1415;
}

默认情况下,接口中的成员变量都是 public static final 类型。但一般而言,我们不会在接口中定义常量。

5-9.4 接口的多实现与多继承

首先要说明的是,接口并不是类,而是抽象方法的集合。不同于类,类只允许单继承,而接口既可以多实现,也可以多继承。

[接口的多实现](#5-9.2.2 默认方法)在上文已有所体现,接下来介绍接口的多继承。

public interface EuclideanGeometry extends Geometry, Mathematics

接口的多继承,只需要在关键字 extends 后列出多个要继承的接口,用 , 隔开即可。

一个实现类实现了最底层的子接口,则应当重写所有的抽象方法

且注意:接口中不允许使用 protected 修饰符。

接下来我们来看这个例子:

public interface SuperInterface {
    default void say() {
        Sytstem.out.println("SUper Interface");
    }
}

public interface SubInterface extends SuperInterface {
    default void say() {
        SuperInterface.super.say();		//允许,调用父接口的默认方法
        System.out.println("Sub Interface");
    }
}

public interface SubSubInterface extends SubInterface {
    default void say() {
        SuperInterface.super.say();		//错误:SuperInterface 不是封闭类
        SubInterface.super.say();		//允许,调用父接口的默认方法
        System.out.println("Sub Sub Interface");
    }
}

public class TestImpl implements SubSubInterface {
    public void say() {
        SuperInterface.super.say();		//错误:SuperInterface 不是封闭类
        SubInterface.super.say();		//错误:SubInterface 不是封闭类
        SubSubInterface.super.say();	//允许:调用所实现的接口的默认方法
        System.out.println("Test Implementation");
    }
}

注意:从上述的代码示例可知,接口线性单继承时,实现类只允许调用所实现接口的方法,而具有继承关系的接口只允许调用具有直接继承关系的父接口同名方法。同样地,多继承时,接口只能够直接调用与之有直接继承关系的父接口方法。

5-9.5 标记接口[^ 注1][^ 注2]

标记接口是一种没有任何方法的接口,不含任何方法和属性,仅仅表明实现它的类属于一个特定的类型,供其他代码来测试允许做一些事情。

The tag/marker interface pattern is a design pattern in computer science, used with languages that provide run-time type information about objects. It provides a means to associate metadata with a class where the language does not have explicit support for such metadata.

标签接口模式或标记接口模式(tag/marker interface pattern)是计算机科学中的一个设计模式(design pattern),用于为编程语言提供对象的运行时信息。编程语言不显式支持元数据时,标记接口模式提供了一种将元数据关联到类的方法。

维基百科上对于标记接口的定义。

其作用简单而言就是给某个对象打上一个标签,使对象拥有某个或某些特权。在[拷贝对象](5 - 4 继承.md)一节中我们就遇到了第一个标记接口 Cloneable,Java 还有其他的标记接口,例如:

java.io.Serializable;
java.lang.Cloneable;
java.util.RandomAccess;
java.rmi.Remote;

以熟悉的 Cloneable 接口为例,实现了 Cloneable 接口的类,则可以重写 Object.clone() 方法(protected)以实现按字段复制该类的实例。在没有实现 Cloneable 接口的类中尝试调用该方法可能会抛出 CloneNotSupportedException 异常。

若一个类实现了 Serializable 接口,表示这个类可以被序列化。因此,我们实际上是通过 Serializable 这个接口给类标记上了 “可被序列化” 的元数据。这就是标记/标签接口名字的由来。

标记接口的目的

  1. 建立一个公共的父接口。

    例如 EventListener 接口,在源码中其表述如下:

    package java.util;
    
    /**
     * A tagging interface that all event listener interfaces must extend.
     * @since 1.1
    */
    public interface EventListener {
    }
    

    查询该接口的层次结构,可以发现,该接口有许多子接口。 EventListener 是一个由几十个其他接口扩展的 API,可以使用一个标记接口建立一组接口的父接口。

  2. 向一个类添加数据类型。

    这是标记接口的最初目的,由于标记接口没有任何方法,实现标记接口的类不需要定义接口任何的方法,但是该类可以通过多态性变成一个接口类型。

5-9.6 注解[^ 注3]

Java 注解(Annotation),又称为 Java 标注,是 JDK 5.0 引入的一种注释机制。有关注解的更多内容,在《注解和反射》学习。

5-9.7 接口小结

在本节末尾,我们来简单总结接口的有关内容。

类与接口

  1. 接口不是类,没有构造器,无法被实例化,只能被类实现,被其他接口继承;
  2. 一个类可以实现多个接口,一个接口可以继承多个接口;
  3. 具体类只有具体方法,抽象类二者皆有,而接口只有规范,由不同类实现

接口中的方法

  1. 默认情况下,接口中的方法为 public abstract 方法,实现了该接口的类必须重写接口中所有抽象方法(除非实现类为抽象类);
  2. 接口中的方法还可以是 staticdefaultprivateprivate default 方法,这些方法必须在接口中提供方法体;
  3. 实现了多个接口,多个接口中存在同名的默认方法,实现类必须重写该方法;
  4. 接口中方法的访问修饰符只支持 privatepublic ,由 private 修饰的方法仅能由该接口中其他方法调用;
  5. 具有继承关系的接口只能调用与之具有直接继承关系的父接口方法;
  6. 在实现类中,静态方法可通过 接口名.方法名(形参列表) 调用;
  7. 在实现类中,非静态方法可通过 接口名.super.方法名(形参列表) 调用;
  8. 在具有多实现的实现类中,来自不同接口的重名方法只需要重写一次即可;
  9. 实现了一个位于最底层的子接口的实现类,应当重写接口继承关系中所有的抽象方法;
  10. 有且只有一个抽象方法的接口称为函数式接口,可以使用 lambda 表达式简化。

接口中的成员变量

  • 接口中的成员变量只有常量,且默认情况下为 public static final 类型;

结语

总而言之,接口是一种规范,体现的是一组规则,可以使得实现和约束分离。接口最能体现对对象的抽象。设计模式所研究的,实际上就是如何合理地抽象。

5-9.8 接口的多态与适配器设计模式

接口的多态:当一个方法的参数是一个接口时,则可以传递这个接口的所有实现类对象,这种方式称为接口的多态。

设计模式(design pattern):设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解,保证代码可靠性、程序重用性。

一共有 23 中常用的设计模式。可以将设计模式简单地理解为各种套路。

适配器设计模式:解决接口与接口实现类之间的矛盾问题。

假设我们现有一个接口:

public interface Inter {
    void method1();
    void method2();
    void method3();
    void method4();
    void method5();
}

在实现类 InterImpl 中,若我们只需要 method5() 方法,由于接口的要求,我们不得不重写其中包含 method5() 的所有抽象方法,这样会使得代码阅读起来比较不便。

这时我们可以考虑单独写一个实现类 InterAdapter,将其用做一个适配器,重写接口中所有抽象方法,但对所有方法提供空实现,让原实现类继承该适配器即可,这时就可以在继承类中仅重写 method5() 方法。同时,为了防止外部实例化该适配器,该适配器应当声明为抽象类。即:

public abstract class InterAdapter implements Inter {
    @Override
    public void method1() {}
    @Override
    public void method2() {}
    @Override
    public void method3() {}
    @Override
    public void method4() {}
    @Override
    public void method5() {}
}

5-4.9 组合优于继承

面向对象的一大特性就是使用继承。但是在面向对象编程中,有一条非常经典的设计原则就是 “组合优于继承,多用组合少用继承” 。

为什么不推荐使用继承?

但是值得注意的是,自然界中并非所有事物都能够简单地用一种继承关系来表示(实现),按照不同的标准,就会有多种不同的继承关系。在一个系统当中,仅仅使用一种继承关系往往并不能很好地满足需求。而采用多种标准实现继承关系,就会产生多种不同的排列组合,造成继承关系变得臃肿复杂。这种继承结构会导致类的耦合程度加深,尤其在大型项目当中变得难以维护。

这时候,就避免使用继承。可以将这种多种不同标准的关系划分抽象成一个个的接口,接口中只提供这种关系的方法。实现类只需要重写接口中的方法即可。若多个实现类所重写同一接口的同一方法的具体实现完全一致,则可以将接口中的该方法定义为默认方法,或再定义一个专门实现该接口的专用实现类,让专用实现类组合于其中即可(组合 + 委托)。

继承和组合,这两种方式实际上都提供了不同的代码复用的实现方式。

并非是完全不用继承

但这并不意味着继承完全一无是处,也不意味着组合就没有缺陷。当继承结构趋于不变,且继承结构简单(层次较浅)时,可以使用继承,因为这是倾向于使用多态的形式提供一种代码复用方案。但是,若继承结构复杂、继承结构易变,则应该考虑使用组合的方式替代继承。

在实际开发中,应当考虑项目的特性从而选择组合或者继承。

该部分内容提炼自站内博文:

在设计原则中,为什么反复强调组合要优于继承? - Kevin.ZhangCG - 博客园 (cnblogs.com)

5-9.A 附录:脚注

[^ 注1]: java中的标记接口(标签接口) - yanggb - 博客园 (cnblogs.com)
[^ 注2]: Java 接口 | 菜鸟教程 (runoob.com)
[^ 注3]: Java 注解(Annotation) | 菜鸟教程 (runoob.com)

posted @ 2023-05-01 17:38  Zebt  阅读(35)  评论(0)    收藏  举报