适配器是一种中间部件,用于连接两个功能件,用现实生活中的物件来进行来比的话,就是我们经常用到的各种转换头,比如现在大多数轻薄笔记本都不再提供VGA接口,因为VGA接口比较大,纤薄的机身没法容纳这种类型的接口,因而多提供HDMI甚至是Type-c接口,而现存的大多数显示器都是不支持HDMI接口,只支持VGA接口,因此我们想要将笔记本连接显示器构建双屏工作环境的时候,HDMI转VGA转接头是必不可少的。
回到工程中,我们在写代码的时候,时长会遇到一些情况,我们需要使用的接口A中,缺少了一些我们需要的方法,而这个方法恰好在接口B中是存在的,这个时候,我们当然有多个方法来解决这个问题。
(1).我们可以在接口A中增加这个方法,但这么做显然是存在一些问题的,接口A可能是专门定义一类操作的接口,例如A可能定义了飞行这么一个操作,而我现在需要吃东西的方法,如果在接口A上加上这么一个方法,那接口A的实现类飞机、导弹也必须实现吃东西这个方法,这显然是不合适的。
(2).我们可以直接在接口A的实现类中加上这个方法,这样做的话自然是避免了上面的问题,不过这样做的话不符合面向接口编程的原则,没有将实现与接口分离,这也不是一种好的选择。
(3).我们可以找到一个接口B,从接口B中找出我们想要的方法,并为A构建一个适配器,从而使接口A的实现类可以间接的使用接口B的方法。
适配器模式主要有三种类型,分别是类适配器、对象适配器和接口适配器,其中,类适配器和对象适配器比较类似,接口适配器则与上述两种差别较大。
1.类适配器
类适配器的实现方法是通过继承,适配器需要实现接口A,并继承接口B,并在接口A中的方法调用接口B中需要的方法。
具体实例如下:
HMDI接口:
Public interface HDMI{
Void connect();
}
VGA接口:
Public interface VGA{
Void connect();
}
VGA接口的实现类:
Public class VGAImpl implements VGA{
@Override
public void connect(){
System.out.print("可以连接到VGA接口的显示器");
}
}
HDMI适配器:
Public class HDMIAdapter extends VGAImpl implements HDMI{
@Override
public void connect(){
super.connect();
}
}
适配器的测试:
Public class Main{
public static void main(String[] args){
HDMIAdapter hdmiAdapter = new HDMIAdapter();
hdmiAdapter.connect();
}
}
测试结果:
可以连接到VGA接口的显示器
2.对象适配器
对象适配器的实现方法是通过对象的组合来实现适配器功能,和第一种方法不同的是,第二种方法在适配器中传入接口B实现类的对象,从而间接的调用接口B中的方法;而第一种方法则是通过继承,然后直接通过其父类方法来间接调用。
我们同样使用上面的HMDI和VGA接口为例,HDMI和VGA接口相关的代码同上,我们这里只给出适配器的代码:
Public class HDMIAdapter2 implements HDMI{
private VGA vga;
public HDMIAdapter2 (VGA vga){
this.vga=vga;
}
@Override
public void connect(){
vga.connect();
}
}
测试结果也是同上面一样
3.接口适配器
假设我们的手机坏了,我们需要对手机进行维修的时候,可能需要吸盘、镊子、小型螺丝刀,而摆在我们身边有一个大的工具箱,里面的工具应有尽有,这种时候,我们需要把工具箱里所有的工具都拿出来吗?显然不要,我们只要将我们需要用到的几种工具拿出来就可以了,这就是接口适配器的功能,我们定义一个工具箱接口的抽象类,所有接口方法中的实现都不进行任何操作,这个抽象类就是接口适配器,当我们需要维修手机时,我们只需要实现吸盘、镊子、小型螺丝刀这几个方法就可以了。下面我们再以动物为例来看看接口适配器的作用。
action接口:
Public interface Action{
void fly();
void run();
void swim();
void eat();
void talk();
}
action接口中定义了一系列的动作,例如飞行、跑、游泳、说话等
action适配器:
Public abstract class ActionAdapter implements Action{
@Override
public void fly(){}
@Override
public void run(){}
@Override
public void swim(){}
@Override
public void eat(){}
@Override
public void talk(){}
}
bird实现类:
public class Bird extends ActionAdapter{
@Override
public void fly() {
System.out.println("I can fly!");
}
@Override
public void run() {
System.out.println("I can run!");
}
@Override
public void eat() {
System.out.println("I can eat!");
}
}
测试类:
public class Main {
public static void main(String[] args){
Bird bird = new Bird();
System.out.println("I am a bird!");
bird.fly();
bird.run();
bird.eat();
}
}
测试结果:
I am a bird!
I can fly!
I can run!
I can eat!
Process finished with exit code 0
适配器适合用在什么地方?
适配器通常是用在复用代码的地方,比如我现在需要实现一个功能,但是呢,已经有人帮我实现类似的功能了,这样的话我就可以把他的方法拿过来,适配成我要用的样子。当然这么说有点抽象,举一个很实在的例子,比如我们想要打印日志,我们就不需要自己取实现一套日志系统,因为现在市面上已经有非常多很好用的日志工具了,例如log4j,slf4j等等,但是我们直接用它这个工具好不好呢,假如我们在工程中使用了log4j,那日志代码肯定是分布在各个类中,当有一天我们需要将log4j更换成slf4j的时候,灾难降临了,我们需要在无数行代码中找到log4j的代码,然后将它修改成slf4j的代码,想一想都是一件无法接收的事情。这种时候,我们怎么来设计我们的日志工具比较好呢?一个比较好的方案就是,我们在自己的工程中定义一个Log接口,然后针对我们需要使用的第三方日志工具写一个对应日志的适配器,这样在我们自己工程中写的日志代码都是基于我们自己的log接口的方法,当我们需要换一个新的第三方日志工具时只需要写一个新的日志适配器然后引入工程中即可,而工程中原有的日志代码一行都不需要修改,你看,适配器的使用是不是可以大大减少我们的工作量呢?