论坛首页 Java版

你擦了吗?确定擦了?真的确定擦了?

浏览 64112 次
该帖已经被评为精华帖
作者 正文
时间:2005-06-07
java的try-finally给我们提供了一个“保证某个动作必然执行”的机会。

一个try-finally结构,只要try块开始执行了,finally块里面的代码保证执行一次并且只有一次。
打个比方,就象你上厕所,只要你一旦开始拉了,我们保证无论如何,是拉稀了也好,放屁了也罢,最终你肯定是擦了屁股走出卫生间。


应用try-finally,我们可以在异常满天飞的程序里保证我们的关键资源被按时正确清理。一个最常见的应用就是jdbc的Connection, Statement, ResultSet等。

但是,我最近惊奇地发现,不知道怎么正确清理资源的人大有人在,即使是一些java老手。

看一个例子先:

[code:1]void f(){
Connection conn = ...;
Statement stmt = conn.createStatement();
ResultSet rset = ...;
...
}[/code:1]
典型的jdbc程序。但是也是典型的光着屁股,其臭如兰地走出厕所的典范。哎,你擦屁股了吗?
有的哥们振振有辞:我不用管,我的jdbc driver/我的应用服务器/garbage collector会处理的。
这是典型的糊涂蛋逻辑。没有close(),jdbc driver, 应用服务器怎么知道你是拉完了,还是光着屁股出去接个电话先?难不成这driver都智能地会算命了?
garbage collector倒确实管得了。不过,garbage collector不一定运行啊。你要是有10G得内存,要是你的程序就用了10M,garbage collector说不定就一直睡大觉。而且,就算它管,也许等你光着屁股上班被警察抓起来之后才匆匆赶到,你等的起吗?


好,有人说,那我擦,我擦,我擦擦擦。行了吧?


[code:1]void f(){
Connection conn = ...;
Statement stmt = conn.createStatement();
ResultSet rset = ...;
rset.close();
conn.close();
...
}[/code:1]

呵呵。我的傻哥们,你只擦了靠近后背的那三公分,剩下的嘛,别人看不见你就乐得省土块儿了是么?

按jdbc标准,ResultSet, Statement, Connection都要close(),也许有的driver会在Connection关闭的时候同时正确清理ResultSet, Statement,但是,并没有一条规定让所有的driver都这么做。
另外,也许你的Connection是从一个池里面来的,它只是回到池中去,如果你不关闭Statement, ResultSet,下一个拿到这个Connection的人也许就倒霉了!
做事要有始有终,既然开始擦了,就擦干净点儿,行不?(那个,谁谁谁,借我个防毒面具先!)


ok,有个讲卫生的小傻子这样擦:


[code:1]void f(){
Connection conn = ...;
Statement stmt = conn.createStatement();
ResultSet rset = ...;
rset.close();
stmt.close();
conn.close();
...
}[/code:1]

然后洋洋得意地说:我是好孩子,我天天擦屁屁。


是啊,多听话的孩子呀。可惜,某天,这孩子正坐在马桶上美着呢,妈妈喊了嗓子:二傻子,吃饭啦。
哦!吃饭。二傻子裤子都没提就窜出来了,熏得妈妈一个跟头。

什么问题,傻子做事一根筋,不能打扰,一旦有异常情况出现,屁股就忘了擦了。



所以,我这里郑重提醒大家,请用"try-finally"!它独有凹槽,防止侧漏...(糟了,串台了)

是啊,java老手们都不是傻子,都知道用try-finally的,可是,别美,你现在就保不齐擦没擦屁股呢!


常见擦法:
[code:1]void f(){
Connection conn = null;
Statement stmt = null;
ResultSet rset = null;
try{
conn = ...;
stmt = ...;
rset = ...;
...
}
finally{
if(rset!=null)rset.close();
if(stmt!=null)stmt.close();
if(conn!=null)conn.close();

}
}[/code:1]

嗯。怎么说呢。挺聪明的。都学会if(xxx!=null)这种传说中条件判断的上古绝学了。
可惜,你屁股大,一张纸不够,你用了第一张纸,满意地看着它圆满地完成了金灿灿的任务,再用第二张,靠,只太薄,破了,一手金灿灿地,象带了个金戒指。你大怒,起,绝尘而去。于是也忘了第三张纸,
哥们儿,close()是可以出异常的,你rset关了,stmt.close()出现了异常,但是conn就不管了?


近日有位室外高人,据说是鬼谷子高徒,鉴于怜我世人,不擦屁股的实多的高尚情操,亲手赚写一本绝世擦功秘籍,其文美,其意高,除了擦不干净之外,真可以说是称霸擦林。

