设计模式与JDK源码:Collections中的策略、InputStream中的装饰器、Runtime中的单例

说实话,很多程序员学设计模式,学完了还是不知道怎么用。我当年也一样,背了二十多种模式的UML图,面试时对答如流,一到写代码就懵了。

后来我悟了——与其死磕理论,不如去看看JDK源码。Java标准库本身就是设计模式的最佳实践集。今天我就带你拆解三个最经典的例子:Collections里的策略模式、InputStream里的装饰器模式、Runtime里的单例模式

核心观点:设计模式不是凭空想出来的,是解决实际问题的套路。JDK源码就是最好的教科书。

一、Collections中的策略模式

先说说策略模式。说白了就是:把算法抽出来,让它们可以互相替换。调用方只关心接口,不关心具体实现。

JDK里最典型的例子就是 java.util.Collectionssort() 方法。你看它的签名:

public static <T> void sort(List<T> list, Comparator<? super T> c)

第二个参数 Comparator 就是策略接口。你传入不同的比较器,排序的行为就完全不同。

举个例子:

// 策略1:按年龄升序
Collections.sort(users, new Comparator<User>() {
    @Override
    public int compare(User a, User b) {
        return a.getAge() - b.getAge();
    }
});

// 策略2:按姓名降序
Collections.sort(users, new Comparator<User>() {
    @Override
    public int compare(User a, User b) {
        return b.getName().compareTo(a.getName());
    }
});

你看,排序算法本身(归并排序)是固定的,但比较策略可以随意切换。这就是策略模式的核心思想——把变化的部分封装起来

我的经验:我在做电商系统的价格计算时,就用过策略模式。满减、打折、优惠券……每种促销都是一个策略。后来加新活动,只需要新增一个策略类,完全不用改核心代码。爽得很。

为什么JDK要这么设计?你想想看,如果 sort() 把比较逻辑写死在里面,那每种排序需求都得重写一个方法。那 Collections 类得膨胀成什么样?

二、InputStream中的装饰器模式

装饰器模式,我个人的理解是:给对象一层层加功能,像穿衣服一样。每一层都实现相同的接口,内部持有上一层的引用。

JDK里最经典的例子就是 java.io.InputStream 体系。你看这个用法:

InputStream in = new BufferedInputStream(
                    new GZIPInputStream(
                      new FileInputStream("data.gz")));

从里到外:FileInputStream 负责读文件,GZIPInputStream 负责解压,BufferedInputStream 负责缓冲。每一层都是 InputStream 的子类,每一层都只做一件事。

这就是装饰器模式的核心——组合优于继承。如果不用装饰器,你要实现「带缓冲的GZIP文件输入流」,是不是得写一个 BufferedGZIPFileInputStream?那如果再要加个加密呢?EncryptedBufferedGZIPFileInputStream?类爆炸了。

避坑指南:我曾经在一个项目中,看到有人自己写了一个「超级输入流」,把缓冲、解压、解密全塞在一个类里。结果后来要换加密算法,整个类都得重写。这就是典型的「没有用装饰器」的后果。

装饰器模式也有个缺点——调试时堆栈很深。你想想看,一层套一层,出错了看调用栈,得翻好几层才能找到真正的问题。所以我的建议是:装饰器层数不要超过3层,否则维护成本就上来了。

三、Runtime中的单例模式

单例模式,大家最熟悉了。但JDK里的 java.lang.Runtime 实现方式,值得你细品。

直接看源码:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
}

这是典型的饿汉式单例——类加载时就创建实例。为什么不用懒汉式?因为 Runtime 是JVM的核心组件,几乎肯定会用到,没必要延迟加载。

而且你看,构造方法是 private 的,外部无法 new 出第二个实例。这就保证了整个JVM进程中只有一个 Runtime 对象。

为什么Runtime必须是单例?因为它代表的是JVM运行时环境。如果搞出两个Runtime对象,一个说内存还剩100M,另一个说还剩200M,你信谁?

我遇到过有人问:单例模式用 synchronized 会不会影响性能?其实对于 Runtime 这种饿汉式,根本不存在线程安全问题——实例在类加载时就创建好了,而类加载是线程安全的。

嗯,这里要注意:不是所有单例都适合饿汉式。如果你的单例对象创建成本很高,而且不一定用得到,那还是用懒汉式或者静态内部类的方式比较好。

四、一张图看懂三种模式

下面这张SVG图,帮你梳理这三种模式在JDK中的位置和关系:

JDK中的三种设计模式 策略模式 Collections.sort() Comparator 接口 ├─ AgeComparator ├─ NameComparator └─ 自定义策略 算法与策略分离 运行时动态切换 装饰器模式 InputStream 体系 FileInputStream └─ GZIPInputStream └─ BufferedInputStream 层层包装,功能叠加 避免类爆炸 调试堆栈较深 单例模式 Runtime 类 饿汉式实现 private 构造方法 static getRuntime() JVM全局唯一 类加载时创建 线程安全 三种模式共同点:将变化封装、组合优于继承、单一职责

五、总结与思考

回顾一下今天的内容:

  • 策略模式Collections.sort()Comparator 参数,把比较算法抽出来,随意替换。
  • 装饰器模式InputStream 的层层包装,每个装饰器只加一种功能,自由组合。
  • 单例模式Runtime 的饿汉式实现,保证JVM运行时环境的全局唯一性。

我个人觉得,学设计模式最好的方法就是「读源码 + 写代码」。光看不练,永远只是纸上谈兵。下次你写代码时,不妨问问自己:这个变化点能不能用策略模式封装?这个功能叠加能不能用装饰器?这个全局状态能不能用单例?

一个小建议:你可以试着在IDE里打开JDK源码,跟着今天讲的三个例子,自己走读一遍。相信我,读源码比看任何教程都管用。

嗯,今天就聊到这儿。设计模式这东西,用多了自然就熟了。


公众号:蓝海资料掘金营,微信deep3321