# 接口的本质

> 转载：[Java编程的逻辑 (19) - 接口的本质](https://www.cnblogs.com/swiftma/p/5588010.html)

## 1. 数据类型的局限

之前我们一直在说，程序主要就是数据以及对数据的操作，而为了方便操作数据，高级语言引入了数据类型的概念，Java定义了八种基本数据类型，而类相当于是自定义数据类型，通过类的组合和继承可以表示和操作各种事物或者说对象。

**但，这种只是将对象看做属于某种数据类型，并按该类型进行操作，在一些情况下，并不能反映对象以及对对象操作的本质。**

为什么这么说呢？很多时候，\*\*我们实际上关心的，并不是对象的类型，而是对象的能力，只要能提供这个能力，类型并不重要。\*\*我们来看一些生活中的例子。

要拍个照片，很多时候，只要能拍出符合需求的照片就行，至于是用手机拍，还是用Pad拍，或者是用单反相机拍，并不重要，关心的是对象是否有拍出照片的能力，而并不关心对象到底是什么类型，手机、Pad或单反相机都可以。

要计算一组数字，只要能计算出正确结果即可，至于是由人心算，用算盘算，用计算器算，用电脑软件算，并不重要，关心的是对象是否有计算的能力，而并不关心对象到底是算盘还是计算器。

要将冷水加热，只要能得到热水即可，至于是用电磁炉加热，用燃气灶加热，还是用电热水壶，并不重要，重要的是对象是否有加热水的能力，而并不关心对象到底是什么类型。

\*\*在这些情况中，类型并不重要，重要的是能力。\*\*那如何表示能力呢？

## 2. 接口的概念

Java使用接口这个概念来表示能力。

接口这个概念在生活中并不陌生，电子世界中一个常见的接口就是USB接口。电脑往往有多个USB接口，可以插各种USB设备，可以是键盘、鼠标、U盘、摄像头、手机等等。

接口声明了一组能力，但它自己并没有实现这个能力，它只是一个约定，它涉及交互两方对象，一方需要实现这个接口，另一方使用这个接口，但**双方对象并不直接互相依赖，它们只是通过接口间接交互**。图示如下：

![2021-03-21-65sVaL](https://image.ldbmcs.com/2021-03-21-65sVaL.jpg)

拿上面的USB接口来说，USB协议约定了USB设备需要实现的能力，每个USB设备都需要实现这些能力，电脑使用USB协议与USB设备交互，电脑和USB设备互不依赖，但可以通过USB接口相互交互。

下面我们来看Java中的接口。

## 3. 定义接口

我们通过一个例子来说明Java中接口的概念。

这个例子是"比较"，很多对象都可以比较，对于求最大值、求最小值、排序的程序而言，它们其实并不关心对象的类型是什么，只要对象可以比较就可以了，或者说，它们关心的是对象有没有可比较的能力。Java API中提供了Comparable接口，以表示可比较的能力，但它使用了**泛型**，而我们还没有介绍泛型，所以本节，我们自己定义一个Comparable接口，叫MyComparable。

现在，首先，我们来定义这个接口，代码如下：

```java
public interface MyComparable {
    int compareTo(Object other);
}
```

解释一下：

* Java使用interface这个关键字来声明接口，修饰符一般都是public。
* interface后面就是接口的名字MyComparable。
* 接口定义里面，声明了一个方法compareTo，但没有定义方法体，接口都不实现方法。接口方法不需要加修饰符，加与不加都是public的，不能是别的修饰符。

再来解释一下compareTo方法：

* 方法的参数是一个Object类型的变量other，表示另一个参与比较的对象。
* 第一个参与比较的对象是自己
* 返回结果是int类型，-1表示自己小于参数对象，0表示相同，1表示大于参数对象

接口与类不同，它的方法没有实现代码。定义一个接口本身并没有做什么，也没有太大的用处，它还需要至少两个参与者，一个需要实现接口，另一个使用接口，我们先来实现接口。

## 4. 实现接口

\*\*类可以实现接口，表示类的对象具有接口所表示的能力。\*\*我们来看一个例子，以前面介绍过的Point类来说明，我们让Point具备可以比较的能力，Point之间怎么比较呢？我们假设按照与原点的距离进行比较，下面是Point类的代码：

```java
public class Point implements MyComparable {
    private int x;
    private int y;
    
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public double distance(){
        return Math.sqrt(x*x+y*y);
    }

    @Override
    public int compareTo(Object other) {
        if(!(other instanceof Point)){
            throw new IllegalArgumentException();
        }
        Point otherPoint = (Point)other;
        double delta = distance() - otherPoint.distance();
        if(delta<0){
            return -1;
        }else if(delta>0){
            return 1;
        }else{
            return 0;
        }
    }

    @Override
    public String toString() {
        return "("+x+","+y+")";
    }
}
```

我们解释一下：

* Java使用implements这个关键字表示实现接口，前面是类名，后面是接口名。
* 实现接口必须要实现接口中声明的方法，Point实现了compareTo方法。

我们再来解释一下Point的compareTo实现：

* Point不能与其他类型的对象进行比较，它首先检查要比较的对象是否是Point类型，如果不是，使用throw抛出一个异常，异常我们还没提到，后续文章讲解，此处可以忽略。
* 如果是Point类型，使用强制类型转换将Object类型的参数other转换为Point类型的参数otherPoint。
* 这种显式的类型检查和强制转换是可以使用泛型机制避免的，后续文章我们再介绍泛型。

**一个类可以实现多个接口，表明类的对象具备多种能力**，各个接口之间以逗号分隔，语法如下所示：

```java
public class Test implements Interface1, Interface2 {

....

}
```

定义和实现了接口，接下来我们来看怎么使用接口。

## 5. 使用接口

与类不同，接口不能new，不能直接创建一个接口对象，对象只能通过类来创建。但可以声明接口类型的变量，引用实现了接口的类对象。比如说，可以这样：

```java
MyComparable p1 = new Point(2,3);
MyComparable p2 = new Point(1,2);
System.out.println(p1.compareTo(p2));
```

p1和p2是MyComparable类型的变量，但引用了Point类型的对象，之所以能赋值是因为Point实现了MyComparable接口。如果一个类型实现了多个接口，那这种类型的对象就可以被赋值给任一接口类型的变量。

p1和p2可以调用MyComparable接口的方法，也只能调用MyComparable接口的方法，实际执行时，执行的是具体实现类的代码。

为什么Point类型的对象非要赋值给MyComparable类型的变量呢？在以上代码中，确实没必要。但在一些程序中，代码并不知道具体的类型，这才是接口发挥威力的地方，我们来看下面使用MyComparable接口的例子。

```java
public class CompUtil {
    public static Object max(MyComparable[] objs){
        if(objs==null||objs.length==0){
            return null;
        }
        MyComparable max = objs[0];
        for(int i=1;i<objs.length;i++){
            if(max.compareTo(objs[i])<0){
                max = objs[i];
            }
        }
        return max;
    }
    
    public static void sort(MyComparable[] objs){
        for(int i=0;i<objs.length;i++){
            int min = i;
            for(int j=i+1;j<objs.length;j++){
                if(objs[j].compareTo(objs[min])<0){
                    min = j;
                }
            }
            if(min!=i){
                 MyComparable temp = objs[i];
                 objs[i] = objs[min];
                 objs[min] = temp;
            }
        }
    }
}
```

类CompUtil提供了两个方法，max获取传入数组中的最大值，sort对数组升序排序，参数都是MyComparable类型的数组。max的代码是比较容易理解的，不再解释，sort使用的是简单选择排序。

可以看出，这个类是针对MyComparable接口编程，它并不知道具体的类型是什么，也并不关心，但却可以对任意实现了MyComparable接口的类型进行操作。我们来看下对Point类型进行操作，代码如下：

```java
Point[] points = new Point[]{
        new Point(2,3),
        new Point(3,4),
        new Point(1,2)
};
System.out.println("max: " + CompUtil.max(points));
CompUtil.sort(points);
System.out.println("sort: "+ Arrays.toString(points));
```

创建了一个Point类型的数组points，然后使用CompUtil的max方法获取最大值，使用sort排序，并输出结果，输出如下：

```java
max: (3,4)
sort: [(1,2), (2,3), (3,4)]
```

这里演示的是对Point数组操作，实际上可以针对任何实现了MyComparable接口的类型数组进行操作。

这就是接口的威力，可以说，\*\*针对接口而非具体类型进行编程，是计算机程序的一种重要思维方式。\*\*针对接口，很多时候反映了对象以及对对象操作的本质。它的优点有很多，首先是代码复用，同一套代码可以处理多种不同类型的对象，只要这些对象都有相同的能力，如CompUtil。

\*\*更重要的是降低了耦合，提高了灵活性，使用接口的代码依赖的是接口本身，而非实现接口的具体类型，程序可以根据情况替换接口的实现，而不影响接口使用者。\*\*解决复杂问题的关键是分而治之，分解为小问题，但小问题之间不可能一点关系没有，分解的核心就是要降低耦合，提高灵活性，接口为恰当分解，提供了有力的工具。

## 6. 接口的细节

上面我们介绍了接口的基本内容，接口还有一些细节，包括：

* 接口中的变量
* 接口的继承
* 类的继承与接口
* instanceof

我们逐个来介绍下。

### 6.1 接口中的变量

接口中可以定义变量，语法如下所示：

```java
public interface Interface1 {
    public static final int a = 0;
}
```

这里定义了一个变量int a，修饰符是`public static final`，但这个修饰符是可选的，即使不写，也是public static final。这个变量可以通过"接口名.变量名"的方式使用，如Interface1.a。

### 6.2 接口的继承

接口也可以继承，一个接口可以继承别的接口，继承的基本概念与类一样，但与类不同，接口可以有多个父接口，代码如下所示：

```java
public interface IBase1 {
    void method1();
}

public interface IBase2 {
    void method2();
}

public interface IChild extends IBase1, IBase2 {
}
```

接口的继承同样使用extends关键字，多个父接口之间以逗号分隔。

### 6.3 类的继承与接口

类的继承与接口可以共存，换句话说，类可以在继承基类的情况下，同时实现一个或多个接口，语法如下所示：

```java
public class Child extends Base implements IChild {

 //...

}
```

extends要放在implements之前。

### 6.4 instanceof

与类一样，接口也可以使用`instanceof`关键字，用来判断一个对象是否实现了某接口，例如：

```java
Point p = new Point(2,3);
if(p instanceof MyComparable){
    System.out.println("comparable");
}
```

## 7. 使用接口替代继承

上节我们提到，可以使用接口替代继承。怎么替代呢？

我们说继承至少有两个好处，一个是复用代码，另一个是利用多态和动态绑定统一处理多种不同子类的对象。

使用组合替代继承，可以复用代码，但不能统一处理。使用接口，针对接口编程，可以实现统一处理不同类型的对象，但接口没有代码实现，无法复用代码。将组合和接口结合起来，就既可以统一处理，也可以复用代码了。我们还是以上节的例子来说明。

我们先增加一个接口IAdd，代码如下：

```java
public interface IAdd {
    void add(int number);
    void addAll(int[] numbers);
}
```

修改Base代码，让他实现IAdd接口，代码基本不变：

```java
public class Base implements IAdd {

//...

}
```

修改Child代码，也是实现IAdd接口，代码基本不变：

```java
public class Child implements IAdd {

 //...

}
```

这样，就既可以复用代码，也可以统一处理，而且不用担心破坏封装了。

## 8. 小结

本节我们谈了数据类型思维的局限，提到了很多时候关心的是能力，而非类型，所以引入了接口，介绍了Java中接口的概念和细节，**针对接口编程是一种重要的程序思维方式，这种方式不仅可以复用代码，还可以降低耦合，提高灵活性，是分解复杂问题的一种重要工具。**

接口没有任何实现代码，而之前介绍的类都有完整的实现，都可以创建对象，Java中还有一个介于接口和类之间的概念，抽象类，它有什么用呢？
