引言:模版方法模式可以说是我们日常工作中遇到最多的设计模式之一了,所以学习好模版方法对于阅读源码和日后编程都是很有用的
定义:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,模版方法使得子类可以不改变一个算法的结构即可冲定义该算法的某些特定步骤。
模式:典型模式就是一个接口或者抽象类,其中有一系列抽象方法,子类去实现具体的抽象方法,父类可以实现规划好一系列骨架,比如具体哪几个抽象方法搭配成一个总的算法
举例:制造一个Html页面的内容,假设我们不使用模板方法模式,直接让各个子类去直接实现这个接口,那么肯定实现的方式千奇百怪,而且步骤也乱七八糟的,这样实在不利于维护和扩展。所以我们可以使用模板方法模式,将这个过程给制定好,然后把具体的内容填充交给子类就好,这样这些子类生成的HTML页面就会非常一致。
public interface PageBuilder {
String bulidHtml();
}
public abstract class AbstractPageBuilder implements PageBuilder{
private StringBuffer stringBuffer = new StringBuffer();
public String bulidHtml() {
//首先加入doctype,因为都是html页面,所以我们父类不需要推迟给子类实现,直接在父类实现
stringBuffer.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");
//页面下面就是成对的一个HTML标签,我们也在父类加入,不需要给子类实现
stringBuffer.append("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
//下面就应该是head标签里的内容了,这个我们父类做不了主了,推迟到子类实现,所以我们定义一个抽象方法,让子类必须实现
appendHead(stringBuffer);
//下面是body的内容了,我们父类依然无法做主,仍然推迟到子类实现
appendBody(stringBuffer);
//html标签的关闭
stringBuffer.append("</html>");
return stringBuffer.toString();
}
//第一个模板方法
protected abstract void appendHead(StringBuffer stringBuffer);
//第二个模板方法
protected abstract void appendBody(StringBuffer stringBuffer);
}
public class MyPageBuilder extends AbstractPageBuilder{
@Override
protected void appendHead(StringBuffer stringBuffer) {
stringBuffer.append("<head><title>你好</title></head>");
}
@Override
protected void appendBody(StringBuffer stringBuffer) {
stringBuffer.append("<body><h1>你好,世界!</h1></body>");
}
public static void main(String[] args) {
PageBuilder pageBuilder = new MyPageBuilder();
System.out.println(pageBuilder.bulidHtml());
}
}
可以看到的是:当我们测试的时候,打印出了html页面,其中包含了<head><title>你好</title></head>和<body><h1>你好,世界!</h1></body>,子类的这些结果嵌套进了父类的
骨架中,所以,其实父类扮演了子类模版的角色,所有继承这个父类的子类都要按照父类的算法骨架来实现,避免每个人做出的页面结构都不一样,这也是模版方法的优点之一,这里注意的是,父类提供的骨架,一般是不希望子类去覆盖的。
模板方法模式还有一种使用的方式,为了给子类足够的自由度,可以提供一些方法供子类覆盖,去实现一些骨架中不是必须但却可以有自定义实现的步骤。
比如上述的例子当中,我们应该都知道,HTML页面中有一些标签是可有可无的。比如meta标签,link标签,script标签等。那么我们可以将刚才的例子细化一下,去看一下上面说的供子类覆盖的方法是什么。我们将刚才的抽象父类细化成如下形式。
public abstract class AbstractPageBuilder implements PageBuilder{
private static final String DEFAULT_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">";
private static final String DEFAULT_XMLNS = "http://www.w3.org/1999/xhtml";
private StringBuffer stringBuffer = new StringBuffer();
public String bulidHtml() {
stringBuffer.append(DEFAULT_DOCTYPE);
stringBuffer.append("<html xmlns=\"" + DEFAULT_XMLNS + "\">");
stringBuffer.append("<head>");
appendTitle(stringBuffer);
appendMeta(stringBuffer);
appendLink(stringBuffer);
appendScript(stringBuffer);
stringBuffer.append("</head>");
appendBody(stringBuffer);
stringBuffer.append("</html>");
return stringBuffer.toString();
}
protected void appendMeta(StringBuffer stringBuffer){
}
protected void appendLink(StringBuffer stringBuffer){
}
protected void appendScript(StringBuffer stringBuffer){
}
protected abstract void appendTitle(StringBuffer stringBuffer);
protected abstract void appendBody(StringBuffer stringBuffer);
}
可以看到,我们将head标签的生成过程更加细化了,分成四个方法,title,meta,link和script。但是这四个里面appendTitle是模板方法,子类必须实现,而其它三个则是普通的空方法。
那么上述三个方法,就是留给子类覆盖的,当然子类可以选择不覆盖,那么生成的HTML就没有meta,link和script这三种标签,如果想有的话,就可以覆盖其中任意一个,比如下面这样。
public class MyPageBuilder extends AbstractPageBuilder{
protected void appendMeta(StringBuffer stringBuffer) {
stringBuffer.append("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />");
}
protected void appendTitle(StringBuffer stringBuffer) {
stringBuffer.append("<title>你好</title>");
}
protected void appendBody(StringBuffer stringBuffer) {
stringBuffer.append("<body>你好,世界!</body>");
}
public static void main(String[] args) {
PageBuilder pageBuilder = new MyPageBuilder();
System.out.println(pageBuilder.bulidHtml());
}
}
如果把appendMeta也写成抽象方法,那么子类就必须实现,但是meta标签又不是必须的,所以子类就有可能把appendMeta,appendLink,appendScript方法全空着了。
所以为了不强制子类实现不必要的抽象方法,但又不剥夺子类自由选择的权利,我们在父类提供一个默认的空实现,来让子类自由选择是否要覆盖掉这些方法。
扩展:jdk中的类加载器就是模版方法最好的例子,jdk的类加载器大致可分为三类,分别是启动类加载器,扩展类加载器,应用程序加载器。
这三者加载类的路径分别如下:
启动(bootstrap)类加载器;引导类装入器是用本地代码实现的类装入器,他负责将JAVA_HOME/lib目录下的核心类库,以及被-Xbootcalsspath参数设定的jar包加载到内存中,由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
系统(System)类加载器:系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器
public abstract class ClassLoader {
//这里留了一个方法给子类选择性覆盖 protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } } 。这是一个模板方法模式,只是它没有定义抽象方法,因为findClass这个方法,并不是必须实现的,所以JDK选择留给程序员们自己选择是否要覆盖。public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先判断该类型是否已经被加载 Class c = findLoadedClass(name); if (c == null) { //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载 try { if (parent != null) { //如果存在父类加载器,就委派给父类加载器加载 c = parent.loadClass(name, false); } else { //如果不存在父类加载器,就检查是否是由启动类加载器加载的类, //通过调用本地方法native findBootstrapClass0(String name) c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
从代码上我们可以看出,在ClassLoader中定义的算法顺序是。
1,首先看是否有已经加载好的类。
2,如果父类加载器不为空,则首先从父类类加载器加载。
3,如果父类加载器为空,则尝试从启动加载器加载。
4,如果两者都失败,才尝试从findClass方法加载。
这是JDK类加载器的双亲委派模型,即先从父类加载器加载,直到继承体系的顶层,否则才会采用当前的类加载器加载。这样做的目的刚才已经说了,是为了JVM中类的一致性。
如果有读者第一次接触这方面的知识,估计会比较迷茫,下面LZ给出一个例子。各位猜测下下面程序的运行结果会是什么?
结果是:测试类package test0927; public class ClassLoaderTest { private String name ="测试类"; public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { Class<?> loadClass = ClassLoader.getSystemClassLoader().loadClass("test0927.ClassLoaderTest"); Object instance = loadClass.newInstance(); ClassLoaderTest ob=(ClassLoaderTest)instance; System.out.println(ob.name); } }
再举一个例子:package test0927; import java.io.IOException; import java.io.InputStream; public class MyClassLoader extends ClassLoader { public Class<?> loadClass(String name) throws ClassNotFoundException { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream is = getClass().getResourceAsStream(fileName); if (is == null) { return super.loadClass(name); } try { byte[] b = new byte[is.available()]; is.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException(); } } }
package test0927; public class ClassLoaderTest { private String name ="测试类"; public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { ClassLoader classLoader = new MyClassLoader(); // Class<?> loadClass = ClassLoader.getSystemClassLoader().loadClass("test0927.ClassLoaderTest"); Class<?> loadClass = classLoader.loadClass("test0927.ClassLoaderTest"); Object instance = loadClass.newInstance(); // ClassLoaderTest ob=(ClassLoaderTest)instance; System.out.println(instance instanceof ClassLoaderTest); } }
结果是false
这是因为如果没有按照ClassLoader中提供的骨架算法去加载类的话,可能会造成JVM中有两个一模一样的类信息,他们是来自一个类文件,但却不是一个加载器加载的,所以这两个类不相等。 这也是类加载器为何要使用模板模式给我们定义好查找的算法,是为了保证我们加载的每一个类在虚拟机当中都有且仅有一个。不过你可能会想,既然如此,为何不把loadClass方法写成final类型的,这样不是更安全吗? 这是因为有的时候我们希望JVM当中每一个类有且仅有一个,但有的时候我们希望有两个,甚至N个,就比如我们的tomcat,你可以想象下,你每一个项目假设都有com.xxx.xxxx.BaseDao等等,如果这些类都是一个的话,你的tomcat还能同时启动多个WEB服务吗?虽说tomcat也是遵循的双亲委派模型,但是从此也可以看出来,我们并不是在所有时候都希望同一个全限定名的类在整个JVM里面只有一个。