成都网站建设公,嵌入式开发方向,教师做网站赚钱,门户网站优化Error#xff1a;表示由JVM所侦测到的无法预期的错误#xff0c;由于这是属于JVM层次的严重错误#xff0c;导致JVM无法继续执行#xff0c;因此#xff0c;这是不可捕捉到的#xff0c;无法采取任何恢复的操作#xff0c;顶多只能显示错误信息。Exception#xff1a;表… Error表示由JVM所侦测到的无法预期的错误由于这是属于JVM层次的严重错误导致JVM无法继续执行因此这是不可捕捉到的无法采取任何恢复的操作顶多只能显示错误信息。Exception表示可恢复的例外这是可捕捉到的。
Java语言规范对这两个定义十分简单
将派生于Error或者RuntimeException的异常称为unchecked异常
所有其他的异常成为checked异常。 Use checked exceptions for recoverable conditions and runtime exceptions for programming errors (Item 58 in 2nd edition) 不过从这句话中我们可以简单引申一下也就是说如果出现了RuntimeException就一定是程序员自身的问题。比如说数组下标越界和访问空指针异常等等只要你稍加留心这些异常都是在编码阶段可以避免的异常。如果你还是觉得这两个概念不好区分那么“最暴力“的方法就是将常见的RuntimeException背下来这样就可以省去很多判断的时间。 Checked ExceptionCE的重要性
有几个我觉得很重要的具有突破性的语言特性Kotlin 并没有实现。另外我还发现一个很重要的 Java 特性被 Kotlin 的设计者给盲目抛弃了。这就是我今天要讲的主题checked exception。我不知道这个术语有什么标准的中文翻译为了避免引起定义混乱下文我就把它简称为“CE”好了。
先来科普一下 CE 到底是什么吧。Java 要求你必须在函数的类型里面声明它可能抛出的异常。比如你的函数如果是这样
void foo(string filename) throws FileNotFoundException
{if (...){throw new FileNotFoundException();}...
}Java 要求你必须在函数头部写上“throws FileNotFoundException”否则它就不能编译。这个声明表示函数在某些情况下会抛出 FileNotFoundException 这个异常。由于编译器看到了这个声明它会严格检查你对 foo 函数的用法。在调用 foo 的时候你必须使用 try-catch 处理这个异常或者在调用的函数头部也声明 “throws FileNotFoundException”把这个异常传递给上一层调用者。
try
{foo(blah);
}
catch (FileNotFoundException e)
{...
}这种对异常的声明和检查叫做“checked exception”。很多语言包括 CC#JavaScriptPython……都有异常机制但它们不要求你在函数的类型里面声明可能出现的异常类型也不使用静态类型系统对异常的处理进行检查和验证。我们说这些语言里面有“exception”却没有“checked exception”。
理解了 CE 这个概念下面我们来谈正事Kotlin 和 C# 对 CE 的误解。
Kotlin 的文档明确的说明它不支持类似 Java 的 checked exceptionCE指出 CE 的缺点是“繁琐”并且列举了几个普通程序员心目中“大牛”的文章想以此来证明为什么 Java 的 CE 是一个错误为什么它不解决问题却带来了麻烦。这些人包括了 Bruce Eckel 和 C# 的设计者 Anders Hejlsberg。
很早的时候我就看过 Hejlsberg 的这些言论。他的话看似有道理然而通过自己编程和设计语言的实际经验我发现他并没有抓住问题的关键。他的论述里有好几处逻辑错误一些自相矛盾还有一些盲目的臆断所以这些言论并没能说服我。正好相反实在的项目经验告诉我CE 是 C# 缺少的一项重要特性没有了 CE 会带来相当麻烦的后果。在微软写 C# 的时候我已经深刻体会到了缺少 CE 所带来的困扰。现在我就来讲一下CE 为什么是很重要的语言特性然后讲一下为什么 Hejlsberg 对它的批评是站不住脚的。
首先写 C# 代码时最让我头痛的事情之一就是 C# 没有 CE。每调用一个函数不管是标准库函数第三方库函数还是队友写的函数甚至我自己写的函数我都会疑惑这个函数是否会抛出异常。由于 C# 的函数类型上不需要标记它可能抛出的异常为了确保一个函数不会抛出异常你就需要检查这个函数的源代码以及它调用的那些函数的源代码……
也就是说你必须检查这个函数的整个“调用树”的代码才能确信这个函数不会抛出异常。这样的调用树可以是非常大的。说白了这就是在用人工对代码进行“全局静态分析”遍历整个调用树。这不但费时费力看得你眼花缭乱还容易漏掉出错。显然让人做这种事情是不现实的所以绝大部分时候程序员都不能确信这个函数调用不会出现异常。
在这种疑虑的情况下你就不得不做最坏的打算你就得把代码写成
try
{foo();
}
catch (Exception)
{...
}注意到了吗这也就是你写 Java 代码时能写出的最糟糕的异常处理代码因为不知道 foo 函数里面会有什么异常出现所以你的 catch 语句里面也不知道该做什么。大部分人只能在里面放一条 log记录异常的发生。这是一种非常糟糕的写法不但繁复而且可能掩盖运行时错误。有时候你发现有些语句莫名其妙没有执行折腾好久才发现是因为某个地方抛出了异常所以跳到了这种 catch 的地方然后被忽略了。如果你忘了写 catch (Exception)那么你的代码可能运行了一段时间之后当掉因为忽然出现一个测试时没出现过的异常……
所以对于 C# 这样没有 CE 的语言很多时候你必须莫名其妙这样写这种做法也就是我在微软的 C# 代码里经常看到的。问原作者为什么那里要包一层 try-catch答曰“因为之前这地方出现了某种异常所以加了个 try-catch然后就忘了当时出现的是什么异常具体是哪一条语句会出现异常总之那一块代码会出现异常……” 如此写代码自己心虚看的人也糊涂软件质量又如何保证
那么 Java 呢因为 Java 有 CE所以当你看到一个函数没有声明异常就可以放心的省掉 try-catch。所以这个 C# 的问题自然而然就被避免了你不需要在很多地方疑惑是否需要写 try-catch。Java 编译器的静态类型检查会告诉你在什么地方必须写 try-catch或者加上 throws 声明。如果你用 IntelliJ把光标放到 catch 语句上面可能抛出那种异常的语句就会被加亮。C# 代码就不可能得到这样的帮助。 CE 看起来有点费事似乎只是为了“让编译器开心”然而这其实是每个程序员必须理解的事情。出错处理并不是 Java 所特有的东西就算你用 C 语言也会遇到本质一样的问题。使用任何语言都无法逃脱这个问题所以必须把它想清楚。在《编程的智慧》一文中我已经讲述了如何正确的进行出错处理。如果你滥用 CE当然会有不好的后果然而如果你使用得当就会起到事半功倍提高代码可靠性的效果。
Java 的 CE 其实对应着一种强大的逻辑概念一种根本性的语言特性它叫做“union type”。这个特性只存在于 Typed Racket 等一两个不怎么流行的语言里。Union type 也存在于 PySonar 类型推导和 Yin 语言里面。你可以把 Java 的 CE 看成是对 union type 的一种不完美的丑陋的实现。虽然实现丑陋写法麻烦CE 却仍然有着 union type 的基本功能。如果使用得当union type 不但会让代码的出错处理无懈可击还可以完美的解决 null 指针等头痛的问题。通过实际使用 Java 的 CE 和 Typed Racket 的 union type 来构建复杂项目我很确信 CE 的可行性和它带来的好处。
现在我来讲一下为什么 Hejlsberg 对于 CE 的批评是站不住脚的。他的第一个错误俗话说就是“人笨怪刀钝”。他把程序员对于出错处理的无知不谨慎和误用怪罪在 CE 这个无辜的语言特性身上。他的话翻译过来就是“因为大部分程序员都很傻没有经过严格的训练不小心又懒惰所以没法正确使用 CE。所以这个特性不好是没用的”
他的论据里面充满了这样的语言
“大部分程序员不会处理这些 throws 声明的异常所以他们就给自己的每个函数都加上 throws Exception。这使得 Java 的 CE 完全失效。”“大部分程序员根本不在乎这异常是什么所以他们在程序的最上层加上 catch (Exception)捕获所有的异常。”“有些人的函数最后抛出 80 多种不同的异常以至于使用者不知道该怎么办。”……
注意到了吗这种给每个函数加上 throws Exception 或者 catch (Exception) 的做法也就是我在《编程的智慧》里面指出的经典错误做法。要让 CE 可以起到良好的作用你必须避免这样的用法你必须知道自己在干什么必须知道被调用的函数抛出的 exception 是什么含义必须思考如何正确的处理它们。
另外 CE 就像 union type 一样如果你不小心分析不假思索就抛出异常就会遇到他提到的“抛出 80 多种异常”的情况。出现这种情况往往是因为程序员没有仔细思考没有处理本来该自己处理的异常而只是简单的把下层的异常加到自己函数类型里面。在多层调用之后你就会发现最上面的函数累积起很多种异常让调用者不知所措只好传递这些异常造成恶性循环。终于有人烦得不行把它改成了“throws Exception”。
我在使用 Typed Racket 的 union type 时也遇到了类似的问题但只要你严格检查被调用函数的异常尽量不让它们传播严格限制自己抛出的异常数目缩小可能出现的异常范围这种情况是可以避免的。CE 和 union type 强迫你仔细的思考理顺这些东西之后你就会发现代码变得非常缜密而优雅。其实就算你写 C 代码或者 JavaScript这些问题是同样存在的只不过这些语言没有强迫你去思考所以很多时候问题被稀里糊涂掩盖了起来直到很长时间之后才暴露出来不可救药。
所以可以说这些问题来自于程序员自己而不是 CE 本身。CE 只提供了一种机制至于程序员怎么使用它是他们自己的职责。再好的特性被滥用也会产生糟糕的结果。Hejlsberg 对这些问题使用了站不住脚的理论。如果你假设程序员都是糊里糊涂写代码那么你可以得出无比惊人的结论所有用于防止错误的语言特性都是没用的因为总有人可以懒到不理解这些特性的用法所以他总是可以滥用它们绕过它们写出错误百出的代码所以静态类型没用CE 没用…… 有这些特性的语言都是垃圾大家都写 PHP 就行了 ;)
Hejlsberg 把这些不理解 CE 用法懒惰滥用它的人作为依据以至于得出 CE 是没用的特性以至于不把它放到 C# 里面。由于某些人会误用 CE结果就让真正理解它的人也不能用它。最后所有人都退化到最笨的情况大家都只好写 catch (Exception)。在 Java 里至少有少数人知道应该怎么做在 C# 里所有人都被迫退化成最差的 Java 程序员 ;)
另外Hejlsberg 还指出 C# 代码里没有被 catch 的异常应该可以用“静态分析”检查出来。可以看出来他并不理解这种静态检查是什么规模的问题。要能用静态分析发现 C# 代码里被忽略的异常你必须进行“全局分析”也就是说为了知道一个函数是否会抛出异常你不能只看这个函数。你必须分析这个函数的代码它调用的代码它调用的代码调用的代码…… 所以你需要分析超乎想象的代码量而且很多时候你没有源代码。所以对于大型的项目这显然是不现实的。
相比之下Java 要求你对异常进行 throws 显式声明实质上把这个全局分析问题分解成了一个个模块化modular的小问题。每个函数作者完成其中的一部分调用它的人完成另外一部分。大家合力帮助编译器高效的完成静态检查防止漏掉异常处理避免不必要的 try-catch。实际上像 Exceptional 一类的 C# 静态检查工具会要求你在注释里写出可能抛出的异常这样它才能发现被忽略的异常。所以 Exceptional 其实重新发明了 Java 的 CE只不过 throws 声明被写成了一个注释而已。
说到 C#其实它还有另外一个特别讨厌的设计错误引起了很多不必要的麻烦。感兴趣的人可以看看我这篇文章《可恶的 C# IDisposable 接口》。这个问题浪费了整个团队两个月之久的时间。所以我觉得作为 C# 的设计者Hejlsberg 的思维局限性相当大。我们应该小心的分析和论证这些人的言论不应该把他们作为权威而盲目接受以至于让一个优秀的语言特性被误解不能进入到新的语言里。
结论
所以我对 Kotlin 是什么“结论”呢我没有结论这篇文章就像我所有的看法一样仅供参考。显然 Kotlin 有的地方做得比 Java 好所以它不会因为没有 CE 而完全失去意义。我不想打击人们对新事物的兴趣我甚至鼓励有时间的人去试试看。
我知道很多人希望我给他们一个结论到底是用一个语言还是不用它这样他们就不用纠结了然而我并不想给出一个结论。一来是因为我不想让人感觉我在“控制”他们如何看待一个东西是他们的自由是否采用一个东西是他们自己的决定。二来是因为我还没有时间和机会去用 Kotlin 来做实际的项目。另外我早就厌倦了试用新的语言如果一个大众化的语言没有特别讨厌不可原谅的设计失误我是不会轻易换用新语言的。我宁愿让其他人做我的小白鼠去试用这些新语言。到后来我有空了再去看看他们的成功或者失败经历 :P
所以对我个人而言我至少现在不会去用 Kotlin但我并不想让其他人也跟我一样。因为 JavaC 和 C 已经能满足我的需求它们相当稳定而且我对它们已经很熟悉所以我为什么要花精力去学一个新的语言去折腾不成熟的工具放下我真正感兴趣的算法和数据结构等问题呢实际上不管我用什么语言写代码我的头脑里都在用同一个语言构造程序。我写代码的过程只不过是在为我脑子里的“万能语言”找到对应的表达方式而已。