代码人生

掌握Java异常(一)

代码人生 http://www.she9.com 2018-09-05 10:26 出处:网络 编辑:@技术狂热粉
Java异常是用来表示和处理程序故障的库类型和语言特性。在本文的前半部分中,您将了解Java1.0以来的基本语言特性和库类型。在第二部分中,您将发现在更近期的Java版本中引入的高级功能。如果您想要理解在源代码中失败

Java异常是用来表示和处理程序故障的库类型和语言特性。在本文的前半部分中,您将了解Java 1.0以来的基本语言特性和库类型。在第二部分中,您将发现在更近期的Java版本中引入的高级功能。如果您想要理解在源代码中失败是如何表示的,那么您可以阅读这篇文章。除了对Java异常的概述之外,我还将介绍Java的语言特性,用于抛出对象、尝试可能失败的代码、捕获抛出的对象以及在抛出异常之后清理Java代码。


什么是Java异常?


当Java程序的正常行为被意外行为中断时,就会发生故障。这种例外情况被称为异常。例如,一个程序试图打开一个文件来读取它的内容,但是这个文件不存在。Java将异常分为几种类型,因此让我们考虑每种类型。


已检查的异常(Checked exceptions


Java将由外部因素(如丢失的文件)引起的异常分类为受控异常。Java编译器检查这些异常是在发生时处理(更正)的,还是记录在其他地方处理的。

异常处理程序:异常处理程序是处理异常的一系列代码。它询问上下文——这意味着它读取从异常发生时范围内的变量中保存的值——然后使用它所学到的知识将Java程序恢复为正常行为流。例如,异常处理程序可能读取已保存的文件名并促使用户替换丢失的文件。


运行时(未检查)异常(Runtime (unchecked) exceptions)

假设一个程序试图用整数除以整数0。这种不可能说明了另一种异常,即运行时异常。与检查异常不同的是,运行时异常通常来自编写糟糕的源代码,因此应该由程序员修复。因为编译器不检查运行时异常是否被处理或记录在其他地方处理,所以您可以将运行时异常视为未检查异常。


关于运行时异常


您可以修改程序来处理运行时异常,但最好是修复源代码。运行时异常通常产生于向库的方法传递无效参数;错误的调用代码应该被修复。


错误

有些异常非常严重,因为它们会危及程序继续执行的能力。例如,一个程序试图从JVM分配内存,但是没有足够的空闲内存来满足请求。另一种严重的情况是,程序试图通过Class.forName()方法调用加载类文件,但类文件已损坏。这种异常被称为错误。您永远不应该尝试自己处理错误,因为JVM可能无法从错误中恢复。


源代码中的异常

异常可以在源代码中表示为错误代码或对象。我将介绍这两种方法,并向您展示为什么对象更优越。

错误码和异常对象

诸如C之类的编程语言使用基于整数的错误代码来表示失败和失败的原因,即异常。这里有几个例子:

if (chdir("C:\temp"))
   printf("Unable to change to temp directory: %dn", errno);
   FILE *fp = fopen("C:\temp\foo");if (fp == NULL)
   printf("Unable to open foo: %dn", errno);

C的chdir()(更改目录)函数返回一个整数:成功时为0,失败时为-1。类似地,C的fopen() (file open)函数在成功时向文件结构返回一个非空指针(整数地址),在失败时返回一个空指针(0)(用常量null表示)。在任何一种情况下,要识别导致失败的异常,必须读取全局errnovariable的基于整数的错误代码。

错误代码存在一些问题:

1、整数是毫无意义的;他们没有描述他们所代表的例外情况。例如,6是什么意思?


2、将上下文与错误代码关联起来是很尴尬的。例如,您可能希望输出无法打开的文件的名称,但是您将在哪里存储该文件的名称呢?


3、整数是任意的,在读取源代码时可能会导致混淆。例如,指定if (!chdir("C:\temp")) (!不是if (chdir("C:\temp"))来测试失败更清楚。但是,选择0表示成功,因此必须指定if (chdir("C:\temp"))来测试失败。


4、错误代码太容易被忽略,这会导致代码错误。例如,程序员可以指定chdir(“C:\temp”);忽略if (fp == NULL)检查。此外,程序员不需要检查errno。通过不测试失败,当任何一个函数返回失败指示器时,程序的行为都是不正常的。


为了解决这些问题,Java采用了一种新的异常处理方法。在Java中,我们将描述异常的对象与基于抛出和捕获这些对象的机制结合起来。下面是使用对象与错误代码来表示异常的一些优点:

1、对象可以从具有有意义名称的类中创建。例如,FileNotFoundException(在java中.io包)比6更有意义。


2、对象可以在不同的字段中存储上下文。例如,您可以存储消息、无法打开的文件名称、解析操作失败的最新位置以及/或对象字段中的其他项。


3、您不使用if语句来测试失败。相反,异常对象被抛出到与程序代码分离的处理程序。因此,源代码更容易阅读,不太可能出现bug。

Throwable及其子类

Java provides a hierarchy of classes that represent different kinds of exceptions. These classes are rooted in the java.lang package's Throwable class, along with its ExceptionRuntimeException, and Error subclasses.

Throwable is the ultimate superclass where exceptions are concerned. Only objects created from Throwable and its subclasses can be thrown (and subsequently caught). Such objects are known as throwables.

Throwable object is associated with a detail message that describes an exception. Several constructors, including the pair described below, are provided to create a Throwable object with or without a detail message:

  • Throwable() creates a Throwable with no detail message. This constructor is appropriate for situations where there is no context. For example, you only want to know that a stack is empty or full.

  • Throwable(String message) creates a Throwable with message as the detail message. This message can be output to the user and/or logged.

Throwable provides the String getMessage() method to return the detail message. It also provides additional useful methods, which I'll introduce later.

The Exception class

Throwable has two direct subclasses. One of these subclasses is Exception, which describes an exception arising from an external factor (such as attempting to read from a nonexistent file). Exception declares the same constructors (with identical parameter lists) as Throwable, and each constructor invokes its Throwable counterpart. Exception inherits Throwable's methods; it declares no new methods.

Java provides many exception classes that directly subclass Exception. Here are three examples:

  • CloneNotSupportedException signals an attempt to clone an object whose class doesn't implement the Cloneableinterface. Both types are in the java.lang package.

  • IOException signals that some kind of I/O failure has occurred. This type is located in the java.io package.

  • ParseException signals that a failure has occurred while parsing text. This type can be found in the java.textpackage.

Notice that each Exception subclass name ends with the word Exception. This convention makes it easy to identify the class's purpose.

You'll typically subclass Exception (or one of its subclasses) with your own exception classes (whose names should end with Exception). Here are a couple of custom subclass examples:

public class StackFullException extends Exception{}public class EmptyDirectoryException extends Exception{
   private String directoryName;

   public EmptyDirectoryException(String message, String directoryName)
   {
      super(message);
      this.directoryName = directoryName;
   }

   public String getDirectoryName()
   {
      return directoryName;
   }}

The first example describes an exception class that doesn't require a detail message. It's default noargument constructor invokes Exception(), which invokes Throwable().

The second example describes an exception class whose constructor requires a detail message and the name of the empty directory. The constructor invokes Exception(String message), which invokes Throwable(String message).

Objects instantiated from Exception or one of its subclasses (except for RuntimeException or one of its subclasses) are checked exceptions.

The RuntimeException class

Exception is directly subclassed by RuntimeException, which describes an exception most likely arising from poorly written code. RuntimeException declares the same constructors (with identical parameter lists) as Exception, and each constructor invokes its Exception counterpart. RuntimeException inherits Throwable's methods. It declares no new methods.

Java provides many exception classes that directly subclass RuntimeException. The following examples are all members of the java.lang package:

  • ArithmeticException signals an illegal arithmetic operation, such as attempting to divide an integer by 0.

  • IllegalArgumentException signals that an illegal or inappropriate argument has been passed to a method.

  • NullPointerException signals an attempt to invoke a method or access an instance field via the null reference.

Objects instantiated from RuntimeException or one of its subclasses are unchecked exceptions.

The Error class

Throwable's other direct subclass is Error, which describes a serious (even abnormal) problem that a reasonable application should not try to handle--such as running out of memory, overflowing the JVM's stack, or attempting to load a class that cannot be found. Like ExceptionError declares identical constructors to Throwable, inherits Throwable's methods, and doesn't declare any of its own methods.

You can identify Error subclasses from the convention that their class names end with Error. Examples include OutOfMemoryErrorLinkageError, and StackOverflowError. All three types belong to the java.lang package.

Throwing exceptions

A C library function notifies calling code of an exception by setting the global errno variable to an error code and returning a failure code. In contrast, a Java method throws an object. Knowing how and when to throw exceptions is an essential aspect of effective Java programming. Throwing an exception involves two basic steps:

  1. Use the throw statement to throw an exception object.

  2. Use the throws clause to inform the compiler.

Later sections will focus on catching exceptions and cleaning up after them, but first let's learn more about throwables.

The throw statement

Java provides the throw statement to throw an object that describes an exception. Here's the syntax of the throwstatement :

throw throwable;

The object identified by throwable is an instance of Throwable or any of its subclasses. However, you usually only throw objects instantiated from subclasses of Exception or RuntimeException. Here are a couple of examples:

throw new FileNotFoundException("unable to find file " + filename);throw new IllegalArgumentException("argument passed to count is less than zero");

The throwable is thrown from the current method to the JVM, which checks this method for a suitable handler. If not found, the JVM unwinds the method-call stack, looking for the closest calling method that can handle the exception described by the throwable. If it finds this method, it passes the throwable to the method's handler, whose code is executed to handle the exception. If no method is found to handle the exception, the JVM terminates with a suitable message.

The throws clause

You need to inform the compiler when you throw a checked exception out of a method. Do this by appending a throwsclause to the method's header. This clause has the following syntax:

throws checkedExceptionClassName (, checkedExceptionClassName)*

throws clause consists of keyword throws followed by a comma-separated list of the class names of checked exceptions thrown out of the method. Here is an example:

public static void main(String[] args) throws ClassNotFoundException{
   if (args.length != 1)
   {
      System.err.println("usage: java ... classfile");
      return;
   }
   Class.forName(args[0]);}

This example attempts to load a classfile identified by a command-line argument. If Class.forName() cannot find the classfile, it throws a java.lang.ClassNotFoundException object, which is a checked exception.

It's necessary to inform the compiler that a checked ClassNotFoundException is being thrown by attaching a throws ClassNotFoundException clause to the invoking main() method's header. After all, the exception isn't handled in this method. When the exception is thrown to the JVM, it will note this. In this case, because there is no parent method of main(), it will terminate with a message.

You'll see additional examples of throws later on. For now, keep these rules in mind for working with throws clauses:

  • If at all possible, don't include the names of unchecked exception classes (such as ArithmeticException) in a throws clause. These names don't need to be included because throws clauses are for checked exceptions only. Including unchecked class names only clutters the source code.

  • You can append a throws clause to a constructor and throw a checked exception from the constructor when something goes wrong while the constructor is executing. The resulting object will not be created.

  • If a superclass method declares a throws clause, the overriding subclass method doesn't have to declare a throwsclause. However, if the subclass method declares a throws clause, the clause must not include the names of checked exception classes that are not also included in the superclass method's throws clause--unless they are the names of exception subclasses. For example, given superclass method void open(String name) throws IOException {}, the overriding subclass method could be declared as void open(String name) {}void open(String name) throws IOException {}, or void open(String name) throws FileNotFoundException {}-- FileNotFoundException subclasses IOException. However, you couldn't specify, in the subclass, void open(String name) throws ClassNotFoundException, because ClassNotFoundException doesn't appear in the superclass's throws clause.

  • A checked exception class name doesn't need to appear in a throws clause when the name of its superclass appears. For example, you don't need to specify throws FileNotFoundException, IOException. Only throws IOException is necessary.

  • The compiler reports an error when a method throws a checked exception and doesn't also handle the exception or list the exception in its throws clause.

  • You can declare a checked exception class name in a method's throws clause without throwing an instance of this class from the method. (Perhaps the method has yet to be fully coded.) However, Java requires that you provide code to handle this exception, even though it isn't thrown.

Trying exceptions

Java provides the try block to delimit a sequence of statements that may throw exceptions. A try block has the following syntax:

try{
   // one or more statements that might throw exceptions}

The statements in a try block serve a common purpose and might directly or indirectly throw an exception. Consider the following example:

FileInputStream fis = null;FileOutputStream fos = null;try{
   fis = new FileInputStream(args[0]);
   fos = new FileOutputStream(args[1]);
   int c;
   while ((c = fis.read()) != -1)
      fos.write(c);}

This example excerpts a larger Java Copy application (see this article's code archive) that copies a source file to a destination file. It uses the java.io package's FileInputStream and FileOutputStream classes (introduced later in the article) for this purpose. Think of FileInputStream as a way to read an input stream of bytes from a file, and FileOutputStream as a way to write an output stream of bytes to a file.

The FileInputStream(String filename) constructor creates an input stream to the file identified by filename. This constructor throws FileNotFoundException when the file doesn't exist, refers to a directory, or another related problem occurs. The FileOutputStream(String filename) constructor creates an output stream to the file identified by filename. It throws FileNotFoundException when the file exists but refers to a directory, doesn't exist and cannot be created, or another related problem occurs.

FileInputStream provides an int read() method to read one byte and return it as a 32-bit integer. This method returns -1 on end-of-file. FileOutputStream provides a void write(int b) method to write the byte in the lower 8 bits of b. Either method throws IOException when something goes wrong.

The bulk of the example is a while loop that repeatedly read()s the next byte from the input stream and write()s that byte to the output stream, until read() signals end-of-file.

The try block's file-copy logic is easy to follow because this logic isn't combined with exception-checking code (if tests and related throw statements hidden in the constructors and methods), exception-handling code (which is executed in one or more associated catch blocks), and cleanup code (for closing the source and destination files; this code is relegated to an associated finally block). In contrast, C's lack of a similar exception-oriented framework results in more verbose code, as illustrated by the following excerpt from a larger C cp application (in this article's code archive) that copies a source file to a destination file:

if ((fpsrc = fopen(argv[1], "rb")) == NULL){
   fprintf(stderr, "unable to open %s for readingn", argv[1]);
   return;}if ((fpdst = fopen(argv[2], "wb")) == NULL){
   fprintf(stderr, "unable to open %s for writingn", argv[1]);
   fclose(fpsrc);
   return;}while ((c = fgetc(fpsrc)) != EOF)
   if (fputc(c, fpdst) == EOF)
   {
      fprintf(stderr, "unable to write to %sn", argv[1]);
      break;
   }

In this example, the file-copy logic is harder to follow because the logic is intermixed with exception-checking, exception-handling, and cleanup code:

  • The two == NULL and one == EOF checks are the equivalent of the hidden throw statements and related checks.

  • The three fprintf() function calls are the exception-handling code whose Java equivalent would be executed in one or more catch blocks.

  • The fclose(fpsrc); function call is cleanup code whose Java equivalent would be executed in a finally block.

Catching exceptions

Java's exception-handling capability is based on catch blocks. This section introduces catch and various catch blocks.

The catch block

Java provides the catch block to delimit a sequence of statements that handle an exception. A catch block has the following syntax:

catch (throwableType throwableObject){
   // one or more statements that handle an exception}

The catch block is similar to a constructor in that it has a parameter list. However, this list consists of only one parameter, which is a throwable type (Throwable or one of its subclasses) followed by an identifier for an object of that type.

When an exception occurs, a throwable is created and thrown to the JVM, which searches for the closest catch block whose parameter type directly matches or is the supertype of the thrown throwable object. When it finds this block, the JVM passes the throwable to the parameter and executes the catch block's statements, which can interrogate the passed throwable and otherwise handle the exception. Consider the following example:

catch (FileNotFoundException fnfe){
   System.err.println(fnfe.getMessage());}

This example (which extends the previous try block example) describes a catch block that catches and handles throwables of type FileNotFoundException. Only throwables matching this type or a subtype are caught by this block.

Suppose the FileInputStream(String filename) constructor throws FileNotFoundException. The JVM checks the catch block following try to see if its parameter type matches the throwable type. Detecting a match, the JVM passes the throwable's reference to fnfe and transfers execution to the block. The block responds by invoking getMessage() to retrieve the exception's message, which it then outputs.

Specifying multiple catch blocks

You can specify multiple catch blocks after a try block. For example, consider this larger excerpt from the aforementioned Copy application:

FileInputStream fis = null;FileOutputStream fos = null;{
   fis = new FileInputStream(args[0]);
   fos = new FileOutputStream(args[1]);
   int c;
   while ((c = fis.read()) != -1)
      fos.write(c);}catch (FileNotFoundException fnfe){
   System.err.println(fnfe.getMessage());}catch (IOException ioe){
   System.err.println("I/O error: " + ioe.getMessage());}

The first catch block handles FileNotFoundExceptions thrown from either constructor. The second catch block handles IOExceptions thrown from the read() and write() methods.

When specifying multiple catch blocks, don't specify a catch block with a supertype before a catch block with a subtype. For example, don't place catch (IOException ioe) before catch (FileNotFoundException fnfe). If you do, the compiler will report an error because catch (IOException ioe) would also handle FileNotFoundExceptions, and catch (FileNotFoundException fnfe) would never have a chance to execute.

Likewide, don't specify multiple catch blocks with the same throwable type. For example, don't specify two catch (IOException ioe) {} blocks. Otherwise, the compiler reports an error.

Cleaning up with finally blocks

Whether or not an exception is handled, you may need to perform cleanup tasks, such as closing an open file. Java provides the finally block for this purpose.

The finally block consists of keyword finally followed by a brace-delimited sequence of statements to execute. It may appear after the final catch block or after the try block.

Cleaning up in a try-catch-finally context

When resources must be cleaned up and an exception isn't being thrown out of a method, a finally block is placed after the final catch block. This is demonstrated by the following Copy excerpt:

FileInputStream fis = null;FileOutputStream fos = null;try{
   fis = new FileInputStream(args[0]);
   fos = new FileOutputStream(args[1]);
   int c;
   while ((c = fis.read()) != -1)
      fos.write(c);}catch (FileNotFoundException fnfe){
   System.err.println(fnfe.getMessage());}catch (IOException ioe){
   System.err.println("I/O error: " + ioe.getMessage());}finally{
   if (fis != null)
      try
      {
         fis.close();
      }
      catch (IOException ioe)
      {
         // ignore exception
      }

   if (fos != null)
      try
      {
         fos.close();
      }
      catch (IOException ioe)
      {
         // ignore exception
      }}

If the try block executes without an exception, execution passes to the finally block to close the file input/output streams. If an exception is thrown, the finally block executes after the appropriate catch block.

FileInputStream and FileOutputStream inherit a void close() method that throws IOException when the stream cannot be closed. For this reason, I've wrapped each of fis.close(); and fos.close(); in a try block. I've left the associated catch block empty to illustrate the common mistake of ignoring an exception.

An empty catch block that's invoked with the appropriate throwable has no way to report the exception. You might waste a lot of time tracking down the exception's cause, only to discover that you could have detected it sooner if the empty catch block had reported the exception, even if only in a log.

Cleaning up in a try-finally context

When resources must be cleaned up and an exception is being thrown out of a method, a finally block is placed after the try block: there are no catch blocks. Consider the following excerpt from a second version of the Copy application:

public static void main(String[] args){
   if (args.length != 2)
   {
      System.err.println("usage: java Copy srcfile dstfile");
      return;
   }

   try
   {
      copy(args[0], args[1]);
   }
   catch (IOException ioe)
   {
      System.err.println("I/O error: " + ioe.getMessage());
   }}static void copy(String srcFile, String dstFile) throws IOException{
   FileInputStream fis = null;
   FileOutputStream fos = null;
   try
   {
      fis = new FileInputStream(srcFile);
      fos = new FileOutputStream(dstFile);
      int c;
      while ((c = fis.read()) != -1)
         fos.write(c);
   }
   finally
   {
      if (fis != null)
         try
         {
            fis.close();
         }
         catch (IOException ioe)
         {
            System.err.println(ioe.getMessage());
         }

      if (fos != null)
         try
         {
            fos.close();
         }
         catch (IOException ioe)
         {
            System.err.println(ioe.getMessage());
         }
   }}

The file-copying logic has been moved into a copy() method. This method is designed to report an exception to the caller, but it first closes each open file.

This method's throws clause only lists IOException. It isn't necessary to include FileNotFoundException because FileNotFoundException subclasses IOException.

Once again, the finally clause presents a lot of code just to close two files. In the second part of this series, you will learn about the try-with-resources statement, which obviates the need to explicitly close these files.

In conclusion

In this article we've focused on the basics of Java's exception-oriented framework, but there is much more to grasp, including the aforementioned try-with-resources statement. Stay tuned for Part 2, which introduces Java's more advanced exception-oriented language features and library types.


请关注公众号:程序你好
0

精彩评论

暂无评论...
验证码 换一张
取 消