设计模式与JDK源码:Collections中的策略、InputStream中的装饰器、Runtime中的单例
说实话,很多程序员学设计模式,学完了还是不知道怎么用。我当年也一样,背了二十多种模式的UML图,面试时对答如流,一到写代码就懵了。
后来我悟了——与其死磕理论,不如去看看JDK源码。Java标准库本身就是设计模式的最佳实践集。今天我就带你拆解三个最经典的例子:Collections里的策略模式、InputStream里的装饰器模式、Runtime里的单例模式。
核心观点:设计模式不是凭空想出来的,是解决实际问题的套路。JDK源码就是最好的教科书。
一、Collections中的策略模式
先说说策略模式。说白了就是:把算法抽出来,让它们可以互相替换。调用方只关心接口,不关心具体实现。
JDK里最典型的例子就是 java.util.Collections 的 sort() 方法。你看它的签名:
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中的位置和关系:
五、总结与思考
回顾一下今天的内容:
- 策略模式:
Collections.sort()的Comparator参数,把比较算法抽出来,随意替换。 - 装饰器模式:
InputStream的层层包装,每个装饰器只加一种功能,自由组合。 - 单例模式:
Runtime的饿汉式实现,保证JVM运行时环境的全局唯一性。
我个人觉得,学设计模式最好的方法就是「读源码 + 写代码」。光看不练,永远只是纸上谈兵。下次你写代码时,不妨问问自己:这个变化点能不能用策略模式封装?这个功能叠加能不能用装饰器?这个全局状态能不能用单例?
一个小建议:你可以试着在IDE里打开JDK源码,跟着今天讲的三个例子,自己走读一遍。相信我,读源码比看任何教程都管用。
嗯,今天就聊到这儿。设计模式这东西,用多了自然就熟了。
公众号:蓝海资料掘金营,微信deep3321