Java 经典设计模式-- 03.结构型模式

前言

书接上文,上一篇中创建型设计模式中的常用设计模式做了简单的介绍,本篇将继续对结构型设计模式中的常用模式进行介绍与分析。
目录:

  • 适配器模式
  • 桥接模式
  • 组合模式
  • 修饰模式
  • 代理模式

简单提及:

  • Marker

适配器模式

在设计模式中,适配器模式(英语:adapter pattern)有时候也称包装样式或者包装(wrapper)。将一个类的接口转接成用户所期待的。一个适配使得因接口不兼容而不能在一起工作的类能在一起工作,做法是将类自己的接口包裹在一个已存在的类中。摘自 Wiki

其实 Wiki 中说的不是很清楚。这种设计模式也是随着历史的推移而出现的,是对于开闭原则的一种实现。它的出现是为了更好的封装已有的逻辑,并且当需要添加新功能是避免对修改已有逻辑。通过 Wiki 提供的 UML 图或许能看的更清楚一些。它的特点是继承自原有的类,是 is-a 的关系。在继承的基础上扩展新方法。
在这里插入图片描述
这里举一个 登陆 功能的例子,通常在业务发展初期,一个拥有注册,登陆功能项目会以用户的手机号为唯一标识符,并将用户的信息记录在自己的数据库中,而后用户可以通过手机号或用户名登陆

/**原始注册服务**/
public class LoginService {

    /**原始注册方法**/
    public void register(String mobile, String userName, String password) {
        //insert user into DB
    }
    
	/**原始检查用户是否已经存在方法**/
    public boolean isValid(String mobile) {
        //检查用户是否已经存在在数据库中了
        return false;
    }

    /**原始手机号登陆方法**/
    public void loginByMobile(String mobile, String code) {
        //手机号 + 验证码登陆
    }
    
    /**原始用户名登陆方法**/
    public void loginByUserName(String userName, String password) {
        //用户名 + 密码登陆
    }
}

随着业务的扩大,某天项目希望以更加便利的方式提供来自不同渠道的用户第三方直接登陆功能(当然这需要对接大量第三方接口),比如微博登陆,微信登陆,qq 登陆等,但是原有的登陆服务类以及注册/登陆逻辑已经正常运行了很长一段时间,对它们做修改显然是不合理的,那么就可以使用适配器模式扩展新的逻辑

/**微信登陆业务扩展,通常第三方登陆都不需要提供注册方法**/
public class WeixinLoginService extends LoginService {

    /**原始手机号登陆方法**/
    public void loginByWeixin(String openId) {
        //通过 openId 获取用户信息
        //用户信息中包含用户的手机号
        String mobile = null;
        //获取手机号后,检查用户是否已经存在
        if (!isValid(mobile)) {
            //生成默认用户名和密码
            String userName = "defaultUserName";
            String password = "defaultPassword";
            //调用注册方法注册用户
            register(mobile, userName, password);
        } else {
            //用户已经通过原有方式注册过了,直接登陆   
        }
    }
}

这种设计模式在项目中是非常普遍且有用的。

桥接模式

桥接模式是软件设计模式中最复杂的模式之一,它把事物对象和其具体行为、具体特征分离开来,使它们可以各自独立的变化。事物对象仅是一个抽象的概念。如“圆形”、“三角形”归于抽象的“形状”之下,而“画圆”、“画三角”归于实现行为的“画图”类之下,然后由“形状”调用“画图”。摘自 Wiki

桥接模式最典型的例子就是 Java 数据结构中的 Collection 接口与 Iterator 接口的关系。在 Java 8 数据结构篇中有详细的介绍。Collection 通过实现 Iterable 接口对外声明有迭代的能力,真正的迭代实现使用 Iterator 的实现类来完成的。Collection 通过持有 Iterator 实现类的对象调用迭代的方法,这样分装了 Iterator 实现类对于各种 Collection 实现类实现方式的差异性。可以通过 Wiki 上的桥接模式 UML 图来进一步对其了解。
在这里插入图片描述
既然前面刚写过 Java 8 数据结构的文章,这里就以数据结构的实现方式为例

/**迭代能力接口**/
interface Iterable {

    /**获得迭代器**/
    Iterator iterator();
}

/**数据结构向外声明有迭代的能力**/
public interface Collection extends Iterable {

}

/**迭代器接口**/
interface Iterator {

