设计模式在JDK源码中的应用:Runtime、Calendar、InputStream、Collections

说实话,学了这么多设计模式,你可能会问:这些东西到底在哪儿用?

我当年也有这个困惑。直到有一天,我翻开JDK源码,发现原来天天用的API,背后全是设计模式。那种感觉,就像发现自家后院埋着宝藏。

今天咱们就挑四个最典型的例子,好好聊聊。

Runtime:单例模式,一个JVM只有一个

先问个问题:你的Java程序能创建几个Runtime对象?

答案是一个。JVM只允许一个Runtime实例存在。为什么?因为Runtime代表整个运行时环境,如果搞出多个,那谁代表谁?

核心要点:Runtime使用饿汉式单例,在类加载时就创建好实例。

咱们看看源码,其实特别简单:

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

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
    // ... 其他方法
}

注意那个私有构造方法。嗯,这是单例模式的标配——不让别人new。

我在项目中遇到过有人想自己new Runtime,结果编译都过不了。这就是设计模式的力量——从语法层面就堵死了错误用法。

避坑指南:我曾经见过一个团队,自己写了个“RuntimeUtil”工具类,里面又包了一层单例。其实完全没必要,直接用Runtime.getRuntime()就行。别重复造轮子。

Calendar:工厂模式,别自己new

你平时怎么获取日历对象?是不是这样:

Calendar cal = Calendar.getInstance();

为什么不用new Calendar()?因为Calendar是个抽象类,不能直接实例化。

那getInstance()背后做了什么?说白了,就是一个工厂方法。它会根据你系统的语言环境,返回不同的子类实现。

public static Calendar getInstance() {
    return createCalendar(TimeZone.getDefault(), 
                          Locale.getDefault(Locale.Category.FORMAT));
}

private static Calendar createCalendar(TimeZone zone, Locale aLocale) {
    // 根据locale判断返回哪种实现
    if (caltype.equals("gregorian")) {
        return new GregorianCalendar(zone, aLocale);
    }
    // 还有其他实现...
}

你想想看,如果让你自己new,你得知道GregorianCalendar这个类名。但用了工厂模式,你只需要调getInstance(),底层细节全封装好了。

我个人习惯,凡是创建对象逻辑可能变化的,都用工厂模式。这样改起来方便,调用方不用动。

InputStream:装饰器模式,一层套一层

这个我特别喜欢。你看这段代码:

InputStream in = new BufferedInputStream(
                     new FileInputStream("test.txt"));

FileInputStream负责读文件,BufferedInputStream给它加缓冲功能。这就是装饰器模式——一层套一层,功能叠加。

JDK里这种例子太多了:

  • DataInputStream:加基本类型读取功能
  • PushbackInputStream:加回退功能
  • LineNumberInputStream:加行号功能

每个装饰器都继承自InputStream,所以你可以任意组合:

InputStream in = new LineNumberInputStream(
                     new BufferedInputStream(
                         new DataInputStream(
                             new FileInputStream("data.bin"))));

注意:装饰器模式虽然灵活,但别套太多层。我曾经接手过一个项目,一个流套了7层装饰器,调试起来简直想哭。一般3层以内比较合理。

为什么JDK要用装饰器模式?因为如果不用,就得为每种组合写一个类。比如“带缓冲的文件输入流”、“带行号的文件输入流”、“带缓冲和行号的文件输入流”……那得多少个类?

装饰器模式把功能拆成小颗粒,自由组合。这就是组合优于继承的典型例子。

Collections:迭代器+适配器,两招齐出

Collections这个工具类,里面藏着两个设计模式。

迭代器模式:统一遍历方式

你写过这样的代码吧:

List<String> list = new ArrayList<>();
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
}

不管底层是ArrayList还是LinkedList,遍历方式都一样。这就是迭代器模式——把遍历逻辑从集合类里抽出来。

JDK里所有集合类都实现了Iterable接口,返回一个Iterator。这样你就不用关心底层是数组还是链表,统一用hasNext()和next()就行。

个人经验:我建议你在设计自己的容器类时,也实现Iterable接口。这样别人用起来顺手,还能直接用增强for循环。

适配器模式:把数组变成列表

这个更巧妙。你看:

String[] arr = {"a", "b", "c"};
List<String> list = Arrays.asList(arr);

Arrays.asList()返回的List,其实是个适配器。它把数组包装成List的样子,但底层还是数组。

源码大概长这样:

public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);  // 注意:这个ArrayList是Arrays的内部类
}

这个内部ArrayList没有自己的存储,直接操作传入的数组。所以你不能对它调用add()或remove()——会抛异常。

我曾经踩过的坑:有次我用Arrays.asList()返回的List,然后调了add(),结果运行时抛UnsupportedOperationException。查了半天才发现,原来这个List是适配器,不是真正的ArrayList。

适配器模式的核心,就是让不兼容的接口能一起工作。数组和List本来不兼容,但通过适配器,数组也能当List用。

一张图看懂

说了这么多,咱们用一张SVG图把今天的内容串起来:

JDK源码中的设计模式 Runtime 单例模式 一个JVM只有一个 饿汉式加载 私有构造方法 Calendar 工厂模式 getInstance() 根据Locale返回子类 封装创建逻辑 InputStream 装饰器模式 BufferedInputStream DataInputStream 功能叠加组合 Collections 迭代器+适配器 Iterator统一遍历 Arrays.asList适配 数组→List转换 核心启示 1. 单例模式:确保全局唯一,适合管理共享资源(Runtime、Spring Bean) 2. 工厂模式:封装创建逻辑,调用方不用关心具体实现类 3. 装饰器模式:功能拆解成小颗粒,自由组合,避免类爆炸 4. 迭代器+适配器:统一遍历接口,让不兼容的类型协同工作 设计模式不是理论,是JDK源码每天都在用的实战技巧

写在最后

今天这四个例子,其实只是JDK里设计模式应用的冰山一角。你平时用的String、ThreadLocal、Collections.synchronizedList(),背后都有设计模式的身影。

我个人建议,有空多翻翻JDK源码。不是为了背代码,而是看看那些大师们是怎么用设计模式解决实际问题的。看得多了,你自己写代码时,自然就知道什么时候该用什么模式了。

一句话总结:设计模式不是高高在上的理论,它就藏在你每天敲的代码里。你已经在用了,只是没意识到而已。


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