6.9 抽象类与接口的应用

抽象类与接口是Java中的重要组成元素,本章将通过具体的实例来讲解抽象类与接口的使用。

6.9.1 为抽象类与接口实例化

在Java中可以通过对象的多态性为抽象类和接口实例化,这样再使用抽象类和接口时就可以调用本子类中所覆写过的方法。

【例6.36】为抽象类实例化

程序执行结果:

用同样的方法可以为接口进行实例化。

【例6.37】为接口实例化

程序执行结果:

上面的两组程序通过对象的多态性为抽象类及接口进行实例化,这样所调用的方法就是被子类所覆写过的方法了。

6.9.2 抽象类的实际应用——模板设计

既然可以为抽象类实例化,那么抽象类到底如何使用呢?来看下面的这样一种场景:假设人分为学生和工人,学生和工人都可以说话,但是学生和工人说话的内容是不一样的,说明说话这个功能应该是一个具体功能,而说话的内容就要由学生或工人来决定了,所以此时就可以使用抽象类实现这种场景,如图6-13所示。

图6-13 抽象类应用

【例6.38】抽象类的实际应用

程序执行结果:

从上面程序的结果可以发现,在Person类中就相当于定义了一个模板,在主方法中调用的时候调用的就是普通方法,而子类只需要实现父类中的抽象方法,就可以取得一个具体的信息。

提示

现实生活中的模板。

对于以上的操作代码,如果读者不是很理解,那么可以看以下的说明,小的时候有些读者因为淘气可能会填写过如下的登记表:

上面的一张表如果是空白的,没有任何的意义,但是每一位违纪者都知道自己该在哪个位置上填写对应的内容,填写完整之后此卡片就有意义了,下面就是一个违纪者填写的表格:

如果一个违纪者将以上的违纪卡填写完之后,相关的管理者就可以从这张卡片中取得自己需要的信息,这实际上提供的是一个模板。

6.9.3 接口的实际应用——制定标准

接口是Java解决多继承局限的一种手段,而且从之前讲解读者也已经清楚可以通过对象多态性为接口进行实例化,但是接口在实际中更多的作用是用来制订标准的。例如,U盘和打印机都可以插在计算机上使用,这是因为它们都实现了USB的接口,对于计算机来说,只要是符合了USB接口标准的设备就都可以插进来,如图6-14所示。

图6-14 USB设备

从图6-14中可以清楚地看到,若打印机和U盘都实现了USB接口,则都可以插入计算机,以上的要求可以变为如下程序。

【例6.39】制订USB标准

程序执行结果:

从上面的程序可以清楚地发现,接口就是规定出了一个标准,而计算机认的只是接口,而对于具体的设备计算机本身并不关心。

6.9.4 设计模式——工厂设计

工厂设计是Java开发中使用的最多的一种设计模式,那么什么叫工厂设计?工厂设计有哪些作用呢?在说明问题前,请先观察以下的程序。

【例6.40】观察程序中的问题

程序执行结果:

图6-15 问题解决

上面的程序相信读者都可以看明白,子类为接口实例化后,调用被子类覆写过的方法,但是以上的操作中是否存在问题呢?前面曾经为读者讲解过这样的一个注意事项:主方法实际上就相当于是一个客户端,如果此时需要更换一个子类的话,则肯定是要修改主方法的,那么这实际上就存在了问题。但对于这样的问题该如何解决呢?本书之前介绍过的JVM工作原理:所有的程序只认JVM,每个JVM会根据所在的操作系统不同自动进行匹配,形成了“程序→JVM→操作系统”伪结构,本程序也可以按照此种方式解决,在接口与具体子类之间加入一个过渡端,通过此过渡端取得接口实例,如图6-15所示。

在图6-15中可以清楚地发现,程序在接口和子类之间加入了一个过渡端,通过此过渡端取得接口的实例化对象,一般都会称这个过渡端为工厂类。

【例6.41】工厂设计模式

程序执行结果:

此时,代码是固定了一个apple的字符串,如果使用初始化参数的方式的话,则可以任意选择要使用的子类标记,如主方法修改如下:

这样在运行的时候直接输入以下的命令即可:

程序的运行结果与之前一样,但是取得实例的过程却不太一样,因为接口对象的实例是通过工厂取得的,这样以后如果再有子类扩充,直接修改工厂类客户端就可以根据标记得到相应的实例,灵活性较高。以上程序的执行流程可以用图6-16表示。

图6-16 工厂类的操作流程

说明

提问:为什么在进行字符串判断时要把字符串常量写在前面?

在工厂类中有下面的一段代码:

回答:这样做可以避免空指向异常。

上面的两种形式实际上都没有任何的问题,写成哪个都可以完成功能,但是如果使用了第2种写法,在运行中有可能出现空指向异常,因为传入的className的值有可能为null。下面比较以下两组代码:

实例1:第1组代码

程序执行结果(出错):

实例2:第2组代码

程序执行结果:

因为字符串本身就是一个String的匿名对象,所以永远是一个实例化对象,同样的两个值交换顺序之后就不会出现空指向异常,在以后的开发中建议读者也使用第2种代码的形式编写代码。

6.9.5 设计模式——代理设计

代理设计也是在Java开发中使用较多的一种设计模式,所谓的代理设计就是指一个代理主题来操作真实主题,真实主题执行具体的业务操作,而代理主题负责其他相关业务的处理,就好比在生活中经常使用到的代理上网那样,客户通过网络代理连接网络,由代理服务器完成用户权限、访问限制等与上网操作相关的操作,如图6-17所示。