    /**向外提供统一迭代方法**/
    void hasNext();

    void next();
}

/**数组链表拥有迭代能力**/
class ArrayList implements Collection {

    public Iterator iterator() {
        return new ArrayListIterator();
    }

    /**真正实现数组链表自己的迭代能力的方式**/
    class ArrayListIterator implements Iterator {

        public void hasNext() {
            System.out.println("这是数组链表判断迭代器游标位置之后是否还有对象的方式");
        }

        public void next() {
            System.out.println("这是数组链表判断迭代器返回游标位置之后元素的方式");
        }
    }
}

/**链接链表拥有迭代能力**/
class LinkedList implements Collection {

    public Iterator iterator() {
        return new LinkedListIterator();
    }

    /**真正实现链接链表自己的迭代能力的方式**/
    class LinkedListIterator implements Iterator {
        public void hasNext() {
            System.out.println("这是链接链表判断迭代器游标位置之后是否还有对象的方式");
        }

        public void next() {
            System.out.println("这是链接链表判断迭代器返回游标位置之后元素的方式");
        }
    }
}

class Test {
    public static void main(String[] args) {
        /**统一的调用方式**/
        Collection collection = new ArrayList();
        collection.iterator().next();
        collection = new LinkedList();
        collection.iterator().next();
    }
}

总之,桥接模式可以说是对于封装的最高级别的应用,它的使用与好处还需要在项目中渐渐摸索,但是切勿盲目的使用。

组合模式

In software engineering, the composite pattern is a partitioning design pattern. The composite pattern describes a group of objects that is treated the same way as a single instance of the same type of object. The intent of a composite is to “compose” objects into tree structures to represent part-whole hierarchies. Implementing the composite pattern lets clients treat individual objects and compositions uniformly. 摘自 Wiki
这种模式的介绍已经从中文版的 Wiki 中删除,但是英文版中还有。在软件工程中,组合模式是一种分隔设计模式。组合米哦啊叔描述了一组对象以像对待同一个对象那么治理的对象。一个组合的目的是“组合”对象到一种树形结构中来提供 部分-全部 继承关系。实现组合模式让客户端可以均匀的操作对象和它们的组合。

其实就是属性结构,叶子与树干拥有相同的方法,来通过 UML 图进一步了解
在这里插入图片描述
可以看到 Leaf 和 Composite 都是 Component 的子类,而对于客户端来说,它只知道自己操作的是 Component,并不知道究竟是 Leaf 还是 Composite。

/**树接口(Component)**/
public interface Tree {

    /**生长方法**/
    void grow();
}

/**叶子类(Leaf)**/
class Leaf implements Tree {

    /**叶子生长方法**/
    public void grow() {
        System.out.println("生长叶子");
    }
}

/**树干类(Composite)**/
class Trunk implements Tree {
    private List<Tree> trees;

    /**添加节点方法,不关心是叶子还是树干**/
    public void add(Tree tree) {
        trees.add(tree);
    }

    /**树干生长方法**/
    public void grow() {
        if (!trees.isEmpty()) {
            System.out.println("我还有树!");
            for (Tree ele : trees) {
                ele.grow();
            }
        } else {
            System.out.println("我长完了");
        }
    }
}

class Test {

    public static void main(String[] args) {
        Trunk trunk = new Trunk();
        Trunk trunk2 = new Trunk();
        Tree leaf1 = new Leaf();
        Tree leaf2 = new Leaf();
        trunk2.add(leaf1);
        trunk2.add(leaf2);
        trunk.add(trunk2);
        trunk.grow();
    }
}

这种设计模式其实不是经常用到,它可以作为 Java 8 业界最佳实现数据结构类的一种扩展。比如在显示 CMS 树形结构的时候使用。

修饰模式

修饰模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式。就功能而言,修饰模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。摘自 Wiki

修饰模式又称为装饰器模式,它与适配器模式的目的相同 – 在不影响原有类结构与方法的基础上,扩展新的逻辑。但是与适配器不同的是,它不是使用继承最原有类的方式,而是使用持有原有类的对象的方式。这样更加符合迪米特法则与合成复用法则,更好的解耦了新逻辑与旧类之间的关系,注意前提是装饰器类与原有类没有强关联关系,如果有强关联关系还是推荐使用适配器模式,对比适配器模式,它与原有类的关系是 has-a 的关系
这里直接修改上面的适配器例子成为一个装饰器模式