[code:1]
void close(Connection conn){
try{
if(conn!=null) conn.close();
}
catch(Exception e){
e.printStackTrace();
}
}
void close(ResultSet rset){
...
}
void close(Statement rset){
...
}
void f(){
Connection conn = null;
Statement stmt = null;
ResultSet rset = null;
try{
conn = ...;
stmt = ...;
rset = ...;
...
}
finally{
close(rset);
close(stmt);
close(conn);

}
}[/code:1]

哈,你们不能纸擦破了就不接着擦啊,甚至大而化之,不能擦股用具有了问题就半途而废呀!

具信,该高人以此法擦遍天下凡十数载,未有擦而无功者。

可惜,高人却忽视了,除了纸会出故障,甚至大而化之,一切擦具(如土块儿,木条儿,手指)都可能出现故障,还有别的地方也会出故障地!
除了Exception,还有Error啊,我的高人!如果close(rset)抛了一个Error,你的close(stmt), close(conn)不都歇菜了?

后来,高人在《绝世武功补遗》里面解释说:Error代表不可恢复错误,说明整个排泄大业都受阻了,所以根本不应该试图对这种情况做任何处理,你也处理不了(自然也隐含此时你也根本无法擦屁股了的论断)。任何试图在这种情况下仍然固执擦屁股的做法都是倒行逆施,螳臂当车,必然被历史的车轮所撵碎。

此书一处,天下辟易。其革命性之深远,难以估量。具有关方面评论,Sun这个公共厕所的try-finally这个工具的设定本身就是不合理的,应该被历史车轮撵碎的,因为try-finally居然试图在出现Error的时候去做一些事情!是可忍,孰不可忍?
可以预见,try-finally将被sun彻底废弃,并且向广大公众做公开道歉以检讨多年来的欺骗造成的恶劣影响。
另外,公厕的构造也受到质疑,因为一旦有一个拉客在擦的时候某一步无可挽回地失败(比如,太紧张,手一抖,纸掉到了坑里,又死活伸手捞不着),那么他就大摇大摆不再继续擦,而如果碰巧此人刚吃了萝卜,就会把整个厕所里的其它拉客都熏得无法继续。(想想一个app server吧。你一个程序歇菜,乐得请病假不擦了,别人也跟着倒霉?)


嘿嘿,那么,你擦了吗?你肯定你擦了?擦干净了?

幸好,我们翻遍上古秘籍,最终在北京山顶洞人的失传宝典《呼呼,擦!》中发现了一个据称绝对干净的擦法,那就是------------

一下一下擦!

具体操作办法如下:

[code:1]
void f(){
final Connection conn = ...;
try{
final Statement stmt = ...;
try{
final ResultSet rset = ...;
try{
...
}
finally{rset.close();}
}
finally{stmt.close();}
}
finally{conn.close();}
}[/code:1]


其诀窍就是,每建立一个需要清理的资源,就用一个try-finally来保证它可以被清理掉。

如此,任何时候,你都是屁股干干静静地离开卫生间。


哪。好多圣人门徒跟我说:这样一下一下擦,姿势非常不雅观(看看那嵌套的try块吧),有违古礼。我们反对!


靠,你说孔丑儿古还是山顶洞人古??
屁股还泛着味儿呢,还拽什么“雅”?

而且,要是死要面子,也可以拉个帘子,擦的时候别让人看见嘛。比如:

[code:1]
interface ResultListener{
void call(ResultSet rset);
}
class SqlReader{
void read(ResultListener l){
final Connection conn = ...;
try{
final Statement stmt = ...;
try{
final ResultSet rset = ...;
try{
l.call(rset);
}
finally{rset.close();}
}
finally{stmt.close();}
}
finally{conn.close();}
}
}[/code:1]

这一下一下擦的动作都藏在SqlReader这个帘子里,你直接在ResultListener里面拉不就行了?

那位高人说了,这太复杂,就为了擦个屁股不值。

这个嘛,值不值的另说,你那个简单,就是简简单单地擦不干净屁股。要不您干脆别擦得了,更简单呢还。反正您出门儿就愣说擦的是Chanel香水儿就是了。有啥比瞪眼儿说白话儿简单?

对了, 我还忘了一个条款:
就是擦屁股的时候按顺序擦。谁进厕所的,要让人家出去。

“什么狗屁规则?“那位问了。

这个这个--,啊,你猜猜~~~?

