上次我们整理了抽象类与基于抽象类的模板设计模式,这篇文章我们来整理一下接口以及接口所能干的一些事情。
当可以灵活使用抽象类和接口进行设计的时候,基本就表示面向对象的概念理解得差不多了,而到这一步需要大量代码累计。
1.接口基本定义
抽象类与普通类相比,最大的好处是实现对子类方法覆写的控制,但是在抽象类中可能依然会有一些普通方法,而普通方法中就可能会涉及到一些安全或者隐私的操作问题,如果我们在开发之中要想对外部隐藏全部的实现细节,就需要使用接口来实现。
接口可以理解为一个纯粹的抽象类,最原始的定义接口之中是只包含有抽象方法与全局常量的,但是从JDK1.8开始,由于引用了Lambda表达式的概念,所以接口的定义也得到了加强,除了抽象方法与全局常量之外还可以定义普通方法或静态方法。但从设计本身的角度来考虑,接口中的组成应该是一抽象方法和全局常量为主。
在Java中,接口主要使用interface关键字来定义,我们来举一个例子:
interface IMessage{//定义接口
public static final String INFO = "www.csdn.com"; //全局常量
public abstract String getInfo(); //抽象方法
}
这里要注意,由于接口与普通类的名称相同会混淆,所以一般我们在定义接口的时候在类名称前加一个大写的I。
同时问题也很明显了,此时的接口并不能直接实例化对象,所以对于接口的使用原则如下:
- 接口需要被子类实现,使用implements关键字来实现接口,一个子类可以实现多个父类接口。
- 子类(如果不是抽象类)那么一定要覆写接口中的全部抽象方法。
- 接口可以利用子类向上转型进行实例化。
接下来我们来定义一个子类并实现接口:
class MessageImpl implements IMessage{
public String getInfo() {
return "实现接口";
}
}
然后我们尝试实例化一个对象:
public class first {
public static void main( String args[] ){
IMessage msg = new MessageImpl();
System.out.println(msg.getInfo());
System.out.println(IMessage.INFO);
}
}
编译之后没有问题,运行结果如下:
实现接口
www.csdn.com
如果我们要实现多个接口,比如我们要实现一个确定getInfo()方法能否实现的接口,我们可以这样写:
interface IChannel{
public abstract boolean connect();//定义是否可以实现
}
然后在MessageImpl类中覆写它的方法,并将返回的布尔值作用于getInfo()方法是否实现上:
class MessageImpl implements IMessage, IChannel{
public boolean connect() {
System.out.println("成功,可以进行接口实现");
return true;
}
public String getInfo() {
if(this.connect()) {
return "实现接口";
}
return "未能实现";
}
}
现在我们再来运行上述代码,结果如下:
成功,可以进行接口实现
实现接口
www.csdn.com
这里可以看到,我们使用 (,)来将两个接口连接在同一个类中,这样就弥补了Java中只能进行单继承的遗憾。
但是我们也要开始考虑一个问题:关于对象的转型问题。
我们知道MessageImpl类上面有IMessage接口与IChannel接口,实例化的MessageImpl对象也同时是IMessage与IChannel的对象,但同时MessageImpl类还有一个默认的父类:Object类,这样子MessageImpl对象不但同时是IMessage与IChannel的对象,并且是Object类的对象。此时,MessageImpl子类的对象可以实现任意父接口的转换。
我们来简单的看一下:
public class first {
public static void main( String args[] ){
IMessage msg = new MessageImpl();
IChannel chl = (IChannel) msg;
System.out.println(chl.connect());
}
}
在这段代码中,我们将msg通过(IChannel)转换为 IChannel 的对象 chl,同时打印chl的connect()方法。
按照常理来说,IMessage接口与IChannel接口是相互独立的两个类,不可能直接转换,但是当我们编译之后,运行结果如下:
成功,可以进行接口实现
true
这个时候我们要知道,由于MessageImpl类实现了IMessage接口与IChannel接口,所以这个子类就可以看作这两个接口的任意一个子类,这是就可以表示为两个接口互相转换,但仅仅存在于这两个接口,因为这两个接口被一个子类实现了。
同样的,在Java中接口是不允许去继承父类的,所以接口绝对不会是Object的子类,但是根据我们刚才所发现MessageImpl类是Object的子类,Object可以接收所有的数据类型,所以接口就一定可以通过Object来接收。
我们来看一下:
public class first {
public static void main( String args[] ){
IMessage msg = new MessageImpl();
Object obj = msg;
IChannel chl = (IChannel) obj;
System.out.println(chl.connect());
}
}
结果如下:
成功,可以进行接口实现
true
Object类对象可以接受所有数据类型,包括基本数据类型、类对象、接口对象、数组。
由于接口描述的是一个公共的定义标准,所以在接口之中所有的抽象方法的访问权限都是public,写与不写都是一样的,比如下面这两段代码的意义是一样的:
interface IMessage{//定义接口
public static final String INFO = "www.csdn.com"; //全局常量
public abstract String getInfo(); //抽象方法
}
interface IMessage{//定义接口
String INFO = "www.csdn.com"; //全局常量
String getInfo(); //抽象方法
}
方法不写访问权限也是public,不是default,所以在覆写的时候只能使用public。
我们要注意的是,一个抽象类可以实现多个接口,而一个普通类只能继承一个抽象类并且可以实现多个接口,但是要求先继承后实现。我们来举一个例子:
interface IChannel{//定义一个接口
public boolean connect();
}
abstract class DatabaseMessage{//定义一个抽象类
public abstract boolean getDatabaseConnect();
}
当我们编译的时候就会报错,因为在接口中 abstract 是可以省略的,但是在抽象类中不可以。
这个抽象类被子类继承时应该在接口的前面:
interface IChannel{
public abstract boolean connect();//定义是否可以实现
}
abstract class DatabaseMessage{
public abstract boolean getDatabaseConnect();
}
class MessageImpl extends DatabaseMessage implements IMessage, IChannel{
public boolean connect() {
System.out.println("成功,可以进行接口实现");
return true;
}
public String getInfo() {
if(this.connect()) {
return "实现接口";
}
return "未能实现";
}
public boolean getDatabaseConnect() {
return true;
}
}
注意子类是先继承抽象类,再实现各种接口的。
虽然一个接口无法去继承一个父类,但是可以通过 extends 来实现多个父接口,此时称为接口多继承,只有在接口时才能使用 extends 来进行多继承,而子类只能进行单继承。我们来举一个例子:
interface IMessage{//定义接口
public abstract String getInfo(); //抽象方法
}
interface IChannel{
public abstract boolean connect();
}
interface IService extends IMessage, IChannel{
public abstract String getServiceInfo();
}
当我们使用一个子类来实现IService接口的时候,我们也要同时覆写IMessage、IChannel接口中的抽象方法。
在实际的开发中,接口使用往往有三类形式:
- 进行标准设置
- 表达一种操作能力
- 暴露远程方法视图,一般在RCP分布式开发中使用
2.接口定义加强
我们知道接口是由抽象方法和全局常量组成,但如果项目设计不当就有可能出现一些问题。
假如我们有一个接口,这个接口在只有一个子类的时候,子类覆写方法是非常方便的;但是,如果随着时间的发展,使用这个接口的子类越来越多,假如有1000个子类都在使用,那么这个时候如果突然间原来的接口中某个方法需要更新,或者添加了一个新的方法,那么在这个过程中,所有的1000个子类都需要重新修改方法,或者添加新的覆写方法,一共1000遍,并且这100遍是一样的过程一直在重复。
这就非常可怕了同学们,这时候往往是因为对于接口的设计不当造成的,而在刚开始使用一个接口的时候,没有人敢保证以后永远不会再修改它,所以我们在这种情况下,往往不会让子类直接继承接口,而是选择在中间过渡一个抽象类。因为抽象类是不能直接实例化对象的,所以当接口中出现新的方法时,我们通过抽象类来实现,然后所有的子类都只需要实例化抽象类的方法就可以了。
但是在JDK1.8之后为了解决接口设计的缺陷,在接口中允许开发者定义普通方法。我们来举个例子:
interface IMessage{//定义接口
public String message();
public boolean connect();
}
class MessageImpl implements IMessage{
public String message() {
return "www.csdn.com";
}
}
public class first {
public static void main( String args[] ){
IMessage msg = new MessageImpl();
}
}
这时候可以看到,我们为了能够使代码正常编译,必须要在MessageImpl类中覆写connect方法,但是就如刚才所说,如果有1000个子类的话,我们就要覆写1000遍,所以我们在JDK1.8之后就可以这样:
public default boolean connect() {
System.out.println("这是一个新方法");
return true;
}
我们使用 default 关键字来定义普通方法,那么如何使用呢?
public class first {
public static void main( String args[] ){
IMessage msg = new MessageImpl();
if(msg.connect()) {
System.out.println(msg.message());
}
}
}
结果如下:
这是一个新方法
www.csdn.com
可以看到,我们在子类中并没有添加任何代码,但可以像使用抽象类中的普通方法一样来使用这个普通方法。
需要注意的是:接口中的普通方法必须追加 default 关键字,同时我们要意识到,这个功能属于补充挽救的功能,所以如果不是在必须的情况下,不能作为设计的首选。
同时除了可以追加普通方法以外,在接口中也可以定义static方法了,而static方法就可以通过接口直接调用了,例如:
interface IMessage{//定义接口
public String message();
public default boolean connect() {
System.out.println("这是一个新方法");
return true;
}
public static IMessage getInstance() {
return new MessageImpl();
}
}
class MessageImpl implements IMessage{
public String message() {
if(this.connect()) {
return "www.csdn.com";
}
return "错误的调用";
}
}
这个时候我们就可以:
public class first {
public static void main( String args[] ){
IMessage msg = IMessage.getInstance();
System.out.println(msg.message());
}
}
编译后运行结果如下:
这是一个新方法
www.csdn.com
或许大家已经注意到了,无论是普通方法还是static方法,如果我们都在接口中可以实现,某种意义上来说,这个接口已经可以取代抽象类了;但是我们也强调过,这两种方法都是作为补充挽救的功能,不应该将这些功能作为接口的主要设计功能,我们还是要坚持一个原则:在接口中以抽象方法和全局常量为主。
3.使用接口的定义标准
对于接口而言在开发之中最为重要的应用就是进行标准的制定,实际上在日常生活中也会经常见到很多接口的名词,以USB插口为例,USB插口就是一个接口,它制定了USB的接口标准,在日常生活中,所有与USB接口有关的东西,实际上只是知道了USB的接口标准而已,它们只关心这个即将要插进来的东西有没有USB接口,或者说有没有USB的标准,而对于这个标准的具体实现类,无论是鼠标、键盘、U盘、移动硬盘,甚至是USB小灯、USB风扇等,只要遵循了USB标准都可以插入。
接下来我们举一个例子:
interface IUSB{
public boolean check();//检查,使可以工作
public void work();//工作
}
class Computer{//定义一个电脑类
public void plugin(IUSB usb) {//插入了一个带有USB的对象
if(usb.check()) {//先检查是否可以工作
usb.work();//可以,开始工作
}
}
}
class Keyboard implements IUSB{
public boolean check() {
return true;
}
public void work() {
System.out.println("开始打字!");
}
}
class Print implements IUSB{
public boolean check() {
return false;
}
public void work() {
System.out.println("开始打印!");
}
}
public class first {
public static void main( String args[] ){
Computer computer = new Computer();//实例化一个电脑
computer.plugin(new Keyboard());//给电脑插入一个键盘
computer.plugin(new Print());//给电脑插入一个打印机
}
}
上述代码中,我们首先定义了一个USB的接口,然后再Computer类中实现了接口的标准定义:检查,然后工作。接下来我们定义了Keyboard类和Print类并给他们设置了USB的接口内容,然后实例化了一个Computer,并在Computer中插入了Keyboard和Print,当我们运行程序的时候,就是我们在Computer中使用Keyboard和Print的时候。
运行结果如下:
开始打字!
我们会看到只有一行输出,是因为我们的Print在check()方法中返回的是false,所以Computer不会让Print进行工作。当我们对代码进行完善,考虑到false的情况时,将Computer类改写为:
class Computer{//定义一个电脑类
public void plugin(IUSB usb) {//插入了一个带有USB的对象
if(usb.check()) {//先检查是否可以工作
usb.work();//可以,开始工作
}else {
System.out.println("检查中发现硬件设备出现了问题,安装失败!");
}
}
}
再次运行,结果如下:
开始打字!
检查中发现硬件设备出现了问题,安装失败!
而在现实开发中,这种标准无处不在,比如高速路上对速度的标准,餐厅对食物的标准等等。
4.工厂设计模式
对接口而言,已经很明确的必须有子类,并且子类可以通过对象可以向上转型来获取接口的实例化对象,但在进行对象实例化的过程中也可能会存在设计问题。
让我们举一个非常简单的例子:
interface IFood{//定义一个食物的标准
public void eat();
}
class Bread implements IFood{
public void eat(){
System.out.println("吃面包!");
}
}
public class first {
public static void main( String args[] ){
IFood food = new Bread();
food.eat();
}
}
这是一个非常简单的例子,在这个程序中根据接口对子类的定义,并且利用对象的向上转型进行接口对象的实例化。
这个程序的结构是这个样子的:
主类就是我们的客户端,接口就是一项标准,如果需要一项实例,那么就new一个:需要面包,客户端就new了一个面包,需要牛奶,那么客户端就应该再new一个牛奶。这里就有一个小问题,客户端如果需要一个食物,那他就可以得到一个食物,但是客户端只关心食物本身是否能通过eat()方法实现吃这个动作,而不关心食物是怎么来的。
这就出现了如果我们需要实现喝牛奶这个举动,就要改变代码:
interface IFood{//定义一个食物的标准
public void eat();
}
class Bread implements IFood{
public void eat(){
System.out.println("吃面包!");
}
}
class Milk implements IFood{
public void eat(){
System.out.println("喝牛奶!");
}
}
public class first {
public static void main( String args[] ){
IFood food = new Milk();
food.eat();
}
}
大家可以发现代码因为我们的需求变成了喝牛奶,所以添加了一个Milk类,最关键的一点是我们的客户端改变了,从IFood food = new Bread(); 变成了 IFood food = new Milk(); 这样就暴露了一个问题,那就是代码的耦合,而造成耦合问题的元凶就是关键字new。
以JVM设计为例,Java实现可移植性的关键在于JVM,而JVM的核心原理就是利用一个虚拟机来运行所有的Java程序,所有的程序并不与具体的操作系统有任何关系,所有的程序都有JVM来进行匹配,所以得出结论:良好的设计应该避免耦合,如何避免?我们使用工厂设计模式来实现。
我们在代码中添加一个新的工厂类:
class Factory{
public static IFood getInstance(String className) {
if("bread".equals(className)) {
return new Bread();
}else if("milk".equals(className)) {
return new Milk();
}else {
return null;
}
}
}
再将主类改为:
public class first {
public static void main( String args[] ){
IFood food = Factory.getInstance("bread");
food.eat();
}
}
这样我们只需要改变 Factory.getInstance()中的参数,就可以new不同的对象了,至于参数可以使用下列方式:
public class first {
public static void main( String args[] ){
Scanner in = new Scanner(System.in);
String thefood = in.next();
IFood food = Factory.getInstance(thefood);
food.eat();
}
}
我们来输入一个食物试一下:
bread
吃面包!
在这段代码中,我们的客户端(也就是主类)并没有与IFood类有任何的关联,所有的关联都是通过Factory类完成的,而在程序运行时就可以通过初始化参数来对使用的子类进行定义。
在刚才我们谈到程序的结构时,提到客户端只关心食物本身是否能通过eat()方法实现吃这个动作,而不关心食物是怎么来的,现在客户端并不需要关注食物本身,只需要关注Factory类就可以了,而Factory类关注着所有的食物与获取途径。如果在日后需要进行子类扩充,那就只需要对Factory类进行扩充就可以了,这就是所谓的工厂设计模式。
5.代理设计模式
代理模式的主要功能是帮助用户将所有的开发注意力只集中在核心业务功能的处理上,就像是我们肚子饿了,关心的是如何吃到东西:
代理设计模式的作用就是来关注一系列的问题,比如:需要准备什么吃的、如何加工、如何送给你吃、吃完后收拾碗筷等等问题,而我们作为一个吃的窗口,只需要关心吃什么就可以了,当我们的客户端(也就是主类)再这个程序中,也只需要管理“代理”就可以管理所有的事物以及管理我们吃了什么。
在代码中实现代理设计模式:
interface IEat{//定义吃的标准
public void get();//吃就只需要知道得到的东西能不能吃
}
class EatReal implements IEat{//定义一个真实的吃东西的类
public void get() {
System.out.println("得到一份食物,开始吃!");
}
}
class EatProxy implements IEat{//定义一个服务代理
private IEat eat;//为了吃而服务
public EatProxy(IEat eat) {//首先要有一个代理项,为谁代理
this.eat = eat;
}
public void get() {//方法覆写
this.prepare();//准备
this.eat.get();//开始吃,这里的吃只是代理
this.clear();//清洁
}
public void prepare() {//准备过程
System.out.println("1.购买食材!");
System.out.println("2.处理食材!");
System.out.println("3.烹饪食材!");
}
public void clear() {//清洁过程
System.out.println("5.清理餐桌!");
}
}
public class first {
public static void main( String args[] ){
IEat eat = new EatProxy(new EatReal());//这里最好使用工厂设计模式
eat.get();
}
在这段代码中,我们一共有三个角色,分别是:
- 一个吃的接口,用来定义吃的标准。
- 一个真实的吃东西的类,会做出行为:"得到一份食物,开始吃!"
- 一个代理,扮演了餐馆的角色,分别管理了:准备、处理、烹饪以及清洁的任务,当然还有将食物代理给真实的吃东西的类。
在这一切都准备好之后,我们的客户端(主类)一旦实例化了一个对象,那么这个对象就只需要做出它的行为:"得到一份食物,开始吃!",而剩下的一切都会交给代理来处理,我们运行程序,结果如下:
1.购买食材!
2.处理食材!
3.烹饪食材!
得到一份食物,开始吃!
5.清理餐桌!
代理设计模式的主要特点是:一个接口提供有两个子类,其中一个子类是真实业务操作类,另一个是代理业务操作类,没有代理业务操作,真实业务无法执行。
6.接口与抽象类的区别
最后提一下接口与抽象类的区别,主要的区别在我前面一篇文章中讲过:Java——抽象类与接口,感兴趣的同学可以去看一下。
今天真的写了好多呢,之前因为是系统的整理,所以一些基础的知识点和简单的程序都比较容易,今天整理的工厂设计模式与代理设计模式是非常重要的两个关于接口的设计模式,以后会经常用到,大家一定要熟练掌握,我们下次见👋