/**微信登陆业务扩展,通常第三方登陆都不需要提供注册方法**/
public class WeixinLoginService {
    
    /**使用持有而非继承的方式,只知道 LoginService 中有哪些方法,完全不清楚它们是如何实现的**/
    private LoginService loginService;

    /**原始手机号登陆方法**/
    public void loginByWeixin(String openId) {
        //通过 openId 获取用户信息
        //用户信息中包含用户的手机号
        String mobile = null;
        //获取手机号后,检查用户是否已经存在
        if (!loginService.isValid(mobile)) {
            //生成默认用户名和密码
            String userName = "defaultUserName";
            String password = "defaultPassword";
            //调用注册方法注册用户
            loginService.register(mobile, userName, password);
        } else {
            //用户已经通过原有方式注册过了,直接登陆
        }
    }
}

所以在没有强依赖的情况下扩展一个类时,应该有限使用装饰器模式

代理模式

代理模式(英语:Proxy Pattern)是程序设计中的一种设计模式。
所谓的代理者是指一个类别可以作为其它东西的接口。代理者可以作任何东西的接口:网络连接、存储器中的大对象、文件或其它昂贵或无法复制的资源。
著名的代理模式例子为引用计数(英语:reference counting)指针对象。
当一个复杂对象的多份副本须存在时,代理模式可以结合享元模式以减少存储器用量。典型作法是创建一个复杂对象及多个代理者,每个代理者会引用到原本的复杂对象。而作用在代理者的运算会转送到原本对象。一旦所有的代理者都不存在时,复杂对象会被移除。

这里 Wiki 说的比较高深,其实代理模式就是提供一个代理类,这个类一般是做一些被代理对象产生时还不知道如何实现的,但是在程序运行时不得不实现的逻辑,该类会代理所有被代理类的方法,实现方式是通过将被代理类的方法的字符串重组生成新的 Java 类之后加载到 ClassLoader 中,运行完代理方法后再将这些 Class 删除,典型代理的应用场景有 Spring 的 AOP,Github 的 PageHelper 等。代理按历史进程主要可以分为静态代理和动态代理,动态代理按实现方式的不同又可以分为 JDK 动态代理和 CgLib 动态代理,当然也可以自己实现动态代理模式。接下来就以网红店抢号码为例来对这三种代理模式进行依次分析。

/**抢号接口,被代理类与代理类都需要实现这个接口**/
public interface GrabNumbers {

	/**抢号方法**/
    void grabNumber();
}
/**我抢号类,被代理类,实现 GrabNumbers 接口,实现 grabNumber 方法**/
public class MeGrabNumbers implements GrabNumbers {

	/**我只需要在抢到号码的时候,通知我就行了,怎么抢,抢完以后怎么办,我不关心,或者我不知道**/
    public void grabNumber() {
        System.out.println("抢到号码了!现在就通知我!");
    }
}
/**帮忙抢号类,代理类,同样实现 GrabNumbers 接口,实现 grabNumber 方法**/
public class HelperGrabNumbers implements GrabNumbers {

	/**必须持有被代理类的对象,这里使用接口是为了使代理类可以代理任何实现了 GrabNumebrs 接口的实现类对象**/
    private final GrabNumbers grabNumbers;

	/**通过构造方法传入初始化被代理类对象**/
    public HelperGrabNumbers(GrabNumbers grabNumbers) {
        this.grabNumbers = grabNumbers;
    }

	/**代理方法,代理方法会添加一些自己的逻辑,然后调用被代理方法**/
    public void grabNumber() {
        System.out.println("开始排队等号");
        grabNumbers.grabNumber();
        System.out.println("工作完成");
    }
}
/**再添加一个被代理类**/
public class YouGrabNumbers implements GrabNumbers {

    public void grabNumber() {
        System.out.println("你抢到号码了!马上通知你!");
    }
}
public class StaticGrabNumberTest {

    public static void main(String[] args) {
    	/**代理类代理了 MyGrabNumbers,代理部分逻辑相同,MyGrabNumbers 自己的实现不同**/
        GrabNumbers grabNumbers = new HelperGrabNumbers(new MeGrabNumbers());
        grabNumbers.grabNumber();
        /**代理类代理了 YouGrabNumbers,代理部分逻辑相同,YouGrabNumbers 自己的实现不同**/
        GrabNumbers grabNumbers1 = new HelperGrabNumbers(new YouGrabNumbers());
        grabNumbers1.grabNumber();
    }
}