嗯,对了,是这样的,上厕所都不着急,姗姗来迟,上课更不着急,更喜欢迟到了,对不对?而谁上课天天迟到早退还不担心毕业?当然是太子党了,是不?
人家都太子党了,你还不让人家先出去?活腻味了你?(此处尾音要拉长,而且向上拐)


反正啊,具体说,ResultSet最后创建,但是要先关。

Statement其次。Connection最后。


当然了,也许在你的环境下,次序错了也没出事情。但是,咱么吃软饭的(吃软件这口饭的)图啥?不就图个放心吗?上厕所图啥?不就图个别让太子党抓去当兔子吗?
也许某个driver对次序不敏感,但是不好说哪天你换个环境就忽然她奶奶的敏感了呢?
比如吧,你有connection pool, conn.close()把connection返回到connection。

你要是先conn.close(),好嘛,connection先回到pool了,正好别的线程里面等着要connection,立马这个connection又给分配出去了。
这下齐了,你statement, resultset还没关呢,那边事故单位领导就找上门了。什么香油油的桌子,什么桐油炸丸子,全给你送来了。这不添堵吗?

好在,在我们《呼呼,擦!》宝典中记载的“一下一下擦”神功,老少咸宜,童叟无欺,有道是:法擦大法好,不如法擦冰箱好!

跑题了。反正是,只要你一个try-finally对应一个资源,你就不可能在次序上出错。自然而然的就是后入先出的堆栈结构。
反观别的擦法,就没有这个效果,次序如何,全靠你自己掌握。弄错了,系统也不告诉你。等着吃桐油炸丸子吧。

这也是我们推广一下一下擦的一个原因。
   
时间:2005-06-07
难道就没有其它的更好的比喻了吗?
   
0 请登录后投票
时间:2005-06-07
ajoo以前在回复一个讨论“自制持久层”的贴子的时候,写过类似的例子,给我的印象很深刻。我完全同意这个原则,资源释放一定要到位。
这次ajoo整理了一个长篇的独立帖子。希望能够对更多的人起指导作用。(举的例子实在不雅。为了让大家阅读的时候,有审美的快乐,而不是一些不快的想象。建议修改。:lol: )

为了清晰,也可以分成几个函数,每个函数就释放自己开辟的资源:

findResults(){
Connection con = null;
try{
con = getConnection();
return findResults();
}finally{
if(con != null) con.close();
}
}

findResults( Connection con){
PreparedStatement ps = null;

try {
ps = con.prepareStatement(...);
findResults(ps);
}finally{
if(ps != null) ps.close();
}
}

findResults(PreparedStatement ps){
ResultSet rs = null;

try {
rs = ps.executeQuery();
loop(rs);
}finally{
if(rs != null) rs.close();
}
}

其实,rs 属于 ps, ps 属于 con.
ps 一关闭,下面的rs 都关闭了 (cached rowset 除外)。
con 一关闭,下面的ps都关闭了。
(这里的关闭,指真正的物理关闭)

但由于涉及到各种con pool, ps pool的复杂情况,还是建议一个一个关闭。
   
0 请登录后投票
时间:2005-06-07
还好是在午饭前看, 不然后果难以想象.
另外, 建议换一个较能体现中心思想的Title. 刚看到这个Title时以为自己逛到开阔天空了.
   
0 请登录后投票
时间:2005-06-07
看到ajoo的东西每次都是很有收获,这里的问题能否归结为过渡使用异常所造成的呢?

客户程序员习惯性的把一些东西包在一个大的try .. catch .. finally 中.

但是他容易忘了,在这段代码中,程序会处于多个状态,正如ajoo例子中所出现的 conn、statement、 resultset, 简单的大的try catch finally 是无法区分这些状态的。

所以有时候我习惯退而求其次,还是用返回值吧,用异常很容易让客户程序员麻木出错的。

可现在没有能够强制 调用者检查函数返回值的 手段, 。。。

ajoo很习惯用listener 注册啊 :)
   
0 请登录后投票
时间:2005-06-07
在C++中,对资源管理是不是 就方便多了啊? 嘿嘿。 析构函数,可java没有,垃圾收理看来也不能解决一切问题啊。
   
0 请登录后投票
时间:2005-06-07
估计在现有直接用jdbc的程序中,99.9%都是没擦干净滴

jinfeng_Wang 写道
看到ajoo的东西每次都是很有收获,这里的问题能否归结为过渡使用异常所造成的呢?

客户程序员习惯性的把一些东西包在一个大的tyr .. catch .. finally 中.

