设计模式在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里设计模式应用的冰山一角。你平时用的String、ThreadLocal、Collections.synchronizedList(),背后都有设计模式的身影。
我个人建议,有空多翻翻JDK源码。不是为了背代码,而是看看那些大师们是怎么用设计模式解决实际问题的。看得多了,你自己写代码时,自然就知道什么时候该用什么模式了。
一句话总结:设计模式不是高高在上的理论,它就藏在你每天敲的代码里。你已经在用了,只是没意识到而已。
公众号:蓝海资料掘金营,微信deep3321