但是这样会存在一个问题,就是如果一个类中的多个方法都需要被代理,就需要一个一个添加代理方法,这样做显然是效率很低的,于是动态代理就诞生了,动态代理之所以重围动态,是因为程序运行前是不知道要代理哪些类的方法,在运行时候才知道,而程序运行结束后,这些代理类都会被删除,这些类的特点之一是类名类似于$Proxyxxx.class 这样的形式,首先来看下 JDK 动态代理

/**为了体现出动态代理的方便之处,它能代理每一个方法,这里接口再新增一个方法**/
public interface GrabNumbers {

	/**抢号方法**/
    void grabNumber();

	/**再抢一次!**/
    void grabAnotherNumber();
}
public class JdkMeGrabNumbers implements GrabNumbers {

	/**我的抢号需要关心的部分**/
    public void grabNumber() {
        System.out.println("抢到号码了!");
    }

	/**我的再抢一次需要关心的部分**/
    public void grabAnotherNumber() {
        System.out.println("又抢到一个号码!");
    }
}
/**动态代理类,实现了 InvocationHandler 接口**/
public class JdkHelperGrabNumbers implements InvocationHandler {

	/**同样必须持有被代理类的对象**/
    private GrabNumbers grabNumbers;

	/**获得代理类实例方法**/
    public Object getInstance(GrabNumbers grabNumbers) {
    	/**缓存被代理类对象,在 invoke 方法中被用到**/
        this.grabNumbers = grabNumbers;
        /**获得被代理类的 class**/
        Class<?> clazz = grabNumbers.getClass();
        /**
        生成一个新的对象(字节码重组),主要是通过如下步骤实现的
        1、拿到被代理类对象的引用,并且获取它的所有接口(通过反射)
        2、JDK Proxy 生成一个新的类,新的类实现被代理类实现的所有接口
        3、动态的生成 Java 代码
        4、编译生成的 Java 代码为 Class 文件
        5、通过被代理类的 ClassLoader 加载到 JVM 中运行
        **/
        return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
    }

	/**代理方法,这个方法将代理被代理对象的所有方法**/
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    	/**代理逻辑**/
        System.out.println("开始排队等号");
        /**调用被代理对象的方法自己的逻辑**/
        method.invoke(this.grabNumbers, args);
        System.out.println("工作完成");
        return null;
    }
}
public class JdkGrabNumbersProxyTest {

    public static void main(String[] args) {
    	/**面向接口,获得代理类对象**/
        GrabNumbers grabNumbers = (GrabNumbers) new JdkHelperGrabNumbers().getInstance(new JdkMeGrabNumbers());
        /**调用第一个方法,被代理了**/
        grabNumbers.grabNumber();
        /**调用第二个方法,依然被代理了**/
        grabNumbers.grabAnotherNumber();
    }
}

但是这样还是有一点不方便,如果一个类希望自己被代理,而它又没有实现任何接口,使用 JDK 代理的方式时它不得不新建一个接口,实现它,因为 JDK 代理需要这个接口作为参数。于是 CgLib 用了一种更合理的方式实现了动态代理

/**被代理类,注意这个类没有实现任何接口!**/
public class CgLibMeGrabNumbers {

	/**需要被代理的方法1**/
    public void grabNumber() {
        System.out.println("抢到号了!立刻通知我!");
    }

	/**需要被代理的方法2**/
    public void grabAnotherNumber() {
        System.out.println("又抢到一个号码!通知我!");
    }
}
/**代理类,需要实现 MethodInterceptor 接口**/
public class CgLibHelperGrabNumbers implements MethodInterceptor {

	/**获得代理类对象,传入被代理类 class**/
    public Object getInstance(Class<?> clazz) throws Exception {
        Enhancer enhancer = new Enhancer();
        /**
        要把哪个类设置为即将生成的新类
        注意这里设置的是父类
        这里的实现逻辑 JDK 类似
        **/
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        /**新建代理对象**/
        return enhancer.create();
    }

	/**代理方法**/
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("开始排队等号");
        /**注意方法签名的含义是调用父类**/
        methodProxy.invokeSuper(o, objects);
        System.out.println("工作完成");
        return null;
    }
}
public class CgLibGrabNumbersProxyTest {