但是他容易忘了,在这段代码中,程序会处于多个状态,正如ajoo例子中所出现的 conn、statement、 resultset, 简单的大的tyr catch finally 是无法区分这些状态的。

所以有时候我习惯退而求其次,还是用返回值吧,用异常很容易让客户程序员麻木出错的。

可现在没有能够强制 调用者检查函数返回值的 手段, 。。。

ajoo很习惯用listener 注册啊 :)

啥listener?8就是个callback嘛

jinfeng_Wang 写道

在C++中,对资源管理是不是 就方便多了啊? 嘿嘿。 析构函数,可java没有,垃圾收理看来也不能解决一切问题啊。

不知所云~
   
0 请登录后投票
时间:2005-06-07
ajoo太逗了,前无古人的擦大便理论,给官僚的IT论坛吹来了一股特别的气息,而我就拿着扇子在旁边煽。
   
0 请登录后投票
时间:2005-06-07
jinfeng_Wang 写道
在C++中,对资源管理是不是 就方便多了啊? 嘿嘿。 析构函数,可java没有,垃圾收理看来也不能解决一切问题啊。


这叫什么话。析构函数不是需要程序员手动delete才能调用的吗?这种手动调用,和书写合理的finally块不是一个性质的吗?谈何“方便多了”?
   
0 请登录后投票
时间:2005-06-07
B1-66-ER 写道
jinfeng_Wang 写道
在C++中,对资源管理是不是 就方便多了啊? 嘿嘿。 析构函数,可java没有,垃圾收理看来也不能解决一切问题啊。


这叫什么话。析构函数不是需要程序员手动delete才能调用的吗?这种手动调用,和书写合理的finally块不是一个性质的吗?谈何“方便多了”?

垃圾收集确实没有解决一切问题。就象你用的是自动马桶,你一站起来它就自动冲水。可惜,你的屁股却属于“托管资源”,马桶把它委托给你自己管理了。

java中,jdbc资源,socket,文件,线程等都是托管资源,程序要自己管理的。


c++呢。jinfeng说的是析构函数的自动调用这个魔鬼,比如:
[code:1]
Connection conn(...);
Statement stmt(...);
ResultSet rset(...);
...[/code:1]

简单哈。函数一退出,资源都自动被清理了,不用头疼finally了。

呵呵。魔鬼给了你一个棒棒糖你就把灵魂卖了?

看看魔鬼用这个语法棒棒糖买走了些什么吧:

1。多态。java的Connection等都是接口。但是在c++下,它们必须是一个具体的类,没有多态可谈。什么接口和实现的分离,想也别想!

2。你只要一站起来,马桶就自动拿个刷子给你刷阿刷。什么?你说你就是坐累了,想站起来伸个懒腰?少废话,站起来你就是拉完了。擦了之后赶紧滚蛋!
自动析构?听着不错,可惜是强迫的。在java里面我可以根据情况不关闭某个资源(比如,我把Connection返回出去,因为我是个ConnectionFactory)

3。其实要返回Connection也可以,老老实实地拷贝吧。就是说,先把你擦完,撵出去,然后你自己再给看门儿的大妈两毛钱重新来过。(乐起,刘欢豪迈的声音响彻卫生间:只不过是从头再来...)

4。人身安全。你肯定不甘心到处拷贝,拉一趟屎花一百块钱,所以你必然在一些地方用指针来绕过马桶和看门大妈的双重监视。比如ResultSet里面一般会有Statement的指针,Statement里面一般会友Connection的指针。
这些指针省钱了,但是却有了无数个出线dangling pointer的可能性。而一旦出现了dangling pointer,老哥,你就不是多花两毛钱的问题了,很可能你就被直接掉粪坑里了。马桶一冲,你就不知道上哪去了。(也许是象nemo一样进入大海遨游了吧,祝你成仙啊)


scope guard倒是看上去不错,除了scope guard的实现用了恶心的hack之外。
可惜,scope guard需要你定义functor。那就跟anonymous class甚至比之还要麻烦。对了,你还可以用lamda拉,FC++拉来绕,玩儿的就是心跳嘛!



其实,说来说去,都是语法棒棒糖。c++的这个糖衣炮弹也实在不怎么样。
c#的using比try-finally甜些。虽然也还是要一下一下擦。


理想的糖应该这样的:

[code:1]
auto Connection conn = ...;
auto Statement stmt = ...;
auto ResultSet rset ...;
...[/code:1]
   
0 请登录后投票
论坛首页 Java版

跳转论坛:
JavaEye推荐