Java基础:面向对象---接口的本质

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

除了基本的数据类型和类概念,还有一些扩展概念,包括接口、抽象类、内部类和枚举。上一章我们提到,继承有其两面性,替代继承的一种方式是使用接口,接口到底是什么呢?此外,介于接口和类之间,还有一个概念:抽象类,它又是什么呢?一个类可以定义在另一个类内部,称为内部类,为什么要有内部类,它到底是什么呢?枚举是一种特殊的数据类型,它有什么用呢?

为了弄清楚上述问题,将分两个章节进行学习,第一章节为本文,第二章节为《面向对象:类的拓展(之二:抽象类、内部类、枚举的本质)》


在之前的章节中,我们一直在强调数据类型的概念,但只是将对象看作属于某种数据类型,并按该类型进行操作,在一些情况下,并不能反应对象以及对象操作的本质.

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

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

切换到编程的逻辑思维中:类型并不重要,重要的是能力.那如何表示能力呢?接口.

下面,将详细介绍接口,包括其概念、用法、一些细节,以及如何用接口替代继承.

1.接口的概念

接口这个概念在生活中并不陌生,电子世界中一个常见的接口就是USB接口.计算机往往有很多USB接口,可以插各种USB设备,如键盘、鼠标、U盘.

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

扫描二维码关注公众号,回复: 8784532 查看本文章

这里写图片描述

拿上面的USB接口来说,USB协议约定了USB设备需要实现的能力,每个USB设备都需要实现这些能力,计算机使用USB协议与USB设备交互,计算机和USB设备互不依赖,但可以通过USB接口相互交互.那,Java中的接口又是怎样的呢?

2.定义接口

我们通过一个例子来说明Java中接口的概念.这个例子是"比较",很多对象都可以比较,对于求最大值、求最小值、排序的程序而言,它们其实并不会关心对象的类型是什么,只要对象可以比较就可以了,或者说,它们关心的是对象有没有可比较的能力.Java API中提供了Comparable接口,以表示可比较的能力.但本章节主要是讲解接口,我们先自己定义一个Comparable接口,叫MyComparable.

首先来定义这个接口,代码如下:

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

定义接口的代码解释如下:

  1. Java使用interface 这个关键字来声明接口,修饰符一般都是public.
  2. interface 后面就是接口的名字MyComparable.
  3. 接口定义里面,声明了一个方法compareTo,但没有定义方法体,Java 8之前,接口内不能实现方法.接口方法不需要加修饰符,加与不加相当于都是public abstract.

再来解释一下compareTo方法:

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

接口与类不同,它的方法没有实现代码.定义一个接口本身就没有做什么,也没有太大的用处,它还需要至少两个参与者:一个需要实现接口,另一个使用接口.

3.使用接口

类可以实现接口,表示类的对象具有接口所表示的能力.我们以Point 类来说明.我们让Point 具备可以比较的能力,Point之间怎么比较呢?我们假设按照与原点的距离进行比较,Point 类代码如下:

public class Point implements MyComparable {

    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.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 0;
        } else {
            return -1;
        }
    }

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

    public double distance() {
        return Math.sqrt(x * x + y * y);
    }
}

代码解释如下:

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

再来解释一下Point的compareTo实现.

  1. Point不能与其他类型的对象进行比较,它首先检查要比较的对象是否是Point类型,如果不是,使用throw抛出一个异常.
  2. 如果是Point类型,则使用强制类型转换将Object类型的参数other转换为Point类型的参数otherPoint.
  3. 这种显式的类型检查和强制装换是可以通过泛型机制避免的.关于泛型的使用,后面章节会专门介绍.

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

public class Text implements Interface1,Interface2{
    //主体代码
}

定义和实现了接口,接下来我们来看如何使用接口.

4.使用接口

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

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

p1和p2是MyCompareable类型的变量,但引用了Point类型的对象,之所以能赋值是因为Point实现了MyCompareable接口.如果一个类型实现了多个接口,那么这个类型的对象就可以被赋值给任一接口的变量.p1和p2可以调用MyCompareable接口的方法,也只能调用MyCompareable接口的方法,实际执行时,执行的是具体实现类的代码.

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

public class CompUtil {