不管是代理操作也好,真实的操作也好,其共同的目的就是一个上网,所以用户关心的只是如何上网,至于里面是如何操作的用户并不关心,可以得出如图6-18的分析结果。

图6-17 代理上网

图6-18 分析结果

从图6-18可以发现,只需要定义一个上网的接口,代理主题和真实主题都同时实现此接口,然后再由代理操作真实主题即可,上面的要求可以形成如下的代码:

【例6.42】代理操作

程序执行结果:

上面的程序执行流程如图6-19所示。

图6-19 代理操作

从图6-19可以看出,真实主题完成的只是上网的最基本功能,而代理主题要做比真实主题更多的业务相关的操作。

6.9.6 设计模式——适配器设计

对于Java程序来说,如果一个类要实现一个接口,则必须要覆写此接口中的全部抽象方法,如果此时一个接口中定义的抽象方法过多,但是在子类中又用不到这么多抽象方法的话,则使用起来很麻烦,所以此时就需要一个中间的过渡,但是此过渡类又不希望被直接使用,所以将此过渡类定义成抽象类最合适,即一个接口首先被一个抽象类先实现(此抽象类通常称为适配器类),并在此抽象类中实现若干方法(方法体为空),则以后的子类直接继承此抽象类,就可以有选择地覆写所需要的方法,如图6-20所示。

图6-20 实现方式

【例6.43】适配器设计实现

上面的代码实现中因为采用了适配器这个中间环节,所以子类就不用必须实现接口中的全部方法,可以有选择地实现所需要的方法。

提示

在图形界面编程的事件处理中经常使用此设计模式。

在以后学习图形界面部分的时候,读者将看到大量的事件监听接口,如果全部实现方法则肯定不方便,所以在Java中将提供大量的适配器类供用户使用,读者从本程序掌握适配器的基本实现原理即可。

6.9.7 内部类的扩展

在面向对象的基础部分曾经为读者讲解过内部类的概念,实际上在一个抽象类中也可以定义多个接口或抽象类,在一个接口中也可以定义多个抽象类或接口。

【例6.44】在一个抽象类中包含接口

程序执行结果:

上面的程序中在抽象类A中定义了一个内部接口B,之后在抽象类的子类中也声明一个内部类并且实现此内部接口,主方法中按照内部类被外部所访问的格式进行对象的实例化操作。

【例6.45】在一个接口中包含抽象类

程序执行结果:

上面的程序组成结果与上一个很类似,唯一不同的就是在一个接口中定义了一个抽象类而已。

注意

抽象类中可以定义多个内部抽象类,接口中可以定义多个内部接口。

除了在抽象类中定义接口及在接口中定义抽象类之外,对于抽象类来讲也可以在内部定义多个抽象类,而一个接口也可以在内部定义多个接口。

6.9.8 抽象类与接口之间的关系

抽象类和接口在系统设计上都是用得最多的,表6-3中列出了两者的主要概念。

表6-3 抽象类与接口的关系

在类的设计中,一定要明确记住以下的原则:一个类不要去继承一个已经实现好的类,要么继承抽象类,要么实现接口,如果接口和抽象类都可以使用的话,那么优先使用接口,避免单继承局限。

注意

牢记以上的概念。

抽象类和接口在Java中是最重要的概念,这些概念不仅仅在开发中使用,在面试的时候也经常会被问到,所以读者一定要牢记以上的各个区别。

6.9.9 接口定义加强

在Java中,接口是解决多继承的主要手段,并且接口是由抽象方法和全局常量所组成。而这样的设计从JDK 1.8后开始发生了改变,即从JDK 1.8开始可以在接口中定义普通方法(使用default声明)与静态方法(使用static声明)。

技术穿越:关于接口定义加强的产生背景。

实际上JDK 1.8中提供的接口加强定义操作也是为了解决设计上的困境。从JDK 1.0(约1995年)开始到JDK 1.7(约2013年)期间,接口里面就只能够定义抽象方法与全局常量,于是这就产生了一个问题:某一个接口使用非常广泛,并且这个接口已经产生了至少30万个子类,可是突然有一天发现,这个接口设计的功能不足,需要扩充一些新的操作方法,并且这些操作方法对于所有的子类实现都是完全相同的。很明显,如果按照已有的习惯,那么该方法肯定要在所有的子类中重复覆写30万次,这样的设计就显得非常糟糕了。所以从JDK 1.8开始对于接口的定义要求开始放宽,接口里面可以定义抽象方法与静态方法,并且这些方法可以根据子类继承的原则被所有的接口子类继承,那么之前的方法扩充问题就得到了很好的解决。

如果现在要在接口中定义普通方法,必须使用default来进行定义。

【例6.46】定义普通方法

程序执行结果:

本程序在Message接口中定义了一个fun()方法,由于此方法是一个普通方法,所以必须使用default进行声明,同时该方法会自动被MessageImpl子类所继承。

使用default定义普通方法,是需要利用实例化对象明确调用的。如果用户有需要还可以使用static定义方法,这样该方法就可以由接口名称直接调用。

【例6.47】定义static方法

程序执行结果:

上面的程序在Message接口中定义有static方法,这样即使在没有IMessage接口实例化对象时也可以直接通过接口名称进行调用。

提示

开发初期不要考虑以上设计。

对于接口的定义与开发,从笔者的角度来讲,开发初期并不建议这样编写,还应该按照传统的方式,在接口中只定义抽象方法或全局常量,如果开发中确有需要,再考虑使用default或static定义方法。