一、参考为什么不建议用 try catch
try catch机制非常好。那些觉得try catch不行的人,是他们自己的水平有问题,无法理解这种机制。并且这群人写代码不遵守规则,喜欢偷懒,这才造成try catch不好的错觉。
详细解释:
1.程序要健壮,必须要设计报错机制。
最古老,也是最常见的,比如:
bool CreateFile( );
//如果创建文件失败就返回false,否则返回true。
这种报错方式,显然不好。因为它没有给出产生错误的具体原因。
2.改进:一个函数或过程,会因为不同的原因产生错误,报错机制必须要把这些错误原因进行区分后,再汇报。
比如:
int CreateFile():
//如果创建成功就返回1.
//如果是因为没有权限,导致失败,返回-1。
//如果是因为文件已经存在,导致失败,返回-2。
//如果是因为创建文件发生超时,导致失败,返回-3。
这样看上去,比【1】要好些,至少指出了比较具体的失败原因,但是,还不够。
3.很多情况下,函数需要把详细的原因,用字符串的方式,返回:
<pre>
class Result
{
....int State;//同【2】
....string ErrorMessage;//如果失败,这里将给出详细的信息,如果有可能,应该把建议也写上去。
}
Result CreateFile();
//如果创建成功,返回的Result,State为1,ErrorMessage为null。
//如果是因为没有权限,导致失败,返回的Result,State为-1,ErrorMessage为"用户【guest】没有权限在【C:\】这个目录下创建该文件。建议您向管理员申请权限,或者更换具有权限的用户。"。
//如果是因为文件已经存在,导致失败,返回的Result,State为-2,ErrorMessage为"文件【C:\abc.txt】已经存在。如果需要覆盖,请添加参数:arg_overwrite = true"。
//如果是因为创建文件发生超时,导致失败,返回的Result,State为-3,ErrorMessage为"在创建文件时超时,请使用chkdsk检查文件系统是否存在问题。"。
</pre>
4.我个人推崇上面这种方式,完整,美观。但是这种流程,容易与正常的代码混在一起,不好区分开。因此,Java、C#等设计了try catch这一种特殊的方式:
<pre>
void CreateFile()
//如果创建成功就不会抛出异常。
//如果是因为没有权限,导致失败,会抛出AccessException,这个Exception的Msg属性为"用户【guest】没有权限在【C:\】这个目录下创建该文件。建议您向管理员申请权限,或者更换具有权限的用户。"。
//如果是因为文件已经存在,导致失败,会抛出FileExistedException,这个Exception的Msg属性为"文件【C:\abc.txt】已经存在。如果需要覆盖,请添加参数:arg_overwrite = true"。
//如果是因为创建文件发生超时,导致失败,会抛出TimeoutException,这个Exception的Msg属性为"在创建文件时超时,请使用chkdsk检查文件系统是否存在问题。"。
</pre>
可见,上述机制,实际上是用不同的Exception代替了【3】的State。
这种机制,在外层使用时:
<pre>
try
{
....CreateFile( "C:\abc.txt" );
}
catch( AccessException e )
{
....//代码进入这里说明发生【没有权限错误】
}
catch( FileExistedException e )
{
....//代码进入这里说明发生【文件已经存在错误】
}
catch( TimeoutException e )
{
....//代码进入这里说明发生【超时错误】
}
</pre>
对比一下【3】,其实这与【3】本质相同,只是写法不同而已。
5.综上,我个人喜欢【3】这类面向过程的写法。但很多喜欢面向对象的朋友,估计更喜欢【4】的写法。然而【3】与【4】都一样。这两种机制都是优秀的错误处理机制。
6.理论说完了,回到正题,题注问:为什么不用try catch?
答:这是因为,很多菜鸟,以及新手,他们是这样写代码的:
void CreateFile( )
//无论遇到什么错误,就抛一个 Exception,并且也不给出Msg信息。
这样的话,在外层只能使用:
<pre>
try
{
....CreateFile( "C:\abc.txt" );
}
catch( Exception e )
{
....//代码进入这里说明发生错误
}
</pre>
当出错后,只知道它出错了,并不知道是什么原因导致错误。这同【1】。
以及,即使CreateFile是按【4】的规则设计的,但菜鸟在外层是这样使用的:
<pre>
try
{
....CreateFile( "C:\abc.txt" );
}
catch( Exception e )
{
....//代码进入这里说明发生错误
....throw Exception( "发生错误" )
}
</pre>
这种情况下,如果这位菜鸟的同事,调用了这段代码,或者用户看到这个错误信息,也只能知道发生了错误,但并不清楚错误的原因。这与【1】是相同的。
出于这些原因,菜鸟的同事,以及用户,并没有想到,造成这个问题是原因菜鸟的水平太差,写代码图简单省事。他们却以为是try catch机制不行。
因此,这就导致了二逼同事,以及傻比用户,不建议用try catch。
二、参考项目中到处都是try-catch是一种常态吗
通常try/catch适用于以下场景:
- 在代码中对可预见而又无法掌控的情况进行处理。比如在SOCKET BIND时发现端口已经被占用了、或者IO在打开文件时发现文件不存在,就需要在catch中做适当的处理避免程序crash掉;
- 将问题向更上一层面传递,将处理权让渡给caller。假如你写了个ORM FRAMEWORK,在delete的时候发现外键关联删除失败,FRAMEWORK不能擅自替上层的代码决定该怎么办,于是只好把DB的报的错误原样(或者加层外衣)throw出来,调用者根据业务需要选择处理方式;
除此之外,所有问题应该由程序员主动判断,就地解决。在规模比较大的软件中,定义自己的Exception体系并正确、克制地使用try/catch,可以让代码变得易读易维护还美观。
传递给上层来解决例子如下:
<pre>
void handlearray(int a[]) throws Npe
{
if(a==null)
throw new Npe();
a[0]……//处理部分
}
上层:
try{
handlearray(a);
}catch(E… e)
{
//对a进行处理。
}
</pre>
这时候传入数组为空,这个错误不是你当前这个函数所能处理的,只能是抛给上层,也就是生成这个数组,或者能对这个数组负责的那部分代码,让上层去处理,上层去try cacth,并在catch中对异常处理,类库中类似的像文件io的时候很多读写类都会抛出FileNotFoundException,也是一个道理,当上层给我一个找不到的文件,那在我的io类中肯定无法处理你这个异常,只能抛到给我这个文件的那一层,让那一层的代码对这个问题进行反应。
当然有些时候不需要,比如:
<pre>
void makearray(int a[])
{
a=new int[];
……//生成部分
if(a==null)
……//处理部分,此处一般不用抛异常,直接可以在这一层处理掉。
}
</pre>
像这个就不一样了,因为这次发生的问题是在我这一层代码所能控制之内的,所以我直接把问题处理掉就好了,没必要给上层了。
为什么讲“正确”并“克制”地使用?因为有些又蠢又懒的程序员喜欢这么干:
- 将函数所有代码都放到try{}之中,哪怕 int i = 1这种赋值的都不放过。然后在catch里输出一个错误信息就万事大吉。这样看起来是很省心哇,不用动脑子去分析哪里可能发生什么错误,反正所有错误都在catch的掌控之中;
- 用try/catch来控制流程。举个简单的例子,假设有这么个要求:
将字符串转换成数字,并返回该数字的绝对值,如果出错了就返回-1. 于是乎,就能见到类似下面代码的奇葩做法:
<pre>
int parse_number(const char* s){
try{
return abs(atoi(s));
}catch(Exception){
return -1;
}
}
</pre>
这多省事儿,不用考虑s是不是NULL、不用考虑s是不是包含非数字的字符、不用考虑s是不是超出int的取值范围...我是个优秀的程序员耶~~,我的代码好简洁。
try/catch和errno可以结合起来使用,二者不是非此即彼的关系,比如在某些场景下,可以将不确定的错误简化归纳为固定的errno输出,调用者直接检查返回的errno即可,简化了代码,也减轻了负担。比如某函数,成功返回0,失败返回-1:
<pre>
int foo(double d){
try{
do_something(d);
return 0;
}catch(Exception){
return -1;
}
}
void bar(double d){
int result = foo(d);
if(result == -1) return;
do_next_steps();
}
</pre>
三、细节
Java中try,catch,finally的用法
Java异常处理的组合方式:
1.try+catch
运行流程:运行到try块中,如果有异常抛出,则转到catch块去处理。然后执行catch块后面的语句
2.try+catch+finally
运行流程:运行到try块中,如果有异常抛出,则转到catch块,catch块执行完毕后,执行finally块的代码,再执行finally块后面的代码。
如果没有异常抛出,执行完try块,也要去执行finally块的代码。然后执行finally块后面的语句
Java finally语句到底是在return之前还是之后执行?
网上有很多人探讨Java中异常捕获机制try...catch...finally块中的finally语句是不是一定会被执行?很多人都说不是,当然他们的回答是正确的,经过我试验,至少有两种情况下finally语句是不会被执行的:
(1)try语句没有被执行到,如在try语句之前就返回了,这样finally语句就不会执行,这也说明了finally语句被执行的必要而非充分条件是:相应的try语句一定被执行到。
(2)在try块中有System.exit(0);这样的语句,System.exit(0);是终止Java虚拟机JVM的,连JVM都停止了,所有都结束了,当然finally语句也不会被执行到。
四、检查型异常和非检查型异常
上图摘自Java 进阶 之 检查型异常与非检查型异常
- 直接继承 Exception 的异常,属于检查型异常,必须用try语句块进行处理或者把异常交给上级方法处理总之就是必须写代码处理它。如IOException,SQLException
- 继承自Runtime Exception或 Error 的是非检查型异常,可以不用捕获
五、throw 和 throws区别
1、throws出现在方法函数头;而throw出现在函数体。
2、throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。
3、两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。