• Java编程的逻辑 (19)


    数据类型的局限

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

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

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

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

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

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

    在这些情况中,类型并不重要,重要的是能力。那如何表示能力呢?

    接口的概念

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

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

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


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

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

    定义接口

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

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

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

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

    解释一下:

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

    再来解释一下compareTo方法:

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

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

    实现接口

    类可以实现接口,表示类的对象具有接口所表示的能力。我们来看一个例子,以前面介绍过的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;
        }
        
        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。
    • 这种显式的类型检查和强制转换是可以使用泛型机制避免的,后续文章我们再介绍泛型。

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

    public class Test implements Interface1, Interface2 {
    
    ....
    
    }

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

    使用接口

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

    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接口的例子。

    复制代码
    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类型进行操作,代码如下:

    复制代码
    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排序,并输出结果,输出如下:

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

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

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

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

    接口的细节

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

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

    我们逐个来介绍下。

    接口中的变量

    接口中可以定义变量,语法如下所示:

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

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

    接口的继承

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

    复制代码
    public interface IBase1 {
        void method1();
    }
    
    public interface IBase2 {
        void method2();
    }
    
    public interface IChild extends IBase1, IBase2 {
    }
    复制代码

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

    类的继承与接口

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

    public class Child extends Base implements IChild {
    
     //...
    
    }

    extends要放在implements之前。

    instanceof

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

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

    使用接口替代继承

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

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

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

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

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

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

    public class Base implements IAdd {
    
    //...
    
    }

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

    public class Child implements IAdd {
    
     //...
    
    }

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

    小结

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

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

  • 相关阅读:
    推荐一个采用方便程序员在线动画学习常用算法的良心网站
    你的ABAP程序给佛祖开过光么?来试试Jerry这个小技巧
    我在德国做SAP CRM One Order redesign工作的心得
    我做SAP CRM One Order redesign的一些心得体会
    一个最简单的WebSocket hello world demo
    推荐一个好用的以多tab标签方式打开windows CMD的工具
    SAP CX Upscale Commerce : SAP全新推出的电商云平台
    TCP socket和web socket的区别
    SAP 前端技术的演化史简介
    Fiori Fundamentals和SAP UI5 Web Components
  • 原文地址:https://www.cnblogs.com/ivy-xu/p/12387374.html
Copyright © 2020-2023  润新知