今天,北京正式供暖第一天。
醒的时候快九点。窗帘缝里那条光比昨天宽了些。前几天试供暖,暖气只是温吞吞的,夜里也没响动。
大概九点半,暖气管里开始响。
咕噜,咕噜。像有人在很远的地方倒水,水顺着管子,一层一层往上走。过了一会儿再摸,客厅那组暖气片的下沿烫了,上沿还是温的。
朋友圈里一半在晒家里的温湿度计,一半在骂不热。物业群里发了一条:今天正式供热,管道里憋着气,不热的自己放放气。
翻出一把一字螺丝刀,又扯了块抹布垫在暖气片下面,从客厅开始。
嘶——一股气出来,带着锈水的味。放到气声没了、有水渗出来,赶紧拧紧。再摸,烫手了。接着是主卧、次卧、厨房、卫生间。一间一间走,动作都一样:蹲下、拧开、等气放完、拧紧、手背贴一下、记一笔。
厨房那组是凉的。卫生间的也是。放完气等了一会儿,还是凉的。
从抽屉里翻出测温枪,对着暖气片挨个点了一下。记的是表面温度,不是屋子里的温度。手机上记了一行:
客厅 52℃ 主卧 47℃ 次卧 44℃
厨房 21℃ 卫生间 19℃
这次聊的迭代器模式(Iterator):把“下一个是谁”交出去,自己只管一个一个往下走。系列第二十二篇。
第一天的暖气,得挨间摸一遍
01 一间一间写死,多一间就得改代码
最直接的写法,是把家里所有暖气片排成一队,从上到下写一遍。
void check(Home home) {
look(home.livingRoom());
look(home.masterBedroom());
look(home.secondBedroom());
look(home.kitchen());
look(home.bathroom());
// 明年添了一组书房,这里还得补一行
}
第一眼能跑。
毛病有三个。
一是顺序写死在方法里。房间增减,得改这个方法;顺序调一下——先看卧室再看客厅——也得改。家里的格局几年一变,方法跟着几年一改。
二是换个东西就得重写。地暖是分水器上一排阀门,老房子是一根立管串着几组,有的房子只有一台壁挂炉。每一样都有自己“怎么数”的规矩:怎么算下一组、什么时候算数完。调用方每遇到一种,就得重新学一遍。
三是“数到第几个”和“摸一下算不算热”搅在一起。两件事本来不相干,挤在同一个方法里,谁也改不动谁。
五组暖气,五行代码,顺序焊死
02 把“下一个是谁”交出去
先把要摸的东西定清楚。一组暖气片,只需要两件事:它是哪一间,表面多少度。
class Radiator {
private final String room;
private final int temp;
Radiator(String room, int temp) {
this.room = room;
this.temp = temp;
}
String room() { // 这是哪一间
return room;
}
int temp() { // 表面多少度,测温枪点的
return temp;
}
}
再定“一屋子暖气”该对外说什么。它不需要把家底摊开,只说一句:我这儿能一件一件往下走。
这句话 JDK 里已经有现成的,叫 Iterable:只要能给出一个迭代器,就能被 for 走一遍。
实现里,游标跟着“这一趟”走,不跟着房子走。
class Home implements Iterable<Radiator> {
private final Radiator[] rooms;
Home(Radiator... rooms) {
this.rooms = rooms;
}
public Iterator<Radiator> iterator() {
return new Walk();
}
private class Walk implements Iterator<Radiator> {
private int i; // 走到第几组,是“这一趟”的事
public boolean hasNext() {
return i < rooms.length;
}
public Radiator next() {
return rooms[i++];
}
}
}
i 放在 Walk 里,不放在 Home 里,是有原因的。一边自己从客厅这头摸过去,一边让家里人从卧室那头摸过来,两个“走到哪了”各记各的,互不干扰。要是把游标挂在房子上,两趟一块走,就会互相踩。
Iterator 接口,和它的 hasNext、next 两个方法,是 JDK 里现成的,二十多年了,形状没怎么变。我们只是照着它的样子用。走到头还调 next(),该抛 NoSuchElementException;这里省了。
游标是这一趟的,不是这间房子的
03 一套动作,走完所有
把家里的五组暖气排进去。
Home home = new Home(
new Radiator("客厅", 52),
new Radiator("主卧", 47),
new Radiator("次卧", 44),
new Radiator("厨房", 21),
new Radiator("卫生间", 19));
检查的时候,只管“一套动作”:
int cold = 0;
for (Radiator r : home) {
boolean warm = r.temp() >= 40;
System.out.println((warm ? "热 " : "凉 ") + r.room() + " " + r.temp() + "℃");
if (!warm) {
cold++;
}
}
System.out.println("→ " + cold + " 组不热,报物业");
跑出来是这样:
热 客厅 52℃
热 主卧 47℃
热 次卧 44℃
凉 厨房 21℃
凉 卫生间 19℃
→ 2 组不热,报物业
那句 for (Radiator r : home) 是迭代器的马甲。编译器把它展开,还是那三句:先问 hasNext(),再取 next(),取完接着问。
明年添一组书房,只加一行:
Home home = new Home(
new Radiator("客厅", 52),
new Radiator("主卧", 47),
new Radiator("次卧", 44),
new Radiator("厨房", 21),
new Radiator("卫生间", 19),
new Radiator("书房", 18)); // 新添一组,别的地方一个字没动
加一组暖气,是往队列里添一项,不是改一段方法。
游标往前走,动作不变:摸一下,记一笔
04 为什么叫迭代器
“迭代”两个字,本来是“一轮一轮重复”。同一套动作,对着不一样的东西,一遍一遍地来。
迭代器做的事,只有一件:把“下一件是什么”从“拿到之后干什么”里拆出去。
这事儿天天见。JDK 的 ArrayList、LinkedList,数据库查出来的 ResultSet 游标,读文件时一行一行往下读,接口分页一页一页拉——形状都一样:先问还有没有,再取下一样,取完接着问。
跟组合、访问者那两篇再对一句。组合(Day⑱)管的是“单个和一组长一个样”,树是它装东西的筐;迭代器管的是“怎么从筐里一件一件拿出来”。访问者(Day⑰)管的是“拿出来之后做什么,由外面递进来的单子说了算”;迭代器连“做什么”都不管,只管递。
所以有几条边界。
三五项、顺序也固定,直接下标 for 就行,别为它包一层。摸五组暖气,犯不上先造一台机器。
边走边改,别硬来。检查到一半,师傅上门换了一组,队列就变了。游标记的是“第几个”,中间塞进来一个,后面的顺序全错位。要是这五组装在 ArrayList 里,边遍历边往集合里加一组、删一组,下一轮就会抛 ConcurrentModificationException,提醒你别这么干。真要改,先把要动的记下来,走完一趟再动。
迭代器不排序、不筛选、不算业务。它只保证一件事:按我给的方式,一个不漏地走完一遍。
还有一句:把内部数组直接交出去,等于把家门钥匙给了别人。iterator() 交出去的是“走一遍的能力”,不是“进来随便翻”。
再把失真点挑明。
代码里五组暖气,各是各的。现实里,它们串在一根管上,串在一栋楼的水力平衡里。你家厨房这组不热,可能是楼上那户把阀门关了,也可能是一楼的总回水堵了。迭代器只能告诉你“这一组凉”,告诉不了你“为什么凉”。一趟检查,只查得到自己家门口。
还有一处:迭代器是“这一趟”的。走完这一趟,拿到的是这一趟的答案。暖气片的温度在变,管里的水在变,人也在变。昨天摸出来的结论,今天不能直接拿来用。
05 一屋子的热
下午又摸了一遍。
客厅烫手,主卧和次卧也热了。厨房和卫生间还是凉的。给物业打了电话,说一会儿统一过来看,让先别自己拆阀门。
师傅来的时候,从楼道立管顶端排了一回气。傍晚,屋里明显不一样了:客厅、卧室都热了,厨房和卫生间还差一点,说是明天再看。早上还得穿着外套,这会儿一件卫衣就够了。窗户开了一条缝,冷风进来,又被暖气顶回去。
我把手机备忘录里那行温度删了,重新记了一行:厨房、卫生间,待修。
跟前面二十一篇对一句:单例管造几个,工厂管怎么造,模板方法管事情怎么走,策略管这一步用什么做法,装饰器管外面加几层,命令管把请求做成对象,代理管谁能碰到真东西,观察者管变化后通知谁,外观管一堆零件只留一个入口,状态管当前状态下怎么做,责任链管请求由哪一环接住,原型管拿哪个现成对象复制,桥接管抽象和实现怎么独立扩展,享元管把能共享的部分只留一份,备忘录管把某个时刻的状态存下来、再原样恢复,生成器管把复杂对象的装配拆成一步一步、最后一次性成型,访问者管对一批稳定的元素做什么操作由递进来的单子说了算,组合管单个和一组长一个样,适配器管两套对不上的接口中间怎么搭一座桥,中介者管一堆对象互相说话时怎么都先经过一个中间人,解释器管一句判断怎么拆成能算的小句、再拼回去算。本篇迭代器,管怎么把一屋子东西一件一件、不漏不重地走完。
带走一句话:迭代器 = 把“下一个是谁”交出去,自己只管一个一个往下走。
屋子暖不暖,得一间一间摸过去。
暖气第一天,总有两组是凉的。急不得,得等水慢慢把整栋楼走一遍;日子也是,一天一天,才走到暖的地方。
参考资料
- 《设计模式:可复用面向对象软件的基础》(GoF 原著)——迭代器模式的原始定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示
- Refactoring Guru · Iterator——把遍历行为从聚合对象里抽出来,用游标逐个访问
- Java API · java.util.Iterator——
hasNext、next、remove的约定 - Java API · ArrayList——遍历期间结构被改动时抛
ConcurrentModificationException(快速失败)的原因