容器中的设计模式
一、 迭代器模式
Collection 实现了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection
从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for (String item : list) {
System.out.println(item);
}
二、迭代器模式的详细介绍
定义:提供一种方法访问一个容器对象中各个元素,而又不暴露该对象的内部细节。
类型:行为类模式
类图:
特别地,为了更好地理解上面的类图,我们以此为契机,介绍一下类图的几个知识点:
- 类图分为三部分,依次是类名、属性、方法;
- 以<<开头和以>>结尾的为注释信息;
- 修饰符+代表public,-代表private,#代表protected,什么都没有代表包可见;
- 带下划线的属性或方法代表是静态的。
类元之间关系 :类元之间的关系有关联、泛化、各种形式的依赖关系,包括实现关系和使用关系
1. 依赖(Dependence)
依赖关系的定义为:对于两个相对独立的对象,当一个对象负责构造另一个对象的实例,或者依赖另一个对象的服务时,这两个对象之间主要体现为依赖关系。定义比较晦涩难懂,但在java中的表现还是比较直观的:类A当中使用了类B,其中类B是作为类A的方法参数、方法中的局部变量、或者静态方法调用。类上面的图例中:People类依赖于Book类和Food类,Book类和Food类是作为类中方法的参数形式出现在People类中的。
代码样例:
public class People{
//Book作为read方法的形参
public void read(Book book){
System.out.println(“读的书是”+book.getName());
}
}
2.关联(Association)、
、
单向关联:
双向关联:
对于两个相对独立的对象,当一个对象的实例与另一个对象的一些特定实例存在固定的对应关系时,这两个对象之间为关联关系。关联关系分为单向关联和双向关联。在java中,单向关联表现为:类A当中使用了类B,其中类B是作为类A的成员变量。双向关联表现为:类A当中使用了类B作为成员变量;同时类B中也使用了类A作为成员变量。
代码样例:
public class Son{
//关联关系中作为成员变量的类一般会在类中赋值
Father father = new Father();
public void getGift(){
System.out.println(“从”+father.getName()+”获得礼物”);
}
}
public class Father{
Son son = new Son();
public void giveGift(){
System.out.println(“送给”+son.getName()+“礼物”);
}
}
3.聚合(Aggregation)
聚合关系是关联关系的一种,耦合度强于关联,他们的代码表现是相同的,仅仅是在语义上有所区别:关联关系的对象间是相互独立的,而聚合关系的对象之间存在着包容关系,他们之间是“整体-个体”的相互关系。
代码样例:
public class People{
Car car;
House house;
//聚合关系中作为成员变量的类一般使用set方法赋值
public void setCar(Car car){
This.car = car;
}
public void setHouse(House house){
This.house = house;
}
public void driver(){
System.out.println(“车的型号:”+car.getType());
}
public void sleep(){
System.out.println(“我在房子里睡觉:”+house.getAddress());
}
}
5.继承(Generalization)
继承表示类与类(或者接口与接口)之间的父子关系。在java中,用关键字extends表示继承关系。UML图例中,继承关系用实线+空心箭头表示,箭头指向父类。
6.实现(Implementation)
表示一个类实现一个或多个接口的方法。接口定义好操作的集合,由实现类去完成接口的具体操作。在java中使用implements表示。UML图例中,实现关系用虚线+空心箭头表示,箭头指向接口。
迭代器模式的结构
- 抽象容器(Aggretate):一般是一个接口,提供一个iterator()方法,例如java中的Collection接口,List接口,Set接口等。
- 具体容器(ConcreteAggretate):就是抽象容器的具体实现类,比如List接口的有序列表实现ArrayList,List接口的链表实现LinkList,Set接口的哈希列表的实现HashSet等。
- 抽象迭代器(Iterator):定义遍历元素所需要的方法,一般来说会有这么三个方法:取得第一个元素的方法first(),取得下一个元素的方法next(),判断是否遍历结束的方法isDone()(或者叫hasNext()),移出当前对象的方法remove(),
迭代器实现(ConcretateIterator):实现迭代器接口中定义的方法,完成集合的迭代。
迭代器模式的代码实现
//抽象迭代器
interface Iterator {
public Object next();
public boolean hasNext();
}
//具体迭代器,
class ConcreteIterator implements Iterator{
private List list = new ArrayList();
private int cursor =0;
public ConcreteIterator(List list){
this.list = list;
}
//实现了是否遍历结束
public boolean hasNext() {
if(cursor==list.size()){
return false;
}
return true;
}
//返回下一个元素
public Object next() {
Object obj = null;
if(this.hasNext()){
obj = this.list.get(cursor++);
}
return obj;
}
}
//抽象容器
interface Aggregate {
public void add(Object obj);
public void remove(Object obj);
public Iterator iterator();
}
//具体容器
class ConcreteAggregate implements Aggregate {
private List list = new ArrayList();
public void add(Object obj) {
list.add(obj);
}
public Iterator iterator() {
return new ConcreteIterator(list);
}
public void remove(Object obj) {
list.remove(obj);
}
}
public class Client {
public static void main(String[] args){
Aggregate ag = new ConcreteAggregate();
ag.add("小明");
ag.add("小红");
ag.add("小刚");
Iterator it = ag.iterator();
while(it.hasNext()){
String str = (String)it.next();
System.out.println(str);
}
}
}
## 三、适配器模式
java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
@SafeVarargs
public static <T> List<T> asList(T... a)
如果要将数组类型转换为 List 类型,应该注意的是 asList() 的参数为泛型的变长参数,因此不能使用基本类型数组作为参数,只能使用相应的包装类型数组。
Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
也可以使用以下方式生成 List。
List list = Arrays.asList(1,2,3);
四、适配器模式详解
4.1、现实生活中的适配器
适配器模式是23中设计模式之一,它的主要作用是在新接口和老接口之间进行适配。它非常像我们出国旅行时带的电源转换器 ,我们国家的电器使用普通的扁平两项或三项插头,而去外国的话,使用的标准就不一样了,比如德国,使用的是德国标准,是两项圆头的插头。如果去德国旅游,那么我们使用的手机充电器插头无法插到德国的插排中去,那就意味着我们无法给手机充电。怎样解决这个问题呢?只要使用一个电源转化器就行了。如下图所示:
该适配器下面的插头符合德国标准,可以插到德国的插排中去,上面提供的接口符合国标,可以供我们的手机充电器使用。
4.2、实现电源适配器
下面我们使用代码来表述适配器模式:
代码中有两个接口,分别为德标接口和国标接口,分别命名为DBSocketInterface和GBSocketInterface,此外还有两个实现类,分别为德国插座和中国插座,分别为DBSocket和GBSocket。为了提供两套接口之间的适配,我们提供了一个适配器,叫做SocketAdapter。除此之外,还有一个客户端,比如是我们去德国旅游时住的一家宾馆,叫Hotel,在这个德国旅馆中使用德国接口。
德标接口:
/**
* 德标接口
*/
public interface DBSocketInterface {
/**
* 这个方法的名字叫做:使用两项圆头的插口供电
*/
void powerWithTwoRound();
}
德国插座实现德标接口:
/**
* 德国插座
*/
public class DBSocket implements DBSocketInterface{
public void powerWithTwoRound(){
System.out.println("使用两项圆头的插孔供电");
}
}
德国旅馆是一个客户端,它里面有德标的接口,可以使用这个德标接口给手机充电:
/**
* 德国宾馆
*/
public class Hotel {
//旅馆中有一个德标的插口
private DBSocketInterface dbSocket;
public Hotel(){}
public Hotel(DBSocketInterface dbSocket) {
this.dbSocket = dbSocket;
}
//提供的两种注入方式
public void setSocket (DBSocketInterface dbSocket){
this.dbSocket = dbSocket;
}
//旅馆中有一个充电的功能
public void charge(){
//使用德标插口充电
dbSocket.powerWithTwoRound();
}
}
现在写一段代码进行测试:
public class Test {
public static void main(String[] args) {
//初始化一个德国插座对象, 用一个德标接口引用它
DBSocketInterface dbSoket = new DBSocket();
//创建一个旅馆对象,并且宾馆里面有德国接口的插座
Hotel hotel = new Hotel(dbSoket);
//在旅馆中给手机充电
hotel.charge();
}
}
现在我去德国旅游,带去的三项扁头的手机充电器。如果没有带电源适配器,我是不能充电的,因为不可能为了我一个旅客而为我更改墙上的插座,更不可能为我专门盖一座使用中国国标插座的宾馆。因为人家德国人一直这么使用,并且用的挺好,俗话说入乡随俗,我就要自己想办法来解决问题。对应到我们的代码中,也就是说,上面的Hotel类,DBSocket类,DBSocketInterface接口都是不可变的(由德国的客户提供),如果我想使用这一套API,那么只能自己写代码解决。
下面是国标接口和中国插座的代码。
国标接口:
/**
* 国标接口
*/
public interface GBSocketInterface {
/**
* 这个方法的名字叫做:使用三项扁头的插口供电
*/
void powerWithThreeFlat();
}
中国插座实现国标接口:
/**
* 中国插座
*/
public class GBSocket implements GBSocketInterface{
@Override
public void powerWithThreeFlat() {
System.out.println("使用三项扁头插孔供电");
}
}
可以认为这两个东西是我带到德国去的,目前他们还不能使用,因为接口不一样。那么我必须创建一个适配器,这个适配器必须满足以下条件:
1 必须符合德国标准的接口,否则的话还是没办法插到德国插座中;
2 在调用上面实现的德标接口进行充电时,提供一种机制,将这个调用转到对国标接口的调用 。
这就要求:
1 适配器必须实现原有的旧的接口
2 适配器对象中持有对新接口的引用,当调用旧接口时,将这个调用委托给实现新接口的对象来处理,也就是在适配器对象中组合一个新接口。
public class SocketAdapter
implements DBSocketInterface{ //实现旧接口
//组合新接口
private GBSocketInterface gbSocket;
/**
* 在创建适配器对象时,必须传入一个新街口的实现类
* @param gbSocket
*/
public SocketAdapter(GBSocketInterface gbSocket) {
this.gbSocket = gbSocket;
}
/**
* 将对就接口的调用适配到新接口
*/
@Override
public void powerWithTwoRound() {
gbSocket.powerWithThreeFlat();
}
}
这个适配器类满足了上面的两个要求。下面写一段测试代码来验证一下适配器能不能工作,我们按步骤一步步的写出代码,以清楚的说明适配器是如何使用的。
1 我去德国旅游,带去的充电器是国标的(可以将这里的GBSocket看成是充电器)
GBSocketInterface gbSocket = new GBSocket();
2 来到德国后, 找到一家德国宾馆住下 (这个宾馆还是上面代码中的宾馆,使用的依然是德国标准的插口)
Hotel hotel = new Hotel();
3 由于没法充电,我拿出随身带去的适配器,并且将我带来的充电器插在适配器的上端插孔中。这个上端插孔是符合国标的,我的充电器完全可以插进去。
SocketAdapter socketAdapter = new SocketAdapter(gbSocket);
4 再将适配器的下端插入宾馆里的插座上
hotel.setSocket(socketAdapter);
5 可以在宾馆中使用适配器进行充电了
hotel.charge();
public class TestAdapter {
public static void main(String[] args) {
GBSocketInterface gbSocket = new GBSocket();
Hotel hotel = new Hotel();
SocketAdapter socketAdapter = new SocketAdapter(gbSocket);
hotel.setSocket(socketAdapter);
hotel.charge();
}
}
适配器模式就是不改变原有的接口,但是还可以使用新接口功能;