相信用过Java的人对接口(interface)都不会陌生,每天写代码都可能会用到它。但是呢写了几年Java代码后,还是觉得对接口的理解不是很深刻,一来是对“接口”这个名字比较纠结,我们平时会听到很多电脑硬件上面的接口,比如USB接口、VGA接口等等,直观地讲接口是就用来“插”的,但是软件里面怎么会有接口,这个接口怎么“插”呢?二来是不是很清楚接口的实际意义是什么,在Java里面经常会看到很多地方会要求我们的类去实现一个接口,这样的要求的目的是什么呢?最近在学习Java虚拟机相关的知识,对这些问题有了更深的理解,写出来跟大家分享一下。

首先我们来谈一谈硬件上的接口。电脑主机不可能把所有设备都装进去,而且用户还需要根据实际的需求来选择使用一些附加设备,所以电脑的主机里面往往只会包含计算机系统必需的和最常用的硬件设备,其他的附加设备放在主机外面,这里就需要一些接口来把这些外部设备和主机连接起来,比如USB,VGA,HDMI,RJ45等等,这里的接口简单直观来讲就是连接不同设备的口子,就叫做“接口”,这里是很好理解的。但是接口往更深层来讲,还会包含另外一个概念,就是协议(protocol)或者说规范(specification)。对于程序员而言,我们会接触到很多协议/规范,比如TCP/IP、HTTP、SMTP,这里面的“P”就是protocol(协议),还有比如The Java Virtual Machine Specification、HDMI 2.1 Specification、USB4 Specification等等规范。几乎每一种接口都有与之对应的规范,那接口和规范有什么联系呢?我们以USB接口作为案例来讲一讲。USB的全称是通用串行总线(Universal Serial Bus),USB最大的特性就是这个通用性(Universal),换句话说就是各种各样的设备都能插上去用。比如什么U盘,鼠标,键盘,摄像头等等,这些设备功能不同,内部结构不同,由不同的设备厂家生产的,他们之间存在很多的差异,那为什么都能插到USB上面去用呢,这里就是协议/规范起了作用。每一个接口都会定义一套严密的规范,不管是什么设备那个厂家生产,只要大家严格遵循这样一套规则,对外暴露一套统一的接口,那么就能够很好得屏蔽内部的差异性,实现很好的兼容性,这里就有一点类似于编程里面“封装”(encapsulation)的概念。封装说简单点就是不管你方法里面是怎么实现的,对于使用者而言,只要理解了方法的输入和输出就可以了。那么对于硬件接口而言,不管你是什么设备,内部是什么结构,只要用户知道这个设备的接口是该插到主机那个口子里面就可以使用了,USB接口就插USB,HDMI就插HDMI。

我们了解了硬件的接口,再来看看软件的接口。我们先看一个简单的接口使用的案例:

public class Main {

    public static void main(String[] args) {

        Student[] students = {new Student("Tom", 80), new Student("Judy", 30)
                , new Student("Bob", 50), new Student("Jay", 20)};

        Arrays.sort(students);

        System.out.println(Arrays.toString(students));
    }
}

class Student implements Comparable<Student> {
    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public int compareTo(Student other) {
        return this.score - other.score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", score=" + score +
                '}';
    }
}

 

很简单的代码,就是按照成绩高低对学生进行排序打印。因为java.util.Arrays里的sort(java.lang.Object[])方法规定数组里面的所有的元素必须都要实现java.lang.Comparable接口,所以Student类实现了Comparable接口的,重写了compareTo( )方法。这是Java里面最简单不过的代码了,但是我们来想想为什么这里要有实现接口的规定呢?要实现sort()方法需要知道两个要素,一个是一套高效的排序算法,二是排序的对象。对于实现JDK的工程师而言,在实现sort()方法时算法往往是可以确定的,只要从这些林林总总的排序算法中选一种合适的实现就可以了,但是对于排序的对象这个时候是无法确定的,这个要素只要在使用sort()方法时才能够确定,我们这里是按学生的成绩排序,也可以按学生的身高排序,也可以按体重排序等等,显然在实现sort()时这个要素是无法实现的,在无法确定这个要素的情况里我们怎么来实现这个sort()方法呢,这就是接口的作用。我们就可以通过定义一个接口来抽象地描素这个不确定的要素,虽然我们不知道这个对象是什么,但是我们知道我们要的是什么,这样我们就可以定义一套规则来对它进行约束。在java.lang.Comparable接口中定义compareTo()方法,这个方法虽然没有方法体,但是它定义了输入输出参数,方法的注释里面还详细写明了这个方法的实现细节要求,通过这些信息我们就能够对这个不确定的要素做一个详细的“画像”。在实现sort()方法时,我们可以通过调用接口方法来暂时规避这个不确定的要素。比如在这里的sort()方法的排序最终的实现是在java.util.ComparableTimSort 的 binarySort(Object[] a, int lo, int hi, int start)里面,里面有这样的两行代码:

                       Comparable pivot = (Comparable) a[start];

                       if (pivot.compareTo(a[mid]) < 0)

这里就调用了数组元素的compareTo()方法来比较两个元素的大小。在我们使用sort()方法时,我们通过复写接口的方法来指明具体的比较对象。当然接口的使用还要得益于Java的多态性,多态性怎么来理解呢,比如上面代码,第一行代码把元素的类型强制转换成Comparable,第二行代码去调用了compareTo()方法,显然我们知道在代码运行期间,这里会去执Student里面复写的compareTo()方法,而不是Comparable接口里面的方法,因为Comparable接口里面的方法是空的,多态性总结起来就是动态链接,真正要被调用的方法取决于实例类型而不是引用类型,说直白一点引用类型调用只是占个位置,申明一下调用的意图,但是真正要执行的代码是运行时实例类型里面的方法。

通过上面的例子我们可以看出,因为我们在实现代码的时候存在一些不确定要素,这些要素只能在使用这段代码时才能确定,接口就用来对这个不确定要素做一些约束。换句话说,定义Java接口也就是在定义一套协议/规范,类比于USB协议;在代码里调用接口方法,就是把接口暴露出来,就类比于主机上面的USB插槽;实现接口也就是在按照接口的规定生成一个实体类,类比于设计一个U盘;在程序运行时,再通过多态性把接口方法的调用链接到实现类方法上面,就类比于把U盘插到USB插槽上面。现在的电脑都支持USB热插拔,这里热插拔就跟Tomcat的热加载是一个意思,Tomcat的热部署主要得益于Java的类动态加载机制。这样我们就理解了为什么接口叫做“接口”,以及接口有什么用的问题了。