1.异常类型

异常类型

所有的异常分为4种类型,它们实际上是相互继承的类。

Throwable班级

所有异常的基类是类Throwable该类Throwable包含将当前调用堆栈(当前方法的堆栈跟踪)写入数组的代码。稍后我们将了解堆栈跟踪是什么。

throw运算符只能接受从Throwable类派生的对象。尽管理论上您可以编写类似 的代码throw new Throwable();,但通常没有人这样做。该类的主要目的Throwable是为所有异常提供一个父类。

Error班级

下一个异常类是Error类,直接继承类Throwable当出现严重问题时, Java 机器会创建该类Error(及其后代)的对象。例如,硬件故障、内存不足等。

通常,作为程序员,在程序中出现这样的错误(应该抛出 an 的那种)时,您无能为力:这些错误太严重了。Error您所能做的就是通知用户程序正在崩溃和/或将有关错误的所有已知信息写入程序日志。

Exception班级

ExceptionRuntimeException是针对许多方法运行中发生的常见错误。每个抛出的异常的目标是被知道如何正确处理它的块捕获。catch

当一个方法由于某种原因不能完成它的工作时,它应该立即通过抛出适当类型的异常来通知调用方法。

换句话说,如果一个变量等于null,该方法将抛出一个NullPointerException。如果将不正确的参数传递给该方法,它将抛出一个InvalidArgumentException. 如果该方法不小心被零除,它会抛出一个ArithmeticException.

RuntimeException班级

RuntimeExceptions是 的一个子集Exceptions。我们甚至可以说这RuntimeException是普通异常 ( ) 的轻量级版本Exception——对此类异常施加的要求和限制更少

Exception稍后您将了解 和 之间的区别RuntimeException


2. Throws:检查异常

抛出:检查异常

所有 Java 异常都分为两类:已检查未检查

所有继承RuntimeExceptionor的异常Error都被认为是未经检查的异常 所有其他都是已检查的异常

重要的!

在引入检查异常 20 年后,几乎每个 Java 程序员都认为这是一个错误。在流行的现代框架中,95% 的异常都是未经检查的。几乎一模一样照抄Java的C#语言,并没有加入checked exceptions

已检查异常未检查异常之间的主要区别是什么?

对已检查的异常有额外的要求。粗略地说,它们是这些:

要求 1

如果一个方法抛出一个已检查的异常它必须在它的签名中指明异常的类型这样,调用它的每个方法都知道这个“有意义的异常”可能会发生在其中。

在关键字后的方法参数后面指明检查异常(不要误用关键字)。它看起来像这样:throwsthrow

type method (parameters) throws exception

例子:

检查异常 未经检查的异常
public void calculate(int n) throws Exception
{
   if (n == 0)
      throw new Exception("n is null!");
}
public void calculate(n)
{
   if (n == 0)
      throw new RuntimeException("n is null!");
}

在右边的例子中,我们的代码抛出了一个未经检查的异常——不需要额外的操作。 在左侧的示例中,该方法抛出一个已检查的异常,因此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子句中。

就好像我们在说,“这些异常非常重要,我们必须捕获它们。如果我们不知道如何处理它们,那么必须通知任何可能调用我们方法的人,其中可能会发生此类异常。

例子:

想象一下,我们正在编写一种方法来创建一个由人类居住的世界。初始人数作为参数传递。所以如果人数太少,我们需要添加例外。

创造地球 笔记
public void createWorld(int n) throws EmptyWorldException, LonelyWorldException
{
   if (n == 0)
      throw new EmptyWorldException("There are no people!");
   if (n == 1)
      throw new LonelyWorldException ("There aren't enough people!");
   System.out.println("A wonderful world was created. Population: " + n);
}
该方法可能会抛出两个已检查的异常:

  • 空世界异常
  • 孤独世界异常

此方法调用可以通过 3 种方式处理:

1.不要捕获任何异常

当方法不知道如何正确处理这种情况时,通常会这样做。

代码 笔记
public void createPopulatedWorld(int population)
throws EmptyWorldException, LonelyWorldException
{
   createWorld(population);
}
调用方法不捕获异常并且必须将它们通知其他人:它将它们添加到自己的throws子句中

2.捕捉一些异常

我们处理我们可以处理的错误。但是我们不理解的,我们把它们丢给调用方法。为此,我们需要将他们的名字添加到 throws 子句中:

代码 笔记
public void createNonEmptyWorld(int population)
throws EmptyWorldException
{
   try
   {
      createWorld(population);
   }
   catch (LonelyWorldException e)
   {
      e.printStackTrace();
   }
}
调用者只捕获一个已检查的异常—— LonelyWorldException. 另一个异常必须添加到它的签名中,在throws关键字之后表明它

3.捕获所有异常

如果该方法没有向调用方法抛出异常,那么调用方法总是确信一切正常。并且它将无法采取任何行动来修复异常情况。

代码 笔记
public void createAnyWorld(int population)
{
   try
   {
      createWorld(population);
   }
   catch (LonelyWorldException e)
   {
      e.printStackTrace();
   }
   catch (EmptyWorldException e)
   {
      e.printStackTrace();
   }
}
在此方法中捕获所有异常。来电者将确信一切顺利。


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.

如果需要,您可以从那里轻松检索它。例子:

代码 笔记
try
{
   // Code where we wrap the checked exception
   // in a RuntimeException
}
catch(RuntimeException e)
{
   Throwable cause = e.getCause();
   if (cause instanceof Exception)
   {
      Exception exp = (Exception) cause;
      // Exception handling code goes here
   }
}







获取存储在对象内部的异常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| RuntimeExceptione),因为RuntimeException类继承了Exception



5.自定义异常

您始终可以创建自己的异常类。您只需创建一个继承该类的类RuntimeException。它看起来像这样:

class ClassName extends RuntimeException
{
}

我们将在您学习 OOP、继承、构造函数和方法覆盖时讨论细节。

然而,即使你只有一个像这样的简单类(完全没有代码),你仍然可以基于它抛出异常:

代码 笔记
class Solution
{
   public static void main(String[] args)
   {
      throw new MyException();
   }
}

class MyException extends RuntimeException
{
}




抛出一个未经检查的 MyException.

Java 多线程任务中,我们将深入研究如何处理我们自己的自定义异常。