    public static void main(String[] args) {
    	/**不再有接口,直接通过代理类获得父类对象(被代理的)**/
        CgLibMeGrabNumbers cgLibMeGrabNumbers = null;
        try {
            cgLibMeGrabNumbers = (CgLibMeGrabNumbers) new CgLibHelperGrabNumbers().getInstance(CgLibMeGrabNumbers.class);
            /**调用第一个方法,被代理了**/
            cgLibMeGrabNumbers.grabNumber();
            /**调用第二个方法,也被代理了**/
            cgLibMeGrabNumbers.grabAnotherNumber();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

所以以后建立动态代理类时,都推荐使用 CgLib 的方式建立,这样做是最方便的。

享元模式

享元模式(英语:Flyweight Pattern)是一种软件设计模式。它使用共享物件,用来尽可能减少内存使用量以及分享资讯给尽可能多的相似物件;它适合用于当大量物件只是重复因而导致无法令人接受的使用大量内存。通常物件中的部分状态是可以分享。常见做法是把它们放在外部数据结构,当需要使用时再将它们传递给享元。
典型的享元模式的例子为文书处理器中以图形结构来表示字符。一个做法是,每个字形有其字型外观, 字模 metrics, 和其它格式资讯,但这会使每个字符就耗用上千字节。取而代之的是,每个字符参照到一个共享字形物件,此物件会被其它有共同特质的字符所分享;只有每个字符(文件中或页面中)的位置才需要另外储存。摘自 Wiki

这种模式在 Wiki 对代理类的描述中也被提及了,所以这里一般介绍一下,但其实我项目中用到的不多,不过看上去应该也会很有用,当需要使用大量类似的对象,可以将部分共享的分享。这里就以 Wiki 的例子为例。

public enum FontEffect { //字体效果枚举类
	//粗体,斜体。。。
    BOLD, ITALIC, SUPERSCRIPT, SUBSCRIPT, STRIKETHROUGH
}

/**字体数据类**/
public final class FontData {
    /**
     * 一个弱引用 hash map 将会丢弃无用的 FontData 引用
     * 值将不得不被包装成弱引用
     * 因为弱引用 hash map 的值对象被强引用持有
     * 查看 WeakHashMap 的 Java doc 可知,一个弱引用 HashMap 将在键值不再被使用的时		
     * 候将它移除
     */
    private static final WeakHashMap<FontData, WeakReference<FontData>> FLY_WEIGHT_DATA =
        new WeakHashMap<FontData, WeakReference<FontData>>(); //弱引用 HashMap,键为 FontData,值为 FontData 的弱引用
    private final int pointSize; //点大小
    private final String fontFace; //字体显示 
    private final Color color; //颜色
    private final Set<FontEffect> effects; //一套字体效果枚举类对象

	/**私有化构造器,接收点大小,字体显示,颜色,一套字体效果枚举类对象,构造一个字体数据类对象**/
    private FontData(int pointSize, String fontFace, Color color, EnumSet<FontEffect> effects) {
        this.pointSize = pointSize;
        this.fontFace = fontFace;
        this.color = color;
        this.effects = Collections.unmodifiableSet(effects); //不可变
    }

	/**静态字体数据创建方法,接收点大小,字体显示,颜色,与一个可变长枚举对象参数列表**/
    public static FontData create(int pointSize, String fontFace, Color color,
        FontEffect... effects) {
        /**使用枚举类建立一个枚举套对象**/
        EnumSet<FontEffect> effectsSet = EnumSet.noneOf(FontEffect.class);
        for (FontEffect fontEffect : effects) { //遍历枚举对象参数列表
            effectsSet.add(fontEffect); //添加到枚举套中
        }
        /**我们不知道创建对象的开销,我们是为了减少内存计算**/
        FontData data = new FontData(pointSize, fontFace, color, effectsSet);

		/**获得之前用给定值创建的实例,如果它还存在的话**/
        WeakReference<FontData> ref = FLY_WEIGHT_DATA.get(data); //传入键,查找值
        /**查看 WeakReference#get 方法的定义,是返回弱引用指向的对象,如果这个弱引用被程序或者 GC 回收了,那么就返回 null**/
        FontData result = (ref != null) ? ref.get() : null; //如果值存在,获取字体数据,否则赋值为 null
        
        //如果没有对应的实例存在。存储新的字体数据实例
        if (result == null) { //如果 result 等于 null
            FLY_WEIGHT_DATA.put(data, new WeakReference<FontData> (data)); //向弱引用 hashMap 中添加
            result = data; //将 data 赋值给 result
        }
        //返回一个给定参数构造的单独的不可变拷贝
        return result;
    }

	/**重写 equals 方法**/
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof FontData) {
            if (obj == this) {
                return true;
            }
            FontData other = (FontData) obj;
            return other.pointSize == pointSize && other.fontFace.equals(fontFace)
                && other.color.equals(color) && other.effects.equals(effects);
        }
        return false;
    }

	/**重写 hashCode 方法**/
    @Override
    public int hashCode() {
        return (pointSize * 37 + effects.hashCode() * 13) * fontFace.hashCode();
    }

    // Getters for the font data, but no setters. FontData is immutable.
}

程序中 WeakHashMap 的作用类似一个缓存,程序中最关键的代码是

/**我们不知道创建对象的开销,我们是为了减少内存计算**/
        FontData data = new FontData(pointSize, fontFace, color, effectsSet);

