Java 编程思想看完, 重新强化了一下Java基础之后, 觉得功力有点增加了, 现在可以考虑看设计模式了.
果然, 现在发现可以很容易的就看明白了, 不像几个月以前感觉还是看天书一样. 这次就结合图解设计模式这本书来过一遍.
在看的过程中发现这本书写的相当早, 是在2001年就成书了, 其中的一些Java代码估计还是非常老的版本, 这次也按照自己的想法更新一下其中的代码.
- Iterator 迭代器模式
- Java提供的迭代器模式接口
- 练习
- Adapter 适配器模式 - 继承
- Adapter 适配器模式 - 委托
- 练习
Iterator模式
迭代器模式现在很容易就看懂了, 一个持有其他的类的对象有一个方法返回一个迭代器对象, 就可以了.
先来看书上的实现, 一个书架持有多个书对象, 然后实现一个接口, 返回迭代器对象, 而迭代器对象实现迭代器接口规定的.next()和.hasNext()方法就可以了.
这本书里的模式和设计体现的很好, 几乎所有的设计模式都是先搭接口, 再用实现类, 接口之间的关系其实就是设计模式了, 实现类其实就是模式的具体实现.
先看两个接口, 即类的集合的接口和迭代器的接口:
//集合的接口
public interface Aggregate {
Iterator iterator();
}
//迭代器的接口
public interface Iterator {
boolean hasNext();
Object next();
}
原书这里的代码在接口里还用了 public abstract 修饰, 按照现在的Java接口规范已经没有必要了. 还使用了Object, 在之后强制转型, 实际上在有了内部类之后, 其实可以更方便的使用内部类来编写迭代器了.
之后是数据对象, 集合和迭代器的实现:
//数据对象Book, 很简单
public class Book {
private String name;
public Book(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
'}';
}
}
//书架, 作为持有书对象的一个集合
public class BookShelf implements Aggregate {
//这个方法很重要, 要返回对于自身的迭代器对象
@Override
public Iterator iterator() {
return new BookShelfIterator(this);
}
private int last = 0;
private Book[] books;
public BookShelf(int length) {
books = new Book[length];
}
public Book getBookAt(int index) {
return this.books[index];
}
public void appendBook(Book book) {
if (last == this.books.length) {
System.out.println("书架已满, 放入失败");
return;
}
books[last] = book;
last++;
}
//这个方法实际上是暴露给迭代器对象, 用于控制迭代器工作的
public int getLength() {
return this.last;
}
}
public class BookShelfIterator implements Iterator {
//要根据一个书架对象生成对应的迭代器, 所以在构造器中传入一个书架对象
private BookShelf bookShelf;
BookShelfIterator(BookShelf bookShelf) {
this.bookShelf = bookShelf;
this.index = 0;
}
//然后需要有一个遍历书架的内容的变量
private int index;
//这个方法的关键是判断是否还能继续取下一个, 当索引小于书架内书的数量的时候, 就说明可以来取出书对象
@Override
public boolean hasNext() {
return index < bookShelf.getLength();
}
//这个是实际取出下一个
@Override
public Object next() {
return bookShelf.getBookAt(index++);
}
}
使用迭代器的方法, 就是先获取之, 然后通过判断.hasNext()是否为真, 来取出下一个对象:
public class Main {
public static void main(String[] args) {
BookShelf bookShelf = new BookShelf(10);
for (int i = 0; i < 15; i++) {
bookShelf.appendBook(new Book(i + ""));
}
Iterator bookshelfIterator = bookShelf.iterator();
while (bookshelfIterator.hasNext()) {
System.out.println(bookshelfIterator.next());
}
}
}
这里可以发现, 每一个集合的迭代器, 都必须包含一个这个集合的对象才能够具体工作, 在这里是通过给迭代器的构造器传入一个集合引用来做到的.
但实际上在现代Java里, 哪个东西可以隐含一个指向另外一个对象的引用呢, 就是非静态内部类了. 每一个非静态内部类对象都有一个引用指向外围类.
所以可以改写一下, 用一个书架对象加上内部类就可以搞定了, 而且代码更加优雅, 语义也更加明显, 迭代器就是和当前的对象绑定的, 而且也实现了对应的接口:
public class BookShelfWithInnerClass implements Aggregate {
//返回内部类对象
@Override
public Iterator iterator() {
return new BookShelfInnerIterator();
}
private int last = 0;
private Book[] books;
public BookShelfWithInnerClass(int length) {
books = new Book[length];
}
public Book getBookAt(int index) {
return this.books[index];
}
public void appendBook(Book book) {
if (last == this.books.length) {
System.out.println("书架已满, 放入失败");
return;
}
books[last] = book;
last++;
}
public int getLength() {
return this.last;
}
public class BookShelfInnerIterator implements Iterator {
private int index;
BookShelfInnerIterator() {
index = 0;
}
@Override
public boolean hasNext() {
return index < last;
}
@Override
public Book next() {
//红字部分是对外围类的引用, 其实也可以省略, 直接调用外围类方法即可.
return BookShelfWithInnerClass.this.getBookAt(index++);
}
}
}
然后可以测试一下带有内部类迭代器的集合对象:
public static void main(String[] args) {
//测试内部类
BookShelfWithInnerClass bookShelfWithInnerClass = new BookShelfWithInnerClass(10);
for (int j = 0; j < 8; j++) {
bookShelfWithInnerClass.appendBook(new Book(j + ""));
}
Iterator innerIterator = bookShelfWithInnerClass.iterator();
while (innerIterator.hasNext()) {
System.out.println(innerIterator.next());
}
}
Java内部的一些带有迭代器的集合对象, 也是使用了该模式. 如果要让增强for语句对我们自己编写的类产生效果, 就不是继承我们自己的接口, 而是Java提供的接口才可以了.
Java提供的迭代器模式接口
在例子中我们自己编写了Aggregate接口用于规范集合一定要提供一个迭代器, 然后编写了Iterator接口, 规定了一定要提供.hasNext()和.next()方法.
可见只要实现这两个接口, 就可以完成迭代器模式. Java对于我们自己编写的这两个接口也提供了两个对应的接口, 实现这两个接口之后, Java的增强for语句就能够作用于我们的代码.
这两个接口分别是:
public interface Iterable<T>
, 位于java.lang中, 其中规定了方法: Iterator<T> iterator()
public interface Iterator<E>
, 位于java.util中, 其中规定了方法: boolean hasNext()
和E next()
方法
两个接口都支持泛型. 现在想让我们的书架集合被增强for支持, 我们只要将自己的类继承的接口换成这两个就可以:
import java.util.Iterator;
//这里由于我们给定的就是Book对象, 因此不使用泛型参数, 直接固定死类型, 就继承Iterable<Book>接口
public class BookShelfForEachSupported implements Iterable<Book> {
@Override
//不需要泛型所以这里类型就定好了
public Iterator<Book> iterator() {
return new BookShelfInnerIterator();
}
private int last = 0;
private Book[] books;
public BookShelfForEachSupported(int length) {
books = new Book[length];
}
public Book getBookAt(int index) {
return this.books[index];
}
public void appendBook(Book book) {
if (last == this.books.length) {
System.out.println("书架已满, 放入失败");
return;
}
books[last] = book;
last++;
}
public int getLength() {
return this.last;
}
//内部迭代器类继承Iterator<Book>接口, 实现对应方法
public class BookShelfInnerIterator implements Iterator<Book> {
private int index;
BookShelfInnerIterator() {
index = 0;
}
@Override
public boolean hasNext() {
return index < last;
}
@Override
public Book next() {
return BookShelfForEachSupported.this.getBookAt(index++);
}
}
}
这里的泛型要和增强for语句中使用的泛型一致, 来测试一下:
public static void main(String[] args) {
//测试增强for对我们编写的书架类的支持
BookShelfForEachSupported bookShelfForEachSupported = new BookShelfForEachSupported(10);
for (int j = 0; j < 8; j++) {
bookShelfForEachSupported.appendBook(new Book(j + ""));
}
for (Book book : bookShelfForEachSupported) {
System.out.println(book);
}
}
一切OK, 迭代器模式就搞定了. 我又阅读了一下其他的文章, 包括Java 编程思想, 现在还是比较推荐使用内部类的方式来使用迭代器. 而且都是推荐直接使用外部类的方法返回内部类对象, 而无需直接创建内部类对象.
练习
练习的要求是使用ArrayList作为书架类持有对象的内部集合. 则书架类等于了一个ArrayList的包装, 修改代码也很容易:
import java.util.ArrayList;
import java.util.Iterator;
public class BookShelfUsingArrayList implements Iterable<Book> {
@Override
public Iterator<Book> iterator() {
return new BookShelfInnerIterator();
}
//改用ArrayList进行实际存放Book对象的容器
private ArrayList<Book> books = new ArrayList<>();
//以下的方法都是对ArrayList API的包装
public Book getBookAt(int index) {
return books.get(index);
}
public void appendBook(Book book) {
books.add(book);
}
public int getLength() {
return books.size();
}
//内部类在初始化的时候加上一个获取当前总Book数量的变量
public class BookShelfInnerIterator implements Iterator<Book> {
private int index;
private int size;
BookShelfInnerIterator() {
index = 0;
size = getLength();
}
@Override
public boolean hasNext() {
return index < size;
}
@Override
public Book next() {
return getBookAt(index++);
}
}
}
测试如下:
public static void main(String[] args) {
//测试使用ArrayLIst的书架类, 无需在构造器中传入长度了
BookShelfUsingArrayList bookShelfUsingArrayList = new BookShelfUsingArrayList();
for (int j = 0; j < 10; j++) {
bookShelfUsingArrayList.appendBook(new Book(j + ""));
}
for (Book book : bookShelfUsingArrayList) {
System.out.println(book);
}
}
以后想要编写集合类的时候, 一定要注意随着集合类编写一个迭代器, 可以方便的取出其中的元素进行操作.
Adapter 适配器模式 - 继承
最常见的是什么, 就是电源适配器. 计算机的电源都是使用直流电的, 而市电是交流电. 将计算机的电源当做一种规范, 也就是接口的话, 市电可以看成是现实中已经编写好的具体类或者说功能, 想要让计算机的其他部件能够正确的调用电源接口来获取电源, 我们就需要使用一个电源适配器.
这个适配器实现了电源接口, 这样计算机的其他部件就可以从其中获得所需要的直流电, 而电源适配器另外一端和市电的功能相匹配, 这样就可以工作在市电上, 这就是适配器模式. 即将接口规范与实际功能联系起来. 看起来也有点像一个代理.
适配器有两种模式, 一种是使用继承; 一种是适配器对象持有另外一个对象,也叫做委托模式.
这里先来看看继承模式. 书中的例子是, 一个实际用于显示字符串的Banner类的有若干个方法, 而程序的规范要求是使用Print接口和其中的方法来显示字符串, Banner类和Print接口如下:
public class Banner {
private String string;
public Banner(String string) {
this.string = string;
}
public void showWithParen() {
System.out.print('(');
System.out.print(string);
System.out.println(')');
}
public void showWithAster() {
System.out.print('*');
System.out.print(string);
System.out.println('*');
}
}
public interface Print {
void printWeak();
void printStrong();
}
由于规范调用的Print接口, 而实际工作的类Banner并没有实现Print接口, 想让Banner类也可以融入我们的程序进行工作, 就可以创建一个适配器类:
public class PrintBannerAdapter extends Banner implements Print {
public PrintBannerAdapter(String string) {
super(string);
}
@Override
public void printWeak() {
showWithParen();
}
@Override
public void printStrong() {
showWithAster();
}
}
注意其中的红字部分, 适配器要实现接口, 继承被适配的类. 在被其他按照规范的程序调用Print接口中的方法的时候, 实际工作采用Banner父类的方法来操作. 由于继承, 域和构造器还有方法都可以直接使用Banner类, 方便很多.
测试的程序很简单, 都是只根据Print接口规范来调用, 而适配器类就成功的让原本没有实现接口的类, 也能根据规范得到使用:
public class Main {
public static void main(String[] args) {
Print p = new PrintBannerAdapter("saner");
p.printStrong();
p.printWeak();
}
}
可以看到, PrintBannerAdapter的实例使用Print类型, 采用多态调用, 即规范不关心具体的实现类. 而通过适配器, 可以把原本不直接属于规范的类, 纳入到规范中进行使用.
Adapter 适配器模式 - 委托
委托模式只需要略微的修改一下适配器的代码, 让其中持有一个实际提供服务的类即可, 而不是直接继承实际提供服务的类:
public class PrintBannerAdapter2 implements Print {
//持有一个banner对象, 实例化的时候将字符串交给banner对象
private Banner banner;
public PrintBannerAdapter2(String string) {
this.banner = new Banner(string);
}
//方法中实际干活的依然是banner对象
@Override
public void printWeak() {
banner.showWithParen();
}
@Override
public void printStrong() {
banner.showWithAster();
}
}
测试代码几乎无需变化, 因为都是统一根据接口调用:
public class Main {
public static void main(String[] args) {
Print p = new PrintBannerAdapter2("saner");
p.printStrong();
p.printWeak();
}
}
适配器模式看上去好像增加了代码, 比如为什么不在接口中直接定义好要适配的类呢, 这是由于解耦的需要, 让适配器可以更灵活的工作.特别是软件升级, 新功能使用新开发的部分, 旧功能通过适配器, 交给原来老的稳定的部分, 是一种非常常见的设计模式.
当然, 适配器和实际工作的代码, 其作用必须是一致的, 只是类的具体结构上有所不同, 是不能把一个打印字符串的接口适配画图的功能上去的.
可以将适配器模式用于填补不同API但是相同功能之间的裂缝, 像胶水一样粘合起相同的功能, 所以适配器一是要连接不同的类, 而是需要改变API(比如banner的API就不是直接由Main调用, 而是调用接口的方法). 所以和Bridge以及装饰器模式有所区别.
练习
练习2.1 很简单, 由于针对的是接口, 如果使用PrintBanner类型, 则只能用于这个适配器, 无法作用于其他类型的适配器. 使用了接口之后, 可以作用于所有实现了该接口的适配器. 如果要特别的调用某个适配器的方法, 进行类型转换即可. 一切都是为了解耦啊.
练习2.2, 需要编写一个 将 java.util.Properties 类适配到自行编写的 FileIO 接口中的适配器类. FileIO接口如下:
import java.io.IOException;
public interface FileIO {
void readFromFile(String filename) throws IOException;
void writeToFile(String filename) throws IOException;
void setValue(String key, String value);
String getValue(String key);
}
上边的接口要抛异常, 不然一会编写具体方法的时候, 也会让你在接口方法上声明异常.
而底层实际干活的是java.util.Properties类, 我们需要编写一个适配器名称叫做FileProperties. 思路其实很简单, 可以采取委托模式:
import java.io.*;
import java.util.Properties;
public class FileProperties implements FileIO {
//实际干活的java.util.Properties对象
Properties properties = new Properties();
//看了一下API, 可以支持Reader对象, 正好用刚学的I/O套壳
@Override
public void readFromFile(String filename) throws IOException {
properties.load(new BufferedReader(new FileReader("file.txt")));
}
//写入方法也使用刚学的PrintWriter简便方法
@Override
public void writeToFile(String filename) throws IOException {
properties.store(new PrintWriter(filename), "written by FileProperties");
}
//剩下两个方法都是直接套用java.util.Properties的方法
@Override
public void setValue(String key, String value) {
properties.setProperty(key, value);
}
@Override
public String getValue(String key) {
return properties.getProperty(key);
}
}
实际使用的时候不管具体实现类, 按照接口规范使用:
import java.io.IOException;
public class Main {
public static void main(String[] args) {
FileIO f = new FileProperties();
try {
f.readFromFile("file.txt");
f.setValue("year", "2014");
f.setValue("month", "6");
f.setValue("day", "29");
f.writeToFile("newfile.txt");
} catch (IOException e) {
e.printStackTrace();
}
}
}
至此看完了第一部分: 适应设计模式. 这一部分作者讲是挑出了最简单的设计模式. 难的还在后边呢, 回想起第一次看这个书真的是稀里糊涂, 到现在很明晰的就看明白了. 功力确实有所增加.
不过不光是Java, 依然推荐要自学计算机的朋友不管怎么样, 先操起C语言然后把CSAPP看了, CSAPP可是真正的易筋经.