    public static Object max(MyComparable[] objs) {
        if (objs == null || objs.length == 0) {
            return null;
        }

        MyComparable max = objs[0];
        for (int i = 0; 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++) {
            for (int j = i + 1; j < objs.length; j++) {
                if (objs[i].compareTo(objs[j]) < 0) {
                    MyComparable temp = objs[i];
                    objs[i] = objs[j];
                    objs[j] = temp;
                }
            }
        }
    }   
}

类CompUtil提供了两个方法,max获取传入数组中的最大值,sort对数组升序排序,参数都是MyComparable类型的数组. max的代码是比较容易理解的,不再解释,sort使用的是简单选择排序,其细节我们留待后续文章解释.

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

Point[] points = new Point[]{
                new Point(2, 3),
                new Point(3, 4),
                new Point(1, 2)
};

Log.e("最大值", "init: " + CompUtil.max(points));

CompUtil.sort(points);
Log.e("排序", "init: " + Arrays.toString(points));

以上代码创建了一个Point类型的数组,然后使用CompUtil的max方法获取最大值,使用sort排序.打印结果如下:

这里写图片描述

这里演示的是对Point数组操作,实际上可以针对任何实现了MyComparable接口的类型数组进行操作。这就是接口的威力,可以说,针对接口而非具体类型进行编程,是计算机程序的一种重要思维方式。接口很多时候反映了对象以及对对象操作的本质。它的优点有很多,首先是代码复用,同一套代码可以处理多种不同类型的对象,只要这些对象都有相同的能力,如CompUtil.

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

5.接口的细节
前面介绍了接口的基本内容,接口还有一些细节,包括:

  • 接口中的变量
  • 接口的继承
  • 类的集成与接口
  • instanceof

下面逐一具体介绍.
(1) 接口中的变量
接口中可以定义变量,语法如下所示:

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

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

(2) 接口的继承
接口也可以继承,一个接口可以继承其他接口,继承的基本概念与类一样,但与类不同的是,接口可以有多个父接口(即:多继承),代码如下所示:

public interface IBase1 {
    void method1();
}

public interface IBase2 {
    void method2();
}

public interface IChild extends IBase1, IBase2 {
}

IChild 有IBase1和IBase2 两个父接口,接口的继承同样使用 extends 关键词,多个父接口之间用逗号分隔.

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

public class Child extends Base implements IChild {
 //主体代码
}

关键词extends 要放在implements 之前.

(4) instanceof
与类一样,接口也可以使用instanceof 关键字,用来判断一个对象是否实现了某接口,例如:

Point p = new Point(2,3);
if(p instanceof MyComparable){
   Log.e("instanceof使用", "init: " + "comparable");
}

6.使用接口替代继承
在之前我们提到,可以使用组合和接口替代继承.怎么替代呢?
继承至少有两个好处**,一个是复用代码;另外一个是利用多态和动态绑定统一处理多种子类的对象**.使用组合替代继承,可以复用代码,但不能统一处理.使用接口替代继承,针对接口编程,可以实现统一处理不同类型的对象,但接口没有代码实现,无法复用代码.将组合和接口结合起来替代继承,就既可以统一处理,又可以复用代码了.

我们先增加一个接口IAdd,代码如下:

public interface IAdd {

    void add(int number);
    
    void addAll(int[] numbers);
}

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

public class Base implements IAdd {

    private static final int   MAX_NUM = 1000;
    private              int[] arr     = new int[MAX_NUM];
    private int count;
    
    public void add(int number) {
        if (count < MAX_NUM) {
            arr[count++] = number;
        }
    }
    
    
    public void addAll(int[] numbers) {
        for (int num : numbers) {
            add(num);
        }
    }
}

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

public class Child implements IAdd{

    private Base mBase;

    private long sum;

    public Child() {
        mBase = new Base();
    }

    
    public void add(int number) {
        mBase.add(number);
        sum += number;
    }
    
    
    public void addAll(int[] numbers) {
        mBase.addAll(numbers);
        for (int i = 0; i < numbers.length; i++) {
            sum += numbers[i];
        }
    }

    public long getSum() {
        return sum;
    }
}

Child 复用了Base的代码,又都实现了IAdd接口. 这样,既复用了代码,又可以统一处理,还不担心破坏封装.

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

发布了81 篇原创文章 · 获赞 37 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/gaolh89/article/details/96844804