		/**获得之前用给定值创建的实例,如果它还存在的话**/
        WeakReference<FontData> ref = FLY_WEIGHT_DATA.get(data); //传入键,查找值
        /**查看 WeakReference#get 方法的定义,是返回弱引用指向的对象,如果这个弱引用被程序或者 GC 回收了,那么就返回 null**/
        FontData result = (ref != null) ? ref.get() : null; //如果值存在,获取字体数据,否则赋值为 null

可以看到虽然第一行代码新建了一个 FuntData 对象,但是只是用来查询弱引用 HashMap 中是否存在与它相同的对象作为键,如果存在,则返回该对象的引用,再通过引用获取到那个对象,如果引用对象存在,那么直接使用引用对象,不使用新建的对象(意味着它会被回收),否则执行后续代码

//如果没有对应的实例存在。存储新的字体数据实例
        if (result == null) { //如果 result 等于 null
            FLY_WEIGHT_DATA.put(data, new WeakReference<FontData> (data)); //向弱引用 hashMap 中添加
            result = data; //将 data 赋值给 result
        }
        //返回一个给定参数构造的单独的不可变拷贝
        return result;

才会将新建的对象加入弱引用 HashMap 中,这样就最大程度的保证了对象的重用,同时由于弱引用 HashMap 本身的性质,在键不被使用的时候会被移除,也保证了内存不至于压力过大。

还有两种 Java 中使用也比较广泛,但是无需太深入分析的设计模式,Marker 与 Module,这两种设计模式都已经从中文版的 Wiki 当中移除了,但是在英文版的 Wiki 当中依然保留着。

Marker

The marker interface pattern is a design pattern in computer science, used with languages that provide run-time type information about objects. It provides a means to associate metadata with a class where the language does not have explicit support for such metadata. 摘自 Wiki
标记型接口模式是一种计算机科学设计模式,用于那些提供运行时对象类型信息的语言。它提供了一种与语言本身没有明确支持的一个类的元数据的意义。

还是比较拗口的,其实标记接口就是指作为一个标记,表示实现类有某种能力,但是不提供任何方法的接口,比如 Java 中的 Serializable 与 Cloneable 都是这样的接口。其实它的作用与注解驱动编程类似,只不过注解是 JDK 1.5 版本之后才有的功能,在 JDK 1.5 之前,就是通过标记接口来完成特定的标记的。以 Cloneable 为例

public class CloneTest implements Cloneable {

    public static void main(String[] args) {
        CloneTest cloneTest = new CloneTest();
        try {
            cloneTest.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

一个类实现了 Cloneable 接口,即使没有重写 clone 方法也是可以的,这个时候调用 clone 方法(从 Object 中继承)也是可以的,但如果一个类没有实现 Cloneable 接口,而直接调用 clone 方法,main 方法中的代码并没有改动,但是就会抛出 CloneNotSupportedException 异常,由此可以推断,在调用 clone 方法时,JVM 会检测类是否实现了 Cloneable 接口,如果没有实现,就会抛出异常,这与注解驱动编程其实是异曲同工的

以上就是对于 Java 结构型设计模式的一些简单介绍与分析,下一篇将对 Java 行为型设计模式进行介绍与分析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值