多年来,我一直无法得到以下问题的一个像样的答案:为什么一些开发人员如此反对受控异常?我有过无数次的对话,在博客上读过一些东西,读过Bruce Eckel说的话(我看到的第一个站出来反对他们的人)。
我目前正在编写一些新代码,并非常注意如何处理异常。我试图了解那些“我们不喜欢受控异常”的人的观点,但我仍然看不出来。
我的每一次谈话都以同样的问题结束。让我把它建立起来:
一般来说(从Java的设计方式来看),
Error is for things that should never be caught (VM has a peanut allergy and someone dropped a jar of peanuts on it)
RuntimeException is for things that the programmer did wrong (programmer walked off the end of an array)
Exception (except RuntimeException) is for things that are out of the programmer's control (disk fills up while writing to the file system, file handle limit for the process has been reached and you cannot open any more files)
Throwable is simply the parent of all of the exception types.
我听到的一个常见的说法是,如果发生了异常,那么所有开发人员要做的就是退出程序。
我听到的另一个常见论点是受控异常使得重构代码更加困难。
对于“我要做的就是退出”的参数,我说即使你退出了,你也需要显示一个合理的错误消息。如果你只是在处理错误上押注,那么当程序在没有明确说明原因的情况下退出时,你的用户不会太高兴。
对于“它使重构变得困难”的人群来说,这表明没有选择适当的抽象级别。与其声明方法抛出IOException,不如将IOException转换为更适合当前情况的异常。
我对用catch(Exception)(或在某些情况下用catch(Throwable)包装Main没有问题,以确保程序可以优雅地退出-但我总是捕获我需要的特定异常。这样做至少可以显示适当的错误消息。
人们从来不会回答的问题是:
如果抛出RuntimeException
子类代替异常
子类,你怎么知道
你应该去抓?
如果答案是catch Exception,那么您也在以与系统异常相同的方式处理程序员错误。在我看来这是不对的。
如果你捕获Throwable,那么你是在以同样的方式处理系统异常和虚拟机错误(等等)。在我看来这是不对的。
如果答案是您只捕获您知道抛出的异常,那么您如何知道抛出了哪些异常呢?当程序员X抛出一个新的异常而忘记捕获它时会发生什么?这对我来说似乎很危险。
我认为显示堆栈跟踪的程序是错误的。不喜欢受控异常的人不会有这种感觉吗?
所以,如果你不喜欢受控异常,你能解释一下为什么不,并回答没有得到回答的问题吗?
我不是在寻求什么时候使用这两种模型的建议,我想知道的是为什么人们从RuntimeException扩展,因为他们不喜欢从Exception扩展,和/或为什么他们捕获一个异常然后重新抛出一个RuntimeException,而不是将抛出添加到他们的方法中。我想了解不喜欢受控异常的动机。
这并不是反对受控异常的纯概念,但是Java用于受控异常的类层次结构是一个畸形秀。我们总是简单地称这些东西为“异常”——这是正确的,因为语言规范也这样称呼它们——但是异常在类型系统中是如何命名和表示的呢?
By the class Exception one imagines? Well no, because Exceptions are exceptions, and likewise exceptions are Exceptions, except for those exceptions that are not Exceptions, because other exceptions are actually Errors, which are the other kind of exception, a kind of extra-exceptional exception that should never happen except when it does, and which you should never catch except sometimes you have to. Except that's not all because you can also define other exceptions that are neither Exceptions nor Errors but merely Throwable exceptions.
哪些是“已检查”异常?可抛出异常是受控异常,除非它们也是错误,是未检查的异常,然后是异常,也是可抛出异常,是受控异常的主要类型,除了有一个例外,那就是if它们也是runtimeexception,因为那是另一种未检查的异常。
What are RuntimeExceptions for? Well just like the name implies, they're exceptions, like all Exceptions, and they happen at run-time, like all exceptions actually, except that RuntimeExceptions are exceptional compared to other run-time Exceptions because they aren't supposed to happen except when you make some silly error, although RuntimeExceptions are never Errors, so they're for things that are exceptionally erroneous but which aren't actually Errors. Except for RuntimeErrorException, which really is a RuntimeException for Errors. But aren't all exceptions supposed to represent erroneous circumstances anyway? Yes, all of them. Except for ThreadDeath, an exceptionally unexceptional exception, as the documentation explains that it's a "normal occurrence" and that that's why they made it a type of Error.
Anyway, since we're dividing all exceptions down the middle into Errors (which are for exceptional execution exceptions, so unchecked) and Exceptions (which are for less exceptional execution errors, so checked except when they're not), we now need two different kinds of each of several exceptions. So we need IllegalAccessError and IllegalAccessException, and InstantiationError and InstantiationException, and NoSuchFieldError and NoSuchFieldException, and NoSuchMethodError and NoSuchMethodException, and ZipError and ZipException.
只不过,即使检查了异常,也总有(相当简单的)方法可以欺骗编译器,在不检查的情况下抛出异常。如果你这样做,你可能会得到一个不确定的throwableexception,除非在其他情况下,它可能抛出一个意外的dexception,或一个未知的异常(与未知的错误无关,只针对“严重的异常”),或一个ExecutionException,或一个InvocationTargetException,或一个ExceptionInInitializerError。
哦,我们一定不能忘记Java 8时髦的新UncheckedIOException,这是一个RuntimeException异常,设计用来通过包装由I/O错误(不会引起IOError异常,尽管也存在)引起的已检查的IOException异常来抛出异常检查的概念,这些异常异常难以处理,因此您需要它们不被检查。
由于Java !
SNR
首先,检查异常降低了代码的“信噪比”。Anders Hejlsberg也谈到了命令式编程和声明式编程,这是一个类似的概念。不管怎样,考虑下面的代码片段:
在Java中从非UI线程更新UI:
try {
// Run the update code on the Swing thread
SwingUtilities.invokeAndWait(() -> {
try {
// Update UI value from the file system data
FileUtility f = new FileUtility();
uiComponent.setValue(f.readSomething());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
} catch (InterruptedException ex) {
throw new IllegalStateException("Interrupted updating UI", ex);
} catch (InvocationTargetException ex) {
throw new IllegalStateException("Invocation target exception updating UI", ex);
}
在c#中从非UI线程更新UI:
private void UpdateValue()
{
// Ensure the update happens on the UI thread
if (InvokeRequired)
{
Invoke(new MethodInvoker(UpdateValue));
}
else
{
// Update UI value from the file system data
FileUtility f = new FileUtility();
uiComponent.Value = f.ReadSomething();
}
}
这对我来说清楚多了。当您开始在Swing中做越来越多的UI工作时,检查异常开始变得非常烦人和无用。
监狱打破
即使要实现最基本的实现,比如Java的List接口,作为契约式设计工具的受控异常也行不通。考虑一个由数据库、文件系统或任何其他抛出受控异常的实现支持的列表。唯一可能的实现是捕获已检查异常,并将其作为未检查异常重新抛出:
@Override
public void clear()
{
try
{
backingImplementation.clear();
}
catch (CheckedBackingImplException ex)
{
throw new IllegalStateException("Error clearing underlying list.", ex);
}
}
现在你要问所有这些代码的意义是什么?被检查的异常只是增加了噪音,异常被捕获但没有处理,契约式设计(就被检查的异常而言)已经失效。
结论
捕获异常与处理异常是不同的。
受控异常会给代码添加噪音。
没有它们,异常处理在c#中也能很好地工作。
我之前在博客上写过。
在过去的三年中,我一直在与一些开发人员一起开发相对复杂的应用程序。我们有一个代码库,它经常使用检查异常,并进行适当的错误处理,而其他一些代码库则没有。
So far, I have it found easier to work with the code base with Checked Exceptions. When I am using someone else's API, it is nice that I can see exactly what kind of error conditions I can expect when I call the code and handle them properly, either by logging, displaying or ignoring (Yes, there is valid cases for ignoring exceptions, such as a ClassLoader implementation). That gives the code I am writing an opportunity to recover. All runtime exceptions I propagate up until they are cached and handled with some generic error handling code. When I find a checked exception that I don't really want to handle at a specific level, or that I consider a programming logic error, then I wrap it into a RuntimeException and let it bubble up. Never, ever swallow an exception without a good reason (and good reasons for doing this are rather scarce)
当我使用没有检查异常的代码库时,它使我在调用函数时很难预先知道我期望什么,这可能会严重破坏一些东西。
当然,这完全取决于开发者的偏好和技能。编程和错误处理的两种方式可能同样有效(或无效),所以我不会说只有一种方法。
总而言之,我发现使用受控异常更容易,特别是在有很多开发人员的大型项目中。
受控异常最初的形式是试图处理意外事件,而不是失败。值得称赞的目标是突出特定的可预测点(无法连接,文件未找到等)并确保开发者能够处理这些问题。
最初的概念中从未包括的是强制宣布大量系统性和不可恢复的故障。这些失败不能被声明为受控异常。
在代码中失败通常是可能的,EJB、web和Swing/AWT容器已经通过提供最外层的“失败请求”异常处理程序来满足这一点。最基本的正确策略是回滚事务并返回错误。
关键的一点是,运行时异常和受控异常在功能上是等价的。检查异常所能做的处理或恢复是运行时异常所不能做的。
反对“受控”异常的最大争论是大多数异常无法修复。简单的事实是,我们并不拥有发生故障的代码/子系统。我们看不到实现,我们不负责它,也不能修复它。
如果我们的应用程序不是一个数据库..我们不应该试图修复DB。这违反了封装原则。
特别有问题的是JDBC (SQLException)和用于EJB的RMI (RemoteException)。而不是根据最初的“受控异常”概念确定可修复的偶发事件,这些强制普遍存在的系统可靠性问题,实际上是不可修复的,被广泛宣布。
Java设计中的另一个严重缺陷是异常处理应该正确地放在尽可能高的“业务”或“请求”级别。这里的原则是“早扔晚接”。受控异常做的很少,但会阻碍这一点。
我们在Java中有一个明显的问题,即需要数千个不做任何事情的try-catch块,其中很大一部分(40%以上)被错误编码。这些方法几乎都没有实现任何真正的处理或可靠性,但会增加大量的编码开销。
最后,“受控异常”与FP函数式编程几乎不兼容。
他们对“立即处理”的坚持与“延迟捕获”异常处理最佳实践和任何抽象循环/或控制流的FP结构都不一致。
许多人谈论“处理”受控异常,但他们是在胡说八道。在失败后继续使用null、不完整或不正确的数据来假装成功没有处理任何事情。这是最低级的工程/可靠性渎职。
干净利落的失败,是处理异常最基本的正确策略。回滚事务、记录错误和向用户报告“失败”响应是良好的实践——最重要的是,防止不正确的业务数据提交到数据库。
在业务、子系统或请求级别上,异常处理的其他策略是“重试”、“重新连接”或“跳过”。所有这些都是通用的可靠性策略,并且在运行时异常时工作得更好。
最后,失败比使用不正确的数据运行要好得多。继续将导致次要错误,远离原始原因,更难调试;或者最终将导致提交错误的数据。有人会因此被解雇。
看到的:
——http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/
这篇文章是我读过的关于Java异常处理的最好的文章。
它更倾向于未检查的异常,而不是已检查的异常,但这个选择的解释非常透彻,并基于强有力的论据。
我不想在这里引用太多的文章内容(最好是整体阅读),但它涵盖了这个线程中未检查异常倡导者的大部分论点。尤其是这个论点(似乎很受欢迎):
以异常在API层底部某处抛出的情况为例,因为没有人知道这个错误甚至可能发生,即使它是一种非常合理的错误类型,当调用代码抛出它时(例如FileNotFoundException而不是VogonsTrashingEarthExcept…)在这种情况下,我们是否处理它并不重要,因为没有任何东西可以处理它)。
作者“回应”:
It is absolutely incorrect to assume that all runtime exceptions
should not be caught and allowed to propagate to the very "top" of the
application. (...) For every exceptional condition that is required to
be handled distinctly - by the system/business requirements -
programmers must decide where to catch it and what to do once the
condition is caught. This must be done strictly according to the
actual needs of the application, not based on a compiler alert. All
other errors must be allowed to freely propagate to the topmost
handler where they would be logged and a graceful (perhaps,
termination) action will be taken.
主要的思想或文章是:
当涉及到软件中的错误处理时,唯一安全且正确的假设是,存在的每个子程序或模块都可能发生故障!
因此,如果“没有人知道这个错误甚至可能发生”,那么这个项目就有问题了。像作者建议的那样,这种异常至少应该由最通用的异常处理程序来处理(例如,处理所有没有更特定的处理程序处理的异常)。
很遗憾,似乎没有多少人发现这篇伟大的文章:-(。我衷心建议每一个犹豫哪种方法更好的人花点时间阅读它。