多年来,我一直无法得到以下问题的正确答案:为什么有些开发人员如此反对受检异常?我进行了无数次对话,阅读了博客上的内容,阅读了布鲁斯·埃克尔(Bruce Eckel)所说的话(我看到的第一个反对他们的人)。
我目前正在编写一些新代码,并非常注意我如何处理异常。我试图看到“我们不喜欢检查异常”人群的观点,但我仍然看不到它。
我的每一次谈话都以同样的问题结束……让我来设置一下:
一般来说(从 Java 的设计方式来看),
错误是针对不应该被捕获的东西(VM 对花生过敏,有人在上面掉了一罐花生)
RuntimeException 是针对程序员做错的事情(程序员离开了数组的末尾)
异常(RuntimeException 除外)是针对程序员无法控制的事情(写入文件系统时磁盘已满,已达到进程的文件句柄限制,您无法再打开任何文件)
Throwable 只是所有异常类型的父级。
我听到的一个常见论点是,如果发生异常,那么开发人员要做的就是退出程序。
我听到的另一个常见论点是检查异常使重构代码变得更加困难。
对于“我要做的就是退出”的论点,我说即使您要退出,您也需要显示合理的错误消息。如果您只是在处理错误,那么当程序退出而没有明确说明原因时,您的用户不会过分高兴。
对于“重构变得困难”的人群,这表明没有选择适当的抽象级别。与其声明一个方法抛出 IOException
,不如将 IOException
转换为更适合正在发生的事情的异常。
我没有用 catch(Exception)
包装 Main (或在某些情况下 catch(Throwable)
以确保程序可以正常退出 - 但我总是捕获我需要的特定异常。这样做可以让我在至少,显示适当的错误消息。
人们从不回答的问题是:
如果你抛出 RuntimeException 子类而不是 Exception 子类,那么你怎么知道你应该捕获什么?
如果答案是 catch Exception
,那么您也将像处理系统异常一样处理程序员错误。这对我来说似乎是错误的。
如果您捕获 Throwable
,那么您将以相同的方式处理系统异常和 VM 错误(等等)。这对我来说似乎是错误的。
如果答案是你只捕获你知道抛出的异常,那么你怎么知道抛出了什么?当程序员 X 抛出一个新异常并忘记捕获它时会发生什么?这对我来说似乎很危险。
我会说显示堆栈跟踪的程序是错误的。不喜欢检查异常的人不会有这种感觉吗?
所以,如果你不喜欢检查的异常,你能解释一下为什么不回答没有得到回答的问题吗?
我不是在寻找关于何时使用这两种模型的建议,我在寻找的是为什么人们从 RuntimeException
扩展,因为他们不喜欢从 Exception
扩展和/或为什么他们抓住一个异常,然后重新抛出一个 RuntimeException
而不是在他们的方法中添加抛出。我想了解不喜欢检查异常的动机。
我想我读过和你一样的布鲁斯·埃克尔采访——它总是让我烦恼。事实上,这个论点是由 .NET 和 C# 背后的 MS 天才 Anders Hejlsberg(如果这确实是你正在谈论的帖子)提出的。
http://www.artima.com/intv/handcuffs.html
尽管我是 Hejlsberg 和他的作品的粉丝,但这个论点一直让我觉得是假的。它基本上归结为:
“检查的异常是不好的,因为程序员只是滥用它们,总是捕捉它们并解雇它们,这导致问题被隐藏和忽略,否则这些问题会呈现给用户”。
“以其他方式呈现给用户”是指如果您使用运行时异常,懒惰的程序员将忽略它(而不是用空的 catch 块捕获它)并且用户会看到它。
该论点的总结是“程序员不会正确使用它们,不正确使用它们比没有它们更糟糕”。
这个论点有一些道理,事实上,我怀疑 Gosling 不在 Java 中放置运算符覆盖的动机来自一个类似的论点——它们使程序员感到困惑,因为它们经常被滥用。
但最后,我发现这是 Hejlsberg 的一个虚假论点,并且可能是为了解释缺乏而创建的一个事后论点,而不是一个经过深思熟虑的决定。
我认为,虽然过度使用检查异常是一件坏事,并且往往会导致用户处理草率,但是正确使用它们可以让 API 程序员给 API 客户端程序员带来很大的好处。
现在 API 程序员必须小心不要到处抛出已检查的异常,否则它们只会惹恼客户端程序员。正如 Hejlsberg 警告的那样,非常懒惰的客户端程序员将求助于捕获 (Exception) {}
,所有好处都将丢失,地狱将接踵而至。但是在某些情况下,一个好的检查异常是无可替代的。
对我来说,经典的例子是文件打开 API。语言历史上的每一种编程语言(至少在文件系统上)都有一个 API,可以让你打开文件。每个使用这个 API 的客户端程序员都知道他们必须处理他们试图打开的文件不存在的情况。让我换个说法:每个使用此 API 的客户端程序员都应该知道他们必须处理这种情况。还有一个问题:API 程序员是否可以帮助他们知道他们应该通过单独评论来处理它,或者他们确实可以坚持让客户处理它。
在 C 中,成语类似于
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
其中 fopen
通过返回 0 表示失败,而 C(愚蠢地)让您将 0 视为布尔值,并且...基本上,您学习了这个习语就可以了。但是,如果您是菜鸟并且没有学习成语怎么办。然后,当然,你从
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
并努力学习。
请注意,我们在这里只讨论强类型语言:对于强类型语言中的 API 有一个清晰的概念:它是一个功能(方法)的大杂烩,供您使用,每个功能(方法)都有一个明确定义的协议。
该明确定义的协议通常由方法签名定义。此处 fopen 要求您向其传递一个字符串(或在 C 的情况下为 char*)。如果你给它其他东西,你会得到一个编译时错误。您没有遵守协议 - 您没有正确使用 API。
在某些(晦涩的)语言中,返回类型也是协议的一部分。如果您尝试在某些语言中调用 fopen()
的等价物而不将其分配给变量,您也会收到编译时错误(您只能使用 void 函数执行此操作)。
我要说明的一点是:在静态类型语言中,API 程序员鼓励客户端正确使用 API,如果客户端代码出现任何明显错误,则阻止其编译。
(在像 Ruby 这样的动态类型语言中,您可以传递任何东西,比如浮点数,作为文件名 - 它会编译。如果您甚至不打算控制方法参数,为什么还要用检查的异常来麻烦用户。此处提出的论点仅适用于静态类型语言。)
那么,检查异常呢?
好吧,这是您可以用来打开文件的 Java API 之一。
try {
f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
// deal with it. No really, deal with it!
... // this is me dealing with it
}
看到那个渔获了吗?这是该 API 方法的签名:
public FileInputStream(String name)
throws FileNotFoundException
请注意,FileNotFoundException
是一个已检查 异常。
API 程序员对你说:“你可以使用这个构造函数来创建一个新的 FileInputStream,但是你
a) 必须将文件名作为字符串传递 b) 必须接受在运行时可能找不到文件的可能性”
就我而言,这就是重点。
关键基本上是问题所说的“程序员无法控制的事情”。我的第一个想法是他/她的意思是 API 程序员无法控制的东西。但事实上,正确使用的检查异常应该是针对客户端程序员和 API 程序员无法控制的事情。我认为这是不滥用检查异常的关键。
我认为打开文件很好地说明了这一点。 API 程序员知道您可能会给他们一个在调用 API 时结果不存在的文件名,并且他们将无法返回您想要的内容,但必须抛出异常。他们也知道这会经常发生,并且客户端程序员可能希望文件名在他们编写调用时是正确的,但在运行时也可能由于他们无法控制的原因而出错。
因此 API 明确表示:在某些情况下,当您致电给我时,该文件不存在,而您最好更好地处理它。
如果有反例,这会更清楚。想象一下,我正在编写一个表 API。我在某处有一个包含此方法的 API 的表模型:
public RowData getRowData(int row)
现在,作为一名 API 程序员,我知道在某些情况下,某些客户端会为行传入负值或在表外传入行值。所以我可能很想抛出一个检查异常并强制客户端处理它:
public RowData getRowData(int row) throws CheckedInvalidRowNumberException
(当然,我不会真正称其为“已检查”。)
这是对检查异常的不好使用。客户端代码将充满获取行数据的调用,每一个都必须使用 try/catch,这是为了什么?他们是否会向用户报告搜索了错误的行?可能不会——因为无论我的表格视图周围的 UI 是什么,它都不应该让用户进入请求非法行的状态。所以这是客户端程序员的一个错误。
API 程序员仍然可以预测客户端将编写此类错误,并应使用 IllegalArgumentException
之类的运行时异常来处理它。
对于 getRowData
中的检查异常,这显然会导致 Hejlsberg 的懒惰程序员简单地添加空捕获。发生这种情况时,即使对测试人员或客户端开发人员进行调试,非法行值也不会很明显,而是会导致难以查明来源的连锁错误。阿里安火箭发射后会爆炸。
好的,问题来了:我说检查异常 FileNotFoundException
不仅是一件好事,而且是 API 程序员工具箱中的一个必不可少的工具,用于以对客户端程序员最有用的方式定义 API。但是 CheckedInvalidRowNumberException
是一个很大的不便,会导致糟糕的编程,应该避免。但是如何区分。
我想这不是一门精确的科学,我想这在一定程度上是 Hejlsberg 论点的基础,并且可能证明了这一点。但是我不乐意把婴儿和洗澡水一起扔在这里,所以请允许我在这里提取一些规则来区分好的检查异常和坏的:
Out of client's control or Closed vs Open: Checked exceptions 应该只在错误情况不受 API 和客户端程序员控制的情况下使用。这与系统的开放程度或封闭程度有关。在客户端程序员可以控制所有按钮、键盘命令等的受限 UI 中,这些按钮、键盘命令等从表视图(封闭系统)中添加和删除行,如果它试图从一个不存在的行。在基于文件的操作系统中,任何数量的用户/应用程序都可以添加和删除文件(开放系统),可以想象客户端请求的文件已在他们不知情的情况下被删除,因此应该期望他们处理它.普遍性:不应在客户端频繁进行的 API 调用上使用已检查的异常。经常我的意思是来自客户端代码中的很多地方 - 不是经常在时间上。因此,客户端代码不会经常尝试打开同一个文件,但我的表格视图通过不同的方法在各处获取 RowData。特别是,我将编写大量代码,例如 if (model.getRowData().getCell(0).isEmpty())
每次都必须包含在 try/catch 中会很痛苦。
通知用户:在您可以想象向最终用户呈现有用的错误消息的情况下,应使用已检查的异常。这就是“当它发生时你会怎么做?”我在上面提出的问题。它也与第 1 项有关。由于您可以预测客户端 API 系统之外的某些东西可能会导致文件不存在,因此您可以合理地告诉用户:“错误:找不到文件 'goodluckfindingthisfile'”由于您的非法行号是由内部错误引起的,并且不是用户的过错,因此您实际上没有任何有用的信息可以提供给他们。如果您的应用程序不允许运行时异常进入控制台,它可能最终会给它们一些丑陋的消息,例如:“发生内部错误:IllegalArgumentException in ....”简而言之,如果您不认为您的客户端程序员可以以帮助用户的方式解释您的异常,那么您可能不应该使用已检查的异常。
所以这些是我的规则。有点做作,毫无疑问会有例外(如果你愿意,请帮助我完善它们)。但我的主要论点是,在某些情况下,例如 FileNotFoundException
,已检查异常在 API 协定中与参数类型一样重要且有用。所以我们不应该仅仅因为它被滥用就放弃它。
抱歉,我不是故意让这件事变得如此冗长和胡扯。最后让我提出两个建议:
答:API 程序员:谨慎使用已检查异常以保持其有用性。如有疑问,请使用未经检查的异常。
B:客户端程序员:养成在开发早期创建包装异常(google it)的习惯。 JDK 1.4 及更高版本为此在 RuntimeException
中提供了一个构造函数,但您也可以轻松创建自己的构造函数。这是构造函数:
public RuntimeException(Throwable cause)
然后养成每当你必须处理一个检查的异常并且你感到懒惰(或者你认为 API 程序员一开始就过分热衷于使用检查的异常)的习惯,不要只是吞下异常,包装它并重新抛出它。
try {
overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
throw new RuntimeException(exception);
}
把它放在你的 IDE 的一个小代码模板中,当你感到懒惰时使用它。这样,如果您确实需要处理已检查的异常,您将在运行时看到问题后被迫返回并处理它。因为,相信我(和 Anders Hejlsberg),你永远不会回到你的 TODO
catch (Exception e) { /* TODO deal with this at some point (yeah right) */}
关于检查异常的事情是,按照通常对概念的理解,它们并不是真正的异常。相反,它们是 API 替代返回值。
异常的整个想法是,在调用链的某处抛出的错误可以冒泡并由更远的代码处理,而无需干预代码担心它。另一方面,已检查的异常要求抛出者和捕获者之间的每一级代码都声明他们知道可以通过它们的所有形式的异常。这在实践中与如果检查的异常只是调用者必须检查的特殊返回值几乎没有什么不同。例如。[伪代码]:
public [int or IOException] writeToStream(OutputStream stream) {
[void or IOException] a= stream.write(mybytes);
if (a instanceof IOException)
return a;
return mybytes.length;
}
由于 Java 不能做替代返回值,或者简单的内联元组作为返回值,所以检查异常是一个合理的响应。
问题是很多代码,包括大量的标准库,滥用检查的异常来处理真正的异常情况,你可能很想赶上几个级别。为什么 IOException 不是 RuntimeException?在所有其他语言中,我都可以让 IO 异常发生,如果我不采取任何措施来处理它,我的应用程序将停止,并且我会得到一个方便的堆栈跟踪来查看。这是可能发生的最好的事情。
可能有两个方法,你想从整个写入到流的过程中捕获所有 IOExceptions,中止该过程并跳转到错误报告代码;在 Java 中,如果不在每个调用级别添加“抛出 IOException”,即使是本身不执行 IO 的级别,也无法做到这一点。这样的方法不需要知道异常处理;必须为其签名添加例外:
不必要地增加耦合;使界面签名很难更改;降低代码的可读性;太烦人了,程序员的常见反应是通过做一些可怕的事情来打败系统,比如“抛出异常”、“捕获(异常 e){}”,或者将所有内容包装在 RuntimeException 中(这使得调试更加困难)。
然后有很多荒谬的库异常,例如:
try {
httpconn.setRequestMethod("POST");
} catch (ProtocolException e) {
throw new CanNeverHappenException("oh dear!");
}
当你不得不用这样可笑的杂物来弄乱你的代码时,难怪受检异常会受到很多讨厌,即使这只是简单的糟糕的 API 设计。
另一个特别不好的影响是控制反转,其中组件 A 向通用组件 B 提供回调。组件 A 希望能够让异常从其回调中抛出回它调用组件 B 的地方,但它不能因为这会改变由 B 修复的回调接口。A 只能通过将真正的异常包装在 RuntimeException 中来做到这一点,这是要编写的更多异常处理样板。
在 Java 及其标准库中实现的已检查异常意味着样板、样板、样板。在已经冗长的语言中,这不是胜利。
return
机制能做到的,也是异常让我们能够实现解耦的原因。底线,例外是流控制,与陈词滥调相反。它们是一个更有限、更易于管理的(因为对状态有更大的保证)GOTO。
我不会针对检查的异常重新讨论所有(许多)原因,而是只选择一个。我已经记不清我写这段代码的次数了:
try {
// do stuff
} catch (AnnoyingcheckedException e) {
throw new RuntimeException(e);
}
99%的时间我对此无能为力。 finally 块进行任何必要的清理(或者至少它们应该)。
我也记不清我看到这个的次数了:
try {
// do stuff
} catch (AnnoyingCheckedException e) {
// do nothing
}
为什么?因为有人不得不处理它并且很懒惰。是不是错了?当然。它会发生吗?绝对地。如果这是一个未经检查的异常呢?该应用程序将刚刚死掉(这比吞下异常更可取)。
然后我们有令人愤怒的代码,它使用异常作为流控制的一种形式,就像 java.text.Format 一样。嗡嗡声。错误的。用户将“abc”放入表单的数字字段中也不例外。
好吧,我想这是三个原因。
我知道这是一个老问题,但我花了一段时间来处理检查的异常,我有一些东西要补充。请原谅我的长度!
我对检查异常的主要不满是它们破坏了多态性。让它们与多态接口很好地配合是不可能的。
采用良好的 Java List
接口。我们有常见的内存实现,例如 ArrayList
和 LinkedList
。我们还有骨架类 AbstractList
,它可以很容易地设计新类型的列表。对于只读列表,我们只需要实现两个方法:size()
和 get(int index)
。
此示例 WidgetList
类从文件中读取一些 Widget
类型的固定大小对象(未显示):
class WidgetList extends AbstractList<Widget> {
private static final int SIZE_OF_WIDGET = 100;
private final RandomAccessFile file;
public WidgetList(RandomAccessFile file) {
this.file = file;
}
@Override
public int size() {
return (int)(file.length() / SIZE_OF_WIDGET);
}
@Override
public Widget get(int index) {
file.seek((long)index * SIZE_OF_WIDGET);
byte[] data = new byte[SIZE_OF_WIDGET];
file.read(data);
return new Widget(data);
}
}
通过使用熟悉的 List
界面公开小部件,您可以检索项目 (list.get(123)
) 或迭代列表 (for (Widget w : list) ...
),而无需了解 WidgetList
本身。可以将此列表传递给任何使用通用列表的标准方法,或将其包装在 Collections.synchronizedList
中。使用它的代码既不需要知道也不需要关心“小部件”是现场制作的,来自数组,还是从文件、数据库、网络或未来的子空间中继中读取的。它仍然可以正常工作,因为 List
接口已正确实现。
除非它不是。上面的类无法编译,因为文件访问方法可能会抛出 IOException
,这是一个您必须“捕获或指定”的检查异常。您不能将其指定为抛出——编译器不会让您这样做,因为这会违反 List
接口的约定。并且 WidgetList
本身没有任何有用的方法可以处理异常(稍后我将详细说明)。
显然,唯一要做的就是捕获并重新抛出已检查的异常作为一些未检查的异常:
@Override
public int size() {
try {
return (int)(file.length() / SIZE_OF_WIDGET);
} catch (IOException e) {
throw new WidgetListException(e);
}
}
public static class WidgetListException extends RuntimeException {
public WidgetListException(Throwable cause) {
super(cause);
}
}
((编辑:Java 8 为这种情况添加了一个 UncheckedIOException
类:用于跨多态方法边界捕获和重新抛出 IOException
。有点证明了我的观点!))
所以检查异常在这种情况下根本不起作用。你不能扔它们。同样适用于由数据库支持的智能 Map
,或通过 COM 端口连接到量子熵源的 java.util.Random
实现。一旦您尝试对多态接口的实现做任何新颖的事情,检查异常的概念就会失败。但是检查的异常是如此阴险,它们仍然不会让你平静下来,因为你仍然必须从低级方法中捕获并重新抛出任何异常,从而使代码和堆栈跟踪变得混乱。
我发现无处不在的 Runnable
接口经常被退回到这个角落,如果它调用了引发检查异常的东西。它不能按原样抛出异常,所以它所能做的就是通过捕获并重新抛出 RuntimeException
来使代码混乱。
实际上,如果你使用 hack,你可以抛出未声明的检查异常。 JVM 在运行时并不关心检查异常规则,所以我们只需要欺骗编译器。最简单的方法是滥用泛型。这是我的方法(显示类名是因为(在 Java 8 之前)它在泛型方法的调用语法中是必需的):
class Util {
/**
* Throws any {@link Throwable} without needing to declare it in the
* method's {@code throws} clause.
*
* <p>When calling, it is suggested to prepend this method by the
* {@code throw} keyword. This tells the compiler about the control flow,
* about reachable and unreachable code. (For example, you don't need to
* specify a method return value when throwing an exception.) To support
* this, this method has a return type of {@link RuntimeException},
* although it never returns anything.
*
* @param t the {@code Throwable} to throw
* @return nothing; this method never returns normally
* @throws Throwable that was provided to the method
* @throws NullPointerException if {@code t} is {@code null}
*/
public static RuntimeException sneakyThrow(Throwable t) {
return Util.<RuntimeException>sneakyThrow1(t);
}
@SuppressWarnings("unchecked")
private static <T extends Throwable> RuntimeException sneakyThrow1(
Throwable t) throws T {
throw (T)t;
}
}
欢呼!使用它,我们可以在堆栈的任何深度抛出已检查异常,而无需声明它,无需将其包装在 RuntimeException
中,也不会弄乱堆栈跟踪!再次使用“WidgetList”示例:
@Override
public int size() {
try {
return (int)(file.length() / SIZE_OF_WIDGET);
} catch (IOException e) {
throw sneakyThrow(e);
}
}
不幸的是,受检查异常的最后一个侮辱是编译器拒绝允许您捕获受检查异常,如果它有缺陷地认为它不能被抛出。 (未经检查的异常没有这条规则。)为了捕捉偷偷抛出的异常,我们必须这样做:
try {
...
} catch (Throwable t) { // catch everything
if (t instanceof IOException) {
// handle it
...
} else {
// didn't want to catch this one; let it go
throw t;
}
}
这有点尴尬,但从好的方面来说,它仍然比用于提取包装在 RuntimeException
中的已检查异常的代码稍微简单一些。
幸运的是,throw t;
语句在这里是合法的,即使检查了 t
的类型,这要感谢 Java 7 中添加的关于重新抛出捕获的异常的规则。
当检查的异常遇到多态性时,相反的情况也是一个问题:当一个方法被指定为可能抛出一个检查的异常,但重写的实现没有。例如,抽象类 OutputStream
的 write
方法都指定 throws IOException
。 ByteArrayOutputStream
是写入内存数组而不是真正的 I/O 源的子类。它被覆盖的 write
方法不会导致 IOException
,因此它们没有 throws
子句,您可以调用它们而不必担心 catch-or-specify 要求。
除了并非总是如此。假设 Widget
具有将其保存到流中的方法:
public void writeTo(OutputStream out) throws IOException;
声明这个方法接受一个普通的 OutputStream
是正确的做法,因此它可以多态地用于各种输出:文件、数据库、网络等等。和内存数组。但是,对于内存中的数组,有一个虚假的要求来处理实际上不可能发生的异常:
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
someWidget.writeTo(out);
} catch (IOException e) {
// can't happen (although we shouldn't ignore it if it does)
throw new RuntimeException(e);
}
像往常一样,检查的异常会阻碍。如果您的变量被声明为具有更多开放式异常要求的基类型,则您必须为这些异常添加处理程序,即使您知道它们不会在您的应用程序中发生。
但是等等,检查的异常实际上是非常烦人的,它们甚至不允许你做相反的事情!想象一下你当前捕获了由 write
调用抛出的任何 IOException
在 OutputStream
上,但您想将变量的声明类型更改为 ByteArrayOutputStream
,编译器会责备您试图捕获它说不能抛出的已检查异常。
该规则会导致一些荒谬的问题。例如,OutputStream
的三个 write
方法之一不被 ByteArrayOutputStream
覆盖。具体来说,write(byte[] data)
是一种便捷方法,它通过调用 write(byte[] data, int offset, int length)
以偏移量为 0 和数组长度来写入完整数组。 ByteArrayOutputStream
覆盖三参数方法,但按原样继承一参数便捷方法。继承的方法完全正确,但它包含一个不需要的 throws
子句。这可能是 ByteArrayOutputStream
设计中的一个疏忽,但他们永远无法修复它,因为它会破坏与捕获异常的任何代码的源代码兼容性——从未、永远不会、永远不会抛出异常!
该规则在编辑和调试期间也很烦人。例如,有时我会临时注释掉一个方法调用,如果它可能抛出一个检查异常,编译器现在会抱怨本地 try
和 catch
块的存在。所以我也必须将它们注释掉,现在在编辑其中的代码时,IDE 将缩进到错误的级别,因为 {
和 }
被注释掉了。呸!这是一个小小的抱怨,但似乎检查异常所做的唯一一件事就是造成麻烦。
我快完成了。我对检查异常的最后一个挫折是,在大多数呼叫站点,您对它们没有任何用处。理想情况下,当出现问题时,我们会有一个称职的特定于应用程序的处理程序,可以通知用户问题和/或适当地结束或重试操作。只有堆栈上的处理程序才能做到这一点,因为它是唯一知道总体目标的处理程序。
相反,我们得到了以下惯用语,它作为一种关闭编译器的方式很猖獗:
try {
...
} catch (SomeStupidExceptionOmgWhoCares e) {
e.printStackTrace();
}
在 GUI 或自动化程序中,不会看到打印的消息。更糟糕的是,它会在异常发生后继续处理其余代码。异常实际上不是错误吗?那就不要打印了。否则,一会儿其他东西会爆炸,到那时原始异常对象将消失。这个习语并不比 BASIC 的 On Error Resume Next
或 PHP 的 error_reporting(0);
好。
调用某种记录器类也好不到哪里去:
try {
...
} catch (SomethingWeird e) {
logger.log(e);
}
这就像 e.printStackTrace();
一样懒惰,并且仍然在不确定状态下继续使用代码。此外,特定日志系统或其他处理程序的选择是特定于应用程序的,因此这会损害代码重用。
可是等等!有一种简单而通用的方法可以找到特定于应用程序的处理程序。它在调用堆栈的上层(或设置为线程的 uncaught exception handler)。所以在大多数情况下,您需要做的就是将异常抛到更高的堆栈上。例如,throw e;
。已检查的异常只会妨碍您。
我确信在设计语言时检查异常听起来是个好主意,但在实践中我发现它们都很麻烦而且没有任何好处。
RuntimeException
中。请注意,一个例程可以同时声明为 throws IOException
并且还指定从嵌套调用抛出的任何 IOException
都应被视为意外并包装。
if (false)
。它避免了 throw 子句问题,并且警告帮助我更快地导航回来。 +++ 也就是说,我同意你写的一切。已检查的异常有一些价值,但与它们的成本相比,这个价值可以忽略不计。他们几乎总是会碍事。
好吧,这与显示堆栈跟踪或静默崩溃无关。这是关于能够在层之间传达错误。
检查异常的问题是它们鼓励人们吞下重要的细节(即异常类)。如果您选择不接受这些细节,那么您必须在整个应用程序中不断添加 throws 声明。这意味着 1) 新的异常类型将影响大量函数签名,以及 2) 您可能会错过您实际想要捕获的异常的特定实例(例如,您为将数据写入文件。辅助文件是可选的,所以你可以忽略它的错误,但因为签名throws IOException
,很容易忽略这一点)。
我现在实际上正在应用程序中处理这种情况。我们将几乎所有异常重新打包为 AppSpecificException。这使得签名非常干净,我们不必担心签名中的 throws
爆炸。
当然,现在我们需要专门化更高级别的错误处理,实现重试逻辑等。但是,一切都是 AppSpecificException,所以我们不能说“如果抛出 IOException,请重试”或“如果抛出 ClassNotFound,则完全中止”。我们没有可靠的方法来处理真正的异常,因为当它们在我们的代码和第三方代码之间传递时,它们会一次又一次地重新打包。
这就是为什么我非常喜欢 python 中的异常处理。您只能捕获您想要和/或可以处理的东西。其他所有东西都会冒泡,就好像你自己重新扔了一样(反正你已经做过了)。
我一次又一次地发现,在我提到的整个项目中,异常处理分为 3 类:
捕获并处理特定的异常。例如,这是为了实现重试逻辑。捕获并重新抛出其他异常。这里发生的一切通常是日志记录,它通常是一条陈词滥调的消息,例如“无法打开 $filename”。这些是您无能为力的错误;只有更高级别的人知道足以处理它。捕获所有内容并显示错误消息。这通常位于调度程序的根部,它所做的一切确保它可以通过非异常机制(弹出对话框、编组 RPC 错误对象等)将错误传达给调用者。
throws
声明的函数。
信噪比
首先,检查异常会降低代码的“信噪比”。 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# 中运行良好。
我写了一篇关于这个 previously 的博客。
Artima published an interview 与 .NET 的一位架构师 Anders Hejlsberg 一起,其中敏锐地涵盖了反对检查异常的论点。短暂的品尝者:
throws 子句,至少它在 Java 中实现的方式,不一定强制您处理异常,但如果您不处理它们,它会强制您准确确认哪些异常可能通过。它要求您要么捕获已声明的异常,要么将它们放入您自己的 throws 子句中。为了解决这个要求,人们会做一些荒谬的事情。例如,他们用“抛出异常”来装饰每个方法。这完全破坏了该功能,并且您只是让程序员编写了更多狼吞虎咽的东西。这对任何人都没有帮助。
我最初同意你的观点,因为我一直支持检查异常,并开始思考为什么我不喜欢在 .Net 中没有检查异常。但后来我意识到我实际上并不喜欢检查异常。
回答你的问题,是的,我喜欢我的程序显示堆栈跟踪,最好是非常丑陋的。我希望应用程序爆炸成一堆你可能想看到的最丑陋的错误消息。
原因是,如果它这样做,我必须修复它,而且我必须立即修复它。我想立即知道有问题。
您实际处理了多少次异常?我不是在谈论捕获异常——我是在谈论处理它们?编写以下内容太容易了:
try {
thirdPartyMethod();
} catch(TPException e) {
// this should never happen
}
而且我知道您可以说这是不好的做法,并且“答案”是对异常做某事(让我猜猜,记录它?),但是在现实世界(tm)中,大多数程序员只是不这样做它。
所以,是的,如果我不必这样做,我不想捕获异常,并且我希望我的程序在我搞砸时会爆炸。默默地失败是最糟糕的结果。
RuntimeExceptions
,您确实不必捕获它,我同意您应该让程序从那里炸毁。您应该始终捕获和处理的异常是已检查的异常,例如 IOException
。如果您得到一个 IOException
,那么您的代码中没有什么需要修复的;您的程序不应该仅仅因为出现网络故障而崩溃。
文章 Effective Java Exceptions 很好地解释了何时使用未检查以及何时使用检查异常。以下是该文章中的一些引述,以突出要点:
偶然性:一种预期的条件,要求一种方法的替代响应,可以用该方法的预期目的来表达。方法的调用者期望这些条件,并有应对它们的策略。故障:阻止方法实现其预期目的的计划外情况,如果不参考方法的内部实现就无法描述。
(SO 不允许使用表格,因此您可能需要阅读 original page...中的以下内容...)
偶然性 被认为是:设计的一部分 预期会发生:经常但很少发生 谁在乎它:调用方法的上游代码 示例:替代返回模式 最佳映射:已检查异常 错误被认为是:讨厌惊喜 预期发生:从不 谁在乎:需要解决问题的人 示例:编程错误、硬件故障、配置错误、丢失文件、不可用的服务器 最佳映射:未经检查的异常
args[]
中的无效参数是什么?它们是偶然事件还是错误?
File#openInputStream
返回 Either<InputStream, Problem>
之类的东西 - 如果这就是您的意思,那么我们可能会同意。
在过去三年中,我一直在与几位开发人员合作开发相对复杂的应用程序。我们有一个代码库,它经常使用检查异常并进行适当的错误处理,而其他一些则没有。
到目前为止,我发现使用 Checked Exceptions 更容易使用代码库。当我使用其他人的 API 时,很高兴我可以准确地看到当我调用代码并通过记录、显示或忽略正确处理它们时会出现什么样的错误情况(是的,有忽略的有效案例异常,例如 ClassLoader 实现)。这使我正在编写的代码有机会恢复。我会传播所有运行时异常,直到它们被缓存并使用一些通用错误处理代码进行处理。当我发现一个我真的不想在特定级别处理的检查异常,或者我认为是编程逻辑错误时,我将它包装到 RuntimeException 中并让它冒泡。永远不要在没有充分理由的情况下吞下异常(这样做的充分理由相当稀缺)
当我使用没有检查异常的代码库时,我很难事先知道调用函数时我能期待什么,这可能会严重破坏一些东西。
这当然是一个偏好和开发人员技能的问题。编程和错误处理的两种方式都可以同样有效(或无效),所以我不会说有 The One Way。
总而言之,我发现使用 Checked Exceptions 更容易,特别是在有很多开发人员的大型项目中。
简而言之:
异常是一个 API 设计问题。 ——不多也不少。
检查异常的参数:
要理解为什么受检异常可能不是好事,让我们转过头来问:受检异常何时或为什么具有吸引力,即为什么您希望编译器强制声明异常?
答案很明显:有时您需要捕获异常,而这只有在被调用的代码为您感兴趣的错误提供特定异常类时才有可能。
因此,检查异常的论点是编译器强制程序员声明抛出哪些异常,并希望程序员随后也会记录特定的异常类和导致它们的错误。
但实际上,包 com.acme
通常只抛出 AcmeException
而不是特定的子类。然后调用者需要处理、声明或重新发送 AcmeExceptions
,但仍不能确定是 AcmeFileNotFoundError
发生还是 AcmePermissionDeniedError
。
因此,如果您只对 AcmeFileNotFoundError
感兴趣,解决方案是向 ACME 程序员提出功能请求,并告诉他们实现、声明和记录 AcmeException
的子类。
那么为什么要打扰呢?
因此,即使有检查异常,编译器也不能强制程序员抛出有用的异常。这仍然只是 API 质量的问题。
因此,没有检查异常的语言通常不会差很多。程序员可能会倾向于抛出通用 Error
类而不是 AcmeException
的非特定实例,但如果他们完全关心 API 质量,他们终究会学会引入 AcmeFileNotFoundError
。
总的来说,异常的规范和文档与普通方法的规范和文档没有太大区别。这些也是 API 设计问题,如果程序员忘记实现或导出有用的功能,则需要改进 API,以便您可以有效地使用它。
如果您遵循这种推理,很明显,在 Java 等语言中很常见的声明、捕获和重新抛出异常的“麻烦”通常不会增加任何价值。
还值得注意的是,Java VM 没有检查异常——只有 Java 编译器检查它们,并且具有更改的异常声明的类文件在运行时是兼容的。 Java VM 的安全性并没有通过检查异常提高,只有编码风格。
AcmeException
而不是 AcmeFileNotFoundError
相结合,祝你好运,找出你做错了什么以及你需要在哪里抓住它。已检查的异常为程序员提供了一些针对不良 API 设计的保护。
例外类别
在谈论异常时,我总是参考 Eric Lippert's Vexing exceptions 博客文章。他将例外分为以下几类:
致命的——这些异常不是你的错:你无法阻止,也无法明智地处理它们。例如,OutOfMemoryError 或 ThreadAbortException。
Boneheaded - 这些异常是你的错:你应该阻止它们,它们代表代码中的错误。例如,ArrayIndexOutOfBoundsException、NullPointerException 或任何 IllegalArgumentException。
烦恼 - 这些异常并非异常,不是你的错,你无法阻止它们,但你必须处理它们。它们通常是不幸的设计决策的结果,例如从 Integer.parseInt 抛出 NumberFormatException 而不是提供在解析失败时返回布尔值 false 的 Integer.tryParseInt 方法。
外生的——这些异常通常是异常的,不是你的错,你不能(合理地)阻止它们,但你必须处理它们。例如,FileNotFoundException。
API 用户:
不得处理致命或愚蠢的异常。
应该处理令人烦恼的异常,但它们不应该出现在理想的 API 中。
必须处理外生异常。
已检查的异常
API 用户必须处理特定异常这一事实是调用者和被调用者之间方法契约的一部分。除其他事项外,合同还指定:被调用者期望的参数的数量和类型、调用者可以期望的返回值类型以及调用者期望处理的异常。
由于 API 中不应该存在令人烦恼的异常,因此只有这些外生异常必须经过检查异常才能成为方法契约的一部分。相对较少的异常是外生的,因此任何 API 都应该有相对较少的检查异常。
已检查异常是必须处理的异常。处理异常可以像吞下它一样简单。那里!异常被处理。时期。如果开发人员想以这种方式处理它,那很好。但他不能忽视异常,并且已经被警告。
API 问题
但是任何检查了令人烦恼和致命异常的 API(例如 JCL)都会给 API 用户带来不必要的压力。此类异常必须处理,但要么异常非常普遍以至于它本来就不应该是异常,要么在处理时无能为力。这导致 Java 开发人员讨厌检查异常。
此外,许多 API 没有适当的异常类层次结构,导致各种非外生异常原因由单个检查异常类(例如 IOException
)表示。这也导致 Java 开发人员讨厌检查异常。
结论
外生异常是那些不是您的错、无法避免且应该处理的异常。这些构成了可以抛出的所有异常的一小部分。 API 应该只检查外生异常,而未检查所有其他异常。这将制作更好的 API,减轻 API 用户的压力,从而减少捕获所有、吞下或重新抛出未经检查的异常的需要。
所以不要讨厌 Java 和它的检查异常。相反,讨厌过度使用检查异常的 API。
好的......检查的异常并不理想并且有一些警告,但它们确实有目的。创建 API 时,会出现特定的失败案例,这些案例与该 API 的约定有关。在 Java 等强静态类型语言的上下文中,如果不使用检查异常,则必须依靠临时文档和约定来传达错误的可能性。这样做会消除编译器在处理错误方面可能带来的所有好处,并且您完全取决于程序员的善意。
那么,移除 Checked 异常,例如在 C# 中完成的,那么如何以编程和结构方式传达错误的可能性呢?如何通知客户端代码可能会发生这样那样的错误并且必须处理?
在处理受检异常时,我听到了各种各样的恐惧,它们被滥用,这是肯定的,但未经检查的异常也是如此。我说等几年,当 API 堆叠很多层时,你会乞求某种结构化手段的回归来传达失败。
以 API 层底部某处抛出异常并冒泡的情况为例,因为没有人知道甚至可能发生此错误,尽管这是一种在调用代码时非常合理的错误扔掉它(例如 FileNotFoundException 而不是 VogonsTrashingEarthExcept... 在这种情况下,我们是否处理它并不重要,因为没有任何东西可以处理它)。
许多人争辩说,无法加载文件几乎总是这个过程的世界末日,它必须死于可怕而痛苦的死亡。所以是的......当然......好吧......你为某事构建一个API并在某个时候加载文件......我作为所述API的用户只能响应......“你到底是谁来决定我的程序应该崩溃!”当然 考虑到异常被吞噬并且不留痕迹的选择,或者 EletroFlabbingChunkFluxManifoldChuggingException 的堆栈跟踪比 Marianna 沟槽更深,我会毫不犹豫地采用后者,但这是否意味着这是处理异常的理想方式?难道我们不能在中间的某个地方,每次遍历到一个新的抽象级别时都会重新转换和包装异常,以便它实际上意味着什么?
最后,我看到的大多数论点是“我不想处理异常,很多人不想处理异常。受检异常迫使我处理它们,因此我讨厌受检异常” 完全消除这种机制并将其降级为goto hell的鸿沟只是愚蠢的,缺乏判断力和远见。
如果我们消除检查异常,我们还可以消除函数的返回类型并始终返回一个“anytype”变量......这会让生活变得如此简单,不是吗?
这不是反对检查异常的纯粹概念的论据,但 Java 用于它们的类层次结构是一个畸形秀。我们总是把事情简单地称为“异常”——这是正确的,因为语言规范 calls them that too——但是异常在类型系统中是如何命名和表示的呢?
通过类 Exception
可以想象吗?不,因为 Exception
是例外,同样例外是 Exception
,除了那些 非 Exception
的例外,因为其他例外是实际上是 Error
,这是另一种异常,一种额外异常的异常,除非它确实发生,否则永远不会发生,除非有时你必须这样做,否则你永远不应该捕获它。除了这还不是全部,因为您还可以定义其他既不是 Exception
也不是 Error
而只是 Throwable
异常的异常。
其中哪些是“已检查”异常? Throwable
是已检查异常,除非它们也是 Error
,它们是未检查异常,然后是 Exception
,它们也是 Throwable
并且是检查异常的主要类型,除了也有一个例外,如果它们也是 RuntimeException
,因为这是另一种未经检查的异常。
RuntimeException
有什么用?就像名字所暗示的那样,它们是异常,就像所有 Exception
一样,它们发生在运行时,就像实际上所有异常一样,除了 RuntimeException
与其他运行时 Exception
相比是异常的因为除非您犯了一些愚蠢的错误,否则它们不应该发生,尽管 RuntimeException
绝不是 Error
,因此它们适用于异常错误但实际上不是 Error
的事情。除了 RuntimeErrorException
,它实际上是 Error
的 RuntimeException
。但是,不是所有的例外都应该代表错误的情况吗?是的,所有这些。除了 ThreadDeath
,这是一个非常普通的例外,因为文档解释说这是“正常发生”,这就是他们将其设为 Error
类型的原因。
无论如何,由于我们将所有异常从中间分为 Error
(用于异常执行异常,因此未检查)和 Exception
(用于较少异常的执行错误,因此检查,除非它们不是),我们现在需要两种不同的几种异常。所以我们需要 IllegalAccessError
和 IllegalAccessException
、InstantiationError
和 InstantiationException
、NoSuchFieldError
和 NoSuchFieldException
、NoSuchMethodError
和 NoSuchMethodException
、ZipError
和 ZipException
。
除了即使检查了异常,也总是有(相当简单的)方法可以欺骗编译器并在不检查的情况下将其抛出。如果您这样做,您可能会得到一个 UndeclaredThrowableException
,除非在其他情况下,它可能会作为 UnexpectedException
或 UnknownException
(与 UnknownError
无关,仅用于“严重异常")、ExecutionException
、InvocationTargetException
或 ExceptionInInitializerError
。
哦,我们不能忘记 Java 8 新奇的 UncheckedIOException
,它是一个 RuntimeException
异常,旨在让您通过包装由 I/O 错误引起的已检查 IOException
异常(它不要导致 IOError
异常,尽管它也存在)异常难以处理,因此您需要不检查它们。
谢谢爪哇!
This article 是我读过的关于 Java 异常处理的最佳文章。
它支持未经检查的异常而不是检查的异常,但这种选择被非常彻底地解释并基于强有力的论据。
我不想在这里引用太多的文章内容(最好将它作为一个整体来阅读),但它涵盖了来自该线程的 unchecked exceptions 倡导者的大部分论点。特别是这个论点(这似乎很流行)被涵盖:
以 API 层底部某处抛出异常并冒泡的情况为例,因为没有人知道甚至可能发生此错误,尽管这是一种在调用代码时非常合理的错误扔掉它(例如 FileNotFoundException 而不是 VogonsTrashingEarthExcept... 在这种情况下,我们是否处理它并不重要,因为没有任何东西可以处理它)。
作者“回应”:
假设不应捕获所有运行时异常并允许传播到应用程序的“顶部”,这是绝对错误的。 (...) 对于每个需要明确处理的异常情况 - 由系统/业务需求 - 程序员必须决定在哪里捕获它以及在捕获条件后做什么。这必须严格根据应用程序的实际需求来完成,而不是基于编译器警报。必须允许所有其他错误自由传播到将记录它们的最顶层处理程序,并且将采取优雅(可能是终止)操作。
主要思想或文章是:
当谈到软件中的错误处理时,唯一安全和正确的假设是,绝对存在的每个子程序或模块都可能发生故障!
因此,如果“没有人知道甚至可能发生此错误”,则该项目有问题。正如作者所建议的那样,此类异常应至少由最通用的异常处理程序(例如,处理所有未由更具体处理程序处理的异常的处理程序)处理。
可悲的是,似乎没有多少人发现这篇很棒的文章:-(。我衷心建议所有犹豫哪种方法更好的人花点时间阅读它。
事实上,检查异常一方面增加了程序的健壮性和正确性(您被迫对接口进行正确的声明——方法抛出的异常基本上是一种特殊的返回类型)。另一方面,您面临的问题是,由于异常“冒泡”,因此当您更改异常时,您通常需要更改大量方法(所有调用者,以及调用者的调用者等)方法抛出。
Java 中的检查异常并不能解决后一个问题; C# 和 VB.NET 把婴儿和洗澡水一起扔掉。
this OOPSLA 2005 paper(或 related technical report)中描述了一种采取中间道路的好方法。)
简而言之,它允许你说:method g(x) throws like f(x)
,这意味着 g 抛出了 f 抛出的所有异常。瞧,检查了没有级联更改问题的异常。
尽管这是一篇学术论文,但我鼓励您阅读(部分)它,因为它很好地解释了检查异常的好处和坏处。
问题
我看到的异常处理机制最糟糕的问题是它引入了大规模的代码重复!老实说:在大多数项目中,95% 的时间里,开发人员真正需要做的就是以某种方式将异常传达给用户(在某些情况下,也传达给开发团队,例如通过发送一个 e -带有堆栈跟踪的邮件)。因此,通常在处理异常的每个地方都使用相同的代码行/代码块。
假设我们在每个 catch 块中对某种类型的检查异常进行简单的日志记录:
try{
methodDeclaringCheckedException();
}catch(CheckedException e){
logger.error(e);
}
如果这是一个常见的例外,在更大的代码库中甚至可能有数百个这样的 try-catch 块。现在让我们假设我们需要引入基于弹出对话框的异常处理而不是控制台日志记录,或者开始另外向开发团队发送电子邮件。
等一下……我们真的要编辑代码中的数百个位置吗?!你明白我的意思:-)。
解决方案
我们为解决该问题所做的工作是将异常处理程序(我将进一步称为EH)的概念引入集中异常处理。对于需要处理异常的每个类,我们的 Dependency Injection 框架都会注入一个异常处理程序实例。所以异常处理的典型模式现在看起来像这样:
try{
methodDeclaringCheckedException();
}catch(CheckedException e){
exceptionHandler.handleError(e);
}
现在要自定义我们的异常处理,我们只需要在一个地方更改代码(EH 代码)。
当然,对于更复杂的情况,我们可以实现 EH 的几个子类并利用 DI 框架提供给我们的特性。通过更改我们的 DI 框架配置,我们可以轻松地全局切换 EH 实现或将 EH 的特定实现提供给具有特殊异常处理需求的类(例如使用 Guice @Named 注释)。
这样,我们可以毫不费力地区分开发和发布版本的应用程序中的异常处理行为(例如,开发 - 记录错误并暂停应用程序,生产 - 记录错误并提供更多详细信息并让应用程序继续执行)。
最后一件事
最后但并非最不重要的一点是,似乎只需将我们的异常“向上”传递,直到它们到达某个顶级异常处理类,就可以获得相同类型的集中化。但这会导致我们的方法的代码和签名混乱,并引入该线程中其他人提到的维护问题。
catch
块的哪一部分对异常做了真正“有用”的事情? 10%就好了。产生异常的常见问题就像试图从不存在的文件中读取配置、OutOfMemoryErrors、NullPointerExceptions、数据库约束完整性错误等。你真的试图从所有这些错误中恢复吗?我不相信你:)。通常没有办法恢复。
没有人提到的一件重要的事情是它如何干扰接口和 lambda 表达式。
假设您定义了一个 MyAppException extends Exception
。它是您的应用程序抛出的所有异常所继承的顶级异常。在某些地方您不想对特定异常做出反应,您希望调用者解决它,因此您声明 throws MyAppException
。
在您想使用其他人的界面之前,一切看起来都很好。显然他们没有声明要抛出 MyAppException
的意图,因此编译器甚至不允许您调用在其中声明 throws MyAppException
的方法。这对于 java.util.function
来说尤其痛苦。
但是,如果您的异常扩展 RuntimeException
,则接口将没有问题。如果您愿意,可以在 JavaDoc 中提及异常。但除此之外,它只是默默地冒出任何东西。当然,这意味着它可以终止您的应用程序。但是在许多企业软件中,您都有异常处理层,未经检查的异常可以节省很多麻烦。
Predicate
、Function
等会变得很麻烦。人们更愿意在 lambda 之外处理异常,但检查异常的方式不允许这样做。如果 Predicate
在其 test
方法中抛出 Exception
,那么它将适用于任何已检查的异常,但它总是需要嵌套在 try/catch-all 中,即使 lambda 根本没有抛出异常,这将比我们目前的情况更糟。
throws
声明中,并且声明或捕获已检查异常的义务将转移到使用 lambda 的类。但由于情况并非如此,库通常更喜欢未经检查的异常,即使在检查异常本来是一个更好的主意的情况下,以保护语法糖。
要尝试仅解决未回答的问题:
如果你抛出 RuntimeException 子类而不是 Exception 子类,那么你怎么知道你应该捕获什么?
这个问题包含似是而非的推理恕我直言。仅仅因为 API 告诉您它抛出了什么并不意味着您在所有情况下都以相同的方式处理它。换句话说,您需要捕获的异常取决于您使用引发异常的组件的上下文。
例如:
如果我正在为数据库编写连接测试器,或者要检查用户输入 XPath 的有效性,那么我可能想要捕获并报告操作引发的所有已检查和未检查的异常。
但是,如果我正在编写一个处理引擎,我可能会以与 NPE 相同的方式处理 XPathException(已检查):我会让它运行到工作线程的顶部,跳过该批处理的其余部分,记录问题(或将其发送给支持部门进行诊断)并留下反馈以供用户联系支持。
正如人们已经说过的那样,Java 字节码中不存在已检查的异常。它们只是一种编译器机制,与其他语法检查不同。我看到检查异常很像我看到编译器抱怨冗余条件:if(true) { a; } b;
。这很有帮助,但我可能是故意这样做的,所以让我忽略你的警告。
事实是,如果你强制执行检查异常,你将无法强迫每个程序员“做正确的事”,而现在其他所有人都是附带损害,他们只是因为你制定的规则而讨厌你。
修复那里的不良程序!不要试图修复语言以不允许它们!对于大多数人来说,“对异常做一些事情”实际上只是告诉用户它。我也可以告诉用户一个未经检查的异常,所以不要让你的检查异常类离开我的 API。
我在 c2.com 上的文章与原来的形式基本保持不变:CheckedExceptionsAreIncompatibleWithVisitorPattern
总之:
访问者模式及其亲属是一类接口,其中间接调用者和接口实现都知道异常,但接口和直接调用者形成一个不知道的库。
CheckedExceptions 的基本假设是所有声明的异常都可以从调用具有该声明的方法的任何点抛出。 VisitorPattern 揭示了这个假设是错误的。
在这种情况下,检查异常的最终结果是大量原本无用的代码,它们基本上在运行时删除了编译器的检查异常约束。
至于根本问题:
我的一般想法是顶级处理程序需要解释异常并显示适当的错误消息。我几乎总是看到 IO 异常、通信异常(出于某种原因 API 区分)或任务致命错误(程序错误或后备服务器上的严重问题),所以如果我们允许堆栈跟踪严重的问题,这应该不会太难服务器问题。
Checked exceptions 的原始形式是尝试处理意外事件而不是失败。值得称赞的目标是突出特定的可预测点(无法连接、找不到文件等)并确保开发人员处理了这些问题。
原始概念中从未包含的内容是强制宣布大量系统性和不可恢复的故障。这些失败永远不会正确地被声明为检查异常。
代码中通常可能出现故障,并且 EJB、Web 和 Swing/AWT 容器已经通过提供最外层的“失败请求”异常处理程序来满足这一点。最基本的正确策略是回滚事务并返回错误。
一个关键点是运行时和检查的异常在功能上是等效的。没有检查异常可以做的处理或恢复,运行时异常不能。
反对“已检查”异常的最大论据是大多数异常无法修复。简单的事实是,我们不拥有损坏的代码/子系统。我们看不到实现,我们不对它负责,也无法修复它。
如果我们的应用程序不是数据库......我们不应该尝试修复数据库。这将违反封装原则。
特别成问题的是 JDBC (SQLException) 和 RMI for EJB (RemoteException) 领域。不是根据最初的“检查异常”概念来识别可修复的意外事件,而是将这些强制普遍存在的、实际上不可修复的系统可靠性问题广泛声明。
Java 设计中的另一个严重缺陷是异常处理应该正确地置于最高可能的“业务”或“请求”级别。这里的原则是“早扔,晚赶”。已检查的异常几乎没有什么作用,但会阻碍这一点。
我们在 Java 中有一个明显的问题,即需要数千个什么都不做的 try-catch 块,其中很大一部分 (40%+) 被错误编码。这些几乎都没有实现任何真正的处理或可靠性,但会带来很大的编码开销。
最后,“检查异常”与 FP 函数式编程几乎不兼容。
他们坚持“立即处理”与“赶上”异常处理最佳实践以及任何抽象循环/或控制流的 FP 结构都不一致。
许多人谈论“处理”检查的异常,但他们是在谈论他们的帽子。在失败后继续使用空、不完整或不正确的数据来假装成功是不处理任何事情。这是最低形式的工程/可靠性弊端。
彻底失败是处理异常的最基本的正确策略。回滚事务、记录错误并向用户报告“失败”响应是合理的做法——最重要的是,防止将不正确的业务数据提交到数据库。
其他异常处理策略是在业务、子系统或请求级别的“重试”、“重新连接”或“跳过”。所有这些都是通用的可靠性策略,并且在运行时异常下工作得很好/更好。
最后,失败比使用不正确的数据运行要好得多。继续会导致次要错误,远离原始原因并且更难调试;或者最终会导致错误的数据被提交。人们因此而被解雇。
请参阅:
- http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/
我认为这是一个很好的问题,完全没有争论。我认为第 3 方库应该(通常)抛出 unchecked 异常。这意味着您可以隔离您对库的依赖项(即您不必重新抛出它们的异常或抛出 Exception
- 通常是不好的做法)。 Spring 的 DAO layer 就是一个很好的例子。
另一方面,来自核心 Java API 的异常通常应该被检查是否可以曾经被处理。拿FileNotFoundException
或(我最喜欢的)InterruptedException
。这些条件几乎总是应该被专门处理(即您对 InterruptedException
的反应与您对 IllegalArgumentException
的反应不同)。检查您的异常这一事实迫使开发人员考虑条件是否可以处理。 (也就是说,我很少看到 InterruptedException
处理得当!)
还有一件事 - RuntimeException
并不总是“开发人员出错的地方”。当您尝试使用 valueOf
创建 enum
并且没有该名称的 enum
时,将引发非法参数异常。这不一定是开发人员的错误!
enum
成员名称,仅仅是因为他们使用 enum
对象。所以错误的名称只能来自外部,无论是导入文件还是其他。处理此类名称的一种可能方法是调用 MyEnum#valueOf
并捕获 IAE。另一种方法是使用预填充的 Map<String, MyEnum>
,但这些是实现细节。
已检查异常的一个问题是,即使该接口的一个实现使用它,异常也经常附加到接口的方法上。
检查异常的另一个问题是它们容易被滥用。 java.sql.Connection
的 close()
方法就是一个完美的例子。即使您已经明确声明您已完成连接,它也会抛出 SQLException
。 close() 可能传达您关心的哪些信息?
通常,当我关闭()一个连接*
时,它看起来像这样:
try {
conn.close();
} catch (SQLException ex) {
// Do nothing
}
另外,不要让我开始使用各种解析方法和 NumberFormatException ...... .NET 的 TryParse,它不会抛出异常,使用起来非常容易,不得不回到 Java 是很痛苦的(我们同时使用 Java 和C# 我工作的地方)。
*
作为补充说明,PooledConnection 的 Connection.close() 甚至不会关闭连接,但您仍然必须捕获 SQLException,因为它是一个已检查的异常。
程序员需要知道方法可能抛出的所有异常,才能正确使用它。所以,仅仅用一些例外来击败他并不一定能帮助粗心的程序员避免错误。
繁重的成本超过了微薄的好处(尤其是在较大、不太灵活的代码库中,不断修改接口签名是不切实际的)。
静态分析可能很好,但真正可靠的静态分析通常不灵活地要求程序员进行严格的工作。有一个成本效益计算,并且需要将标准设置为高,以进行导致编译时错误的检查。如果 IDE 承担传达方法可能抛出哪些异常(包括哪些是不可避免的)的角色,将会更有帮助。尽管如果没有强制异常声明它可能不会那么可靠,但大多数异常仍然会在文档中声明,并且 IDE 警告的可靠性并不那么重要。
不需要 Checked Exception 的好证明是:
很多框架都为 Java 工作。就像 Spring 将 JDBC 异常包装到未经检查的异常,向日志中抛出消息一样 Java 之后出现的许多语言,甚至在 Java 平台上也是如此 - 他们不使用它们 检查的异常,这是对客户端如何使用代码的善意预测引发异常。但是编写此代码的开发人员永远不会知道代码客户端正在工作的系统和业务。例如,强制抛出已检查异常的接口方法。系统上有 100 个实现,50 个甚至 90 个实现都不会抛出这个异常,但是如果用户引用该接口,客户端仍然必须捕获这个异常。那些 50 或 90 个实现倾向于在它们自己内部处理这些异常,将异常放入日志(这对它们来说是很好的行为)。我们应该怎么做?我最好有一些背景逻辑来完成所有这些工作 - 将消息发送到日志。如果我,作为代码的客户,会觉得我需要处理异常 - 我会这样做。我可能会忘记它,对 - 但如果我使用 TDD,我的所有步骤都会被覆盖,我知道我想要什么。另一个例子,当我在 java 中使用 I/O 时,如果文件不存在,它会强制我检查所有异常?我该怎么办?如果不存在,系统将不会进行下一步。此方法的客户端不会从该文件中获得预期的内容 - 他可以处理运行时异常,否则我应该首先检查检查异常,将消息写入日志,然后从方法中抛出异常。不...不 - 我最好使用 RuntimeEception 自动执行它,它会自动亮起。手动处理它没有任何意义 - 我很高兴在日志中看到一条错误消息(AOP 可以帮助解决这个问题.. 修复 java 的东西)。如果最终我认为系统应该向最终用户显示弹出消息 - 我会显示它,这不是问题。
如果在使用核心库(如 I/O)时,java 可以让我选择使用什么,我会很高兴。 Like 提供了两个相同类的副本——一个用 RuntimeEception 包装。然后我们可以比较人们会使用什么。不过,就目前而言,许多人最好选择基于 java 或其他语言的一些框架。像 Scala、JRuby 什么的。许多人只是相信 SUN 是对的。
RuntimeException
)。不幸的是,让外部方法 throws
成为内部方法的异常比让它包装内部方法的异常更简洁,因为后者的操作过程通常是正确的。
我们已经看到了一些对 C# 首席架构师的引用。
以下是 Java 专家关于何时使用检查异常的另一种观点。他承认其他人提到的许多负面因素:Effective Exceptions
尽管阅读了整页,我仍然找不到一个合理的反对检查异常的论据。大多数人都在谈论糟糕的 API 设计,无论是在某些 Java 类中还是在他们自己的类中。
此功能可能令人讨厌的唯一情况是原型设计。这可以通过向语言添加一些机制来解决(例如,一些@supresscheckedexceptions 注释)。但是对于常规编程,我认为检查异常是一件好事。
我已经阅读了很多关于异常处理的内容,即使(大部分时间)我真的不能说我对检查异常的存在感到高兴或难过这是我的看法:低级代码中的检查异常(IO,网络、操作系统等)和高级 API/应用程序级别中的未经检查的异常。
即使它们之间没有那么容易划清界限,我发现在同一屋檐下集成多个 API / 库而不总是包装大量检查异常确实很烦人/很难,但另一方面,有时它被迫捕获一些异常并提供在当前上下文中更有意义的不同异常是有用/更好的。
我正在做的项目需要大量的库并将它们集成在同一个 API 下,API 完全基于未检查的异常。这个框架提供了一个高级 API,它在开始时充满了已检查的异常,只有几个未检查的异常异常(初始化异常,配置异常等),我必须说不是很友好。大多数时候,您必须捕获或重新抛出您不知道如何处理的异常,或者您甚至不关心(不要与您应该忽略异常混淆),尤其是在客户端click 可能会引发 10 个可能的(已检查)异常。
当前版本(第 3 个)仅使用未经检查的异常,并且它有一个全局异常处理程序,负责处理任何未捕获的内容。 API 提供了一种注册异常处理程序的方法,该处理程序将决定是否将异常视为错误(大多数情况下是这种情况),这意味着记录并通知某人,或者它可能意味着其他东西——比如这个异常,AbortException,这意味着中断当前的执行线程并且不记录任何错误,因为不希望这样做。当然,为了解决所有自定义线程必须使用 try {...} catch(all) 处理 run() 方法。
公共无效运行(){
try {
... do something ...
} catch (Throwable throwable) {
ApplicationContext.getExceptionService().handleException("Handle this exception", throwable);
}
}
如果您使用 WorkerService 来安排作业(Runnable、Callable、Worker),这不是必需的,它会为您处理所有事情。
当然,这只是我的意见,可能不是正确的,但对我来说这似乎是一个好方法。我会在我发布项目后看到,如果我认为它对我有好处,对其他人也有好处...... :)
不定期副业成功案例分享