1.异常类型

所有的异常分为4种类型,它们实际上是相互继承的类。
Throwable
班级
所有异常的基类是类Throwable
。该类Throwable
包含将当前调用堆栈(当前方法的堆栈跟踪)写入数组的代码。稍后我们将了解堆栈跟踪是什么。
throw运算符只能接受从Throwable
类派生的对象。尽管理论上您可以编写类似 的代码throw new Throwable();
,但通常没有人这样做。该类的主要目的Throwable
是为所有异常提供一个父类。
Error
班级
下一个异常类是Error
类,直接继承类Throwable
。当出现严重问题时, Java 机器会创建该类Error
(及其后代)的对象。例如,硬件故障、内存不足等。
通常,作为程序员,在程序中出现这样的错误(应该抛出 an 的那种)时,您无能为力:这些错误太严重了。Error
您所能做的就是通知用户程序正在崩溃和/或将有关错误的所有已知信息写入程序日志。
Exception
班级
和Exception
类RuntimeException
是针对许多方法运行中发生的常见错误。每个抛出的异常的目标是被知道如何正确处理它的块捕获。catch
当一个方法由于某种原因不能完成它的工作时,它应该立即通过抛出适当类型的异常来通知调用方法。
换句话说,如果一个变量等于null
,该方法将抛出一个NullPointerException
。如果将不正确的参数传递给该方法,它将抛出一个InvalidArgumentException
. 如果该方法不小心被零除,它会抛出一个ArithmeticException
.
RuntimeException
班级
RuntimeExceptions
是 的一个子集Exceptions
。我们甚至可以说这RuntimeException
是普通异常 ( ) 的轻量级版本Exception
——对此类异常施加的要求和限制更少
Exception
稍后您将了解 和 之间的区别RuntimeException
。
2. Throws
:检查异常
所有 Java 异常都分为两类:已检查和未检查。
所有继承RuntimeException
or的异常Error
都被认为是未经检查的异常。 所有其他都是已检查的异常。
在引入检查异常 20 年后,几乎每个 Java 程序员都认为这是一个错误。在流行的现代框架中,95% 的异常都是未经检查的。几乎一模一样照抄Java的C#语言,并没有加入checked exceptions。
已检查异常和未检查异常之间的主要区别是什么?
对已检查的异常有额外的要求。粗略地说,它们是这些:
要求 1
如果一个方法抛出一个已检查的异常,它必须在它的签名中指明异常的类型。这样,调用它的每个方法都知道这个“有意义的异常”可能会发生在其中。
在关键字后的方法参数后面指明检查异常(不要误用关键字)。它看起来像这样:throws
throw
type method (parameters) throws exception
例子:
检查异常 | 未经检查的异常 |
---|---|
|
|
在右边的例子中,我们的代码抛出了一个未经检查的异常——不需要额外的操作。 在左侧的示例中,该方法抛出一个已检查的异常,因此throws
将关键字与异常类型一起添加到方法签名中。
如果一个方法希望抛出多个已检查的异常,则必须在关键字后指定所有这些异常throws
,并以逗号分隔。顺序并不重要。例子:
public void calculate(int n) throws Exception, IOException
{
if (n == 0)
throw new Exception("n is null!");
if (n == 1)
throw new IOException("n is 1");
}
要求 2
如果您调用的方法在其签名中包含已检查的异常,则您不能忽略它抛出异常的事实。
您必须通过catch
为每个异常添加块来捕获所有此类异常,或者将它们添加到您的方法的throws
子句中。
就好像我们在说,“这些异常非常重要,我们必须捕获它们。如果我们不知道如何处理它们,那么必须通知任何可能调用我们方法的人,其中可能会发生此类异常。
例子:
想象一下,我们正在编写一种方法来创建一个由人类居住的世界。初始人数作为参数传递。所以如果人数太少,我们需要添加例外。
创造地球 | 笔记 |
---|---|
|
该方法可能会抛出两个已检查的异常:
|
此方法调用可以通过 3 种方式处理:
1.不要捕获任何异常
当方法不知道如何正确处理这种情况时,通常会这样做。
代码 | 笔记 |
---|---|
|
调用方法不捕获异常并且必须将它们通知其他人:它将它们添加到自己的throws 子句中 |
2.捕捉一些异常
我们处理我们可以处理的错误。但是我们不理解的,我们把它们丢给调用方法。为此,我们需要将他们的名字添加到 throws 子句中:
代码 | 笔记 |
---|---|
|
调用者只捕获一个已检查的异常—— LonelyWorldException . 另一个异常必须添加到它的签名中,在throws 关键字之后表明它 |
3.捕获所有异常
如果该方法没有向调用方法抛出异常,那么调用方法总是确信一切正常。并且它将无法采取任何行动来修复异常情况。
代码 | 笔记 |
---|---|
|
在此方法中捕获所有异常。来电者将确信一切顺利。 |
3.包装异常
受检异常在理论上看起来很酷,但在实践中却是一个巨大的挫折。
假设你的项目中有一个超级流行的方法。它从您程序中的数百个地方调用。你决定向它添加一个新的检查异常。很可能这个已检查的异常真的很重要而且很特别,只有main()
方法知道如果它被捕获时该怎么做。
这意味着您必须将已检查的异常添加到throws
调用您的超级流行方法的每个方法的子句中。以及在throws
调用这些方法的所有方法的子句中。以及调用这些方法的方法。
结果,throws
项目中一半方法的子句得到了一个新的检查异常。当然,您的项目包含在测试中,现在测试无法编译。现在您还必须在测试中编辑 throws 子句。
然后你的所有代码(数百个文件中的所有更改)都必须由其他程序员审查。在这一点上,我们问自己为什么要对项目进行如此多的血腥更改?一天(几?)的工作,以及失败的测试——所有这些都是为了添加一个检查异常?
当然,仍然存在与继承和方法重写相关的问题。检查异常带来的问题比好处大得多。归根结底,现在很少有人喜欢它们,也很少有人使用它们。
然而,仍然有很多代码(包括标准 Java 库代码)包含这些已检查的异常。他们该怎么办?我们不能忽视它们,我们也不知道如何处理它们。
Java 程序员建议将已检查的异常包装在RuntimeException
. 换句话说,捕获所有已检查的异常,然后创建未检查的异常(例如,RuntimeException
)并抛出它们。这样做看起来像这样:
try
{
// Code where a checked exception might occur
}
catch(Exception exp)
{
throw new RuntimeException(exp);
}
这不是一个非常漂亮的解决方案,但这里没有任何犯罪行为:异常只是被塞进了一个RuntimeException
.
如果需要,您可以从那里轻松检索它。例子:
代码 | 笔记 |
---|---|
|
获取存储在对象内部的异常 RuntimeException 。该cause 变量可能会null 确定其类型并将其转换为已检查的异常类型。 |
4.捕获多个异常
程序员真的很讨厌重复代码。他们甚至提出了相应的开发原则:Don't Repeat Yourself (DRY)。但是在处理异常时,经常会出现一个try
块后面跟着几个catch
具有相同代码的块的情况。
或者可能有 3 个catch
块具有相同的代码,另外 2 个catch
块具有其他相同的代码。当您的项目负责任地处理异常时,这是一种标准情况。
从版本 7 开始,在 Java 语言中添加了在单个块中指定多种异常类型的功能catch
。它看起来像这样:
try
{
// Code where an exception might occur
}
catch (ExceptionType1 | ExceptionType2 | ExceptionType3 name)
{
// Exception handling code
}
catch
您可以拥有任意数量的块。但是,单个catch
块不能指定相互继承的异常。也就是说不能写catch( Exception
| RuntimeException
e),因为RuntimeException
类继承了Exception
。
5.自定义异常
您始终可以创建自己的异常类。您只需创建一个继承该类的类RuntimeException
。它看起来像这样:
class ClassName extends RuntimeException
{
}
我们将在您学习 OOP、继承、构造函数和方法覆盖时讨论细节。
然而,即使你只有一个像这样的简单类(完全没有代码),你仍然可以基于它抛出异常:
代码 | 笔记 |
---|---|
|
抛出一个未经检查的 MyException . |
在Java 多线程任务中,我们将深入研究如何处理我们自己的自定义异常。
GO TO FULL VERSION