跳至主要內容

⑤JAVA 异常

holic-x...大约 21 分钟JAVA基础

⑤JAVA 异常

学习核心

  • 异常
    • 介绍JAVA的异常体系?
    • Exception和Error有什么区别?
    • 受检和未受检异常是什么?
    • 如何自定义异常?

学习资料

JAVA异常核心

1.基础概念

异常机制(异常类层次结构)

​ Java的异常机制能让程序具有良好的容错性,让程序更加健壮,当程序出现意外的情况,系统会自动生成一个Exception对象通知程序进程处理,从而实现业务功能和实现代码的分离。

image-20240521083545516

​ Throwable 是 Java 语言中所有错误与异常的超类(包括Error错误和Exception异常),它包含了其线程创建时线程执行堆栈的快照,提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息

​ Error(错误)及其子类:程序中无法处理的错误(表示运行应用程序出现了严重的错误),此类错误一般表示代码运行时 JVM 出现问题。Error是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,不应该实现任何新的Error子类的。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。

​ Exception(异常):程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和非运行时异常(编译时异常)

  • 运行时异常:RuntimeException类及其子类异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生(常见有空指针异常、下标越界异常)
  • 非运行时异常(编译时异常):除RuntimeException以外的子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常

异常关键字(try、catch、finally、throw、throws)

try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。

catch – 用于捕获异常。catch用来捕获try语句块中发生的异常

finallyfinally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止

throw – 用于抛出异常

throws – 用在方法签名中,用于声明该方法可能抛出的异常

2.异常捕获

try...catch...捕获异常

基础语法

try {
  // 业务逻辑处理
	// 可能出现异常的代码;
} catch(异常类名 变量名) {
	// 异常的处理代码;
}

单异常捕获(可catch多层)

public class ExceptionDemo {

    public static void main(String[] args) {
        /**
         * 使用try...catch语句捕获异常
         */
        try
        {
			System.out.println(9/0);
//			Class.forName("java.util.ArrayListhahah");
            FileInputStream fis = new FileInputStream("c:/test.txt");
        }catch (ArithmeticException e) {
            System.out.println("捕获到了数学运算异常的错误...");
        } catch (FileNotFoundException e) {
            System.out.println("捕获到了文件不存在的异常...");
        } catch (Exception e) {
            System.out.println("捕获到了未知的异常...");
        }
    }
}

多异常捕获(JAVA7提供了多异常捕获,一个catch可以捕获多个异常)

  • 捕获多个异常,多种异常之间用|分割

  • 捕获多种类型的异常时,异常变量有隐式的final修饰,所以程序不能对异常变量重新赋值

public class MultiExceptionDemo {

    public static void main(String[] args) {
        try {
            int a = Integer.parseInt(args[0]);
            int b = Integer.parseInt(args[1]);
            int c = a / b;
            System.out.println("当前执行的结果c是:" + c);
            /**
             * 一次性捕获多个异常需用 |隔开
             */
        } catch (IndexOutOfBoundsException|NumberFormatException|ArithmeticException e) {
            System.out.println("捕获到了数组越界,数字格式异常,算数异常 等之一的错误...");
            /**
             * 多异常捕获的变量是默认被final修饰的  所以下方的代码报错...
             * e=new ArithmeticException("aa");
             * 不能够对final修饰的变量进行二次修改赋值
             */
        } catch (Exception e) { //最大的异常始终是放在最后一个捕获
            System.out.println("捕获到了未知的异常...");
            e=new RuntimeException("aaa");
        }
    }

}

访问异常信息

public class AccessExceptionDemo {
    /**
     * 通过相关的方法了解具体捕获到的异常信息
     */
    public static void main(String[] args) {
        try
        {
            FileInputStream fis = new FileInputStream("c://a.txt");
        }catch(IOException e)
        {
            System.out.println(e.getClass());
            System.out.println(e.getMessage());
            System.out.println(e.getStackTrace());
            //了解具体的异常信息
            e.printStackTrace();
        }
    }
}

// 测试结果
class java.io.FileNotFoundException
c:/a.txt (No such file or directory)
[Ljava.lang.StackTraceElement;@5305068a
java.io.FileNotFoundException: c:/a.txt (No such file or directory)
	at java.base/java.io.FileInputStream.open0(Native Method)
	at java.base/java.io.FileInputStream.open(FileInputStream.java:216)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:111)
	at com.noob.base.exception.AccessExceptionDemo.main(AccessExceptionDemo.java:16)

try...catch...finally

语法

try {                        
    //执行程序代码,可能会出现异常                 
} catch(Exception e) {   
    //捕获异常并处理   
} finally {
    //必执行的代码
}

执行顺序说明

  • 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
  • 当try捕获到异常,catch语句块里没有处理此异常的情况:当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
  • 当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句;

try...finally

语法

try {                        
    //执行程序代码,可能会出现异常                 
}finally {
    //必执行的代码
}

执行顺序说明

  • try块中引起异常,异常代码之后的语句不再执行,直接执行finally语句
  • try块中没有引起异常,执行完try块之后就执行finally语句

应用场景

​ try-finally可用在不需要捕获异常的代码,可以保证资源在使用后被关闭。例如IO流中执行完相应操作后,关闭相应资源;使用Lock对象保证线程同步,通过finally可以保证锁会被释放;数据库连接代码时,关闭连接操作等等

//以Lock加锁为例,演示try-finally
ReentrantLock lock = new ReentrantLock();
try {
    //需要加锁的代码
} finally {
    lock.unlock(); //保证锁一定被释放
}

finally失效的场景

  • 在前面的代码中用了System.exit()退出程序
  • finally语句块中发生了异常
  • 程序所在的线程死亡
  • 关闭CPU

try-with-resource

语法(try-with-resource是Java 7中引入的)

​ 在finally使用close方法关闭资源也可能会抛出IOException,从而覆盖了原始异常。JAVA7提供了更加优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了 AutoCloseable 接口的类。

private  static void tryWithResourceTest(){
    try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){
        // code
    } catch (IOException e){
        // handle exception
    }
}

​ Scanner

public final class Scanner implements Iterator<String>, Closeable {
  // ...
}
public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

​ try 代码块退出时,会自动调用 scanner.close 方法,和把 scanner.close 方法放在 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取

3.异常基础总结

关键字说明

  • try、catch和finally都不能单独使用,只能是try-catch、try-finally或者try-catch-finally
  • try语句块监控代码,出现异常就停止执行下面的代码,然后将异常移交给catch语句块来处理
  • finally语句块中的代码一定会被执行,常用于回收资源
  • throws:声明一个异常,告知方法调用者
  • throw :抛出一个异常,至于该异常被捕获还是继续抛出都与它无关

JAVA常见异常类

​ 在Java中提供了一些异常用来描述经常发生的错误,对于这些异常,有的需要进行捕获处理或声明抛出,有的是由Java虚拟机自动进行捕获处理。Java中常见的异常类:

  • RuntimeException
    • java.lang.ArrayIndexOutOfBoundsException 数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
    • java.lang.ArithmeticException 算术条件异常。譬如:整数除零等。
    • java.lang.NullPointerException 空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等
    • java.lang.ClassNotFoundException 找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
    • java.lang.NegativeArraySizeException 数组长度为负异常
    • java.lang.ArrayStoreException 数组中包含不兼容的值抛出的异常
    • java.lang.SecurityException 安全性异常
    • java.lang.IllegalArgumentException 非法参数异常
  • IOException
    • IOException:操作输入流和输出流时可能出现的异常。
    • EOFException 文件已结束异常
    • FileNotFoundException 文件未找到异常
  • 其他
    • ClassCastException 类型转换异常类
    • ArrayStoreException 数组中包含不兼容的值抛出的异常
    • SQLException 操作数据库异常类
    • NoSuchFieldException 字段未找到异常
    • NoSuchMethodException 方法未找到抛出的异常
    • NumberFormatException 字符串转换为数字抛出的异常
    • StringIndexOutOfBoundsException 字符串索引超出范围抛出的异常
    • IllegalAccessException 不允许访问某类异常
    • InstantiationException 当应用程序试图使用Class类中的newInstance()方法创建一个类的实例,而指定的类对象无法被实例化时,抛出该异常

4.异常实践

​ 在Java中需要思考如何处理异常、处理哪些异常、怎样处理异常,一些开发团队还会指定一些规则来规范异常处理。异常不仅仅是一个错误控制机制,也是一个通信媒介,一个团队要制定出一个最佳实践和规则、理解其通用概念,在场景中更好地使用它。

1)只针对不正常的情况才使用异常

​ 异常只应该被用于不正常的条件,它们永远不应该被用于正常的控制流。《阿里手册》中:【强制】Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException等等

  • 异常机制的设计初衷是用于不正常的情况,所以很少会会JVM实现试图对它们的性能进行优化。所以,创建、抛出和捕获异常的开销是很昂贵的
  • 把代码放在try-catch中返回阻止了JVM实现本来可能要执行的某些特定的优化
  • 对数组进行遍历的标准模式并不会导致冗余的检查,有些现代的JVM实现会将它们优化掉

2)finally块中清理资源或者使用try...with...resource语句

​ 当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在try块的最后关闭资源。

🐶错误示例参考

​ 在try块的最后close资源,但是这段代码的逻辑是只有当try块中没有异常抛出的时候才可以正常工作,也就是说当try块中前面的代码出现异常并抛出,则这段代码可能不会执行到close部分,就会导致资源无法正常关闭。因此此处应该将资源的释放放到finally块进行处理,或者借助JAVA7的try...with...resource语句

public void doNotCloseResourceInTry() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
        // use the inputStream to read a file
        // do NOT do this
        inputStream.close();
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

更正方案1:在finally块中释放资源

public void closeResourceInFinally() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
        // use the inputStream to read a file
    } catch (FileNotFoundException e) {
        log.error(e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error(e);
            }
        }
    }
}

更正方案2:JAVA7的try-with-resource 语法

​ 如果资源实现了 AutoCloseable 接口,可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭

public void automaticallyCloseResource() {
    File file = new File("./tmp.txt");
    try (FileInputStream inputStream = new FileInputStream(file);) {
        // use the inputStream to read a file
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

3)尽量使用标准的异常

Java标准异常中有几个是经常被使用的异常。如下表格:

异常使用场合
IllegalArgumentException参数的值不合适
IllegalStateException参数的状态不合适
NullPointerException在null被禁止的情况下参数值为null
IndexOutOfBoundsException下标越界
ConcurrentModificationException在禁止并发修改的情况下,对象检测到并发修改
UnsupportedOperationException对象不支持客户请求的方法

重用现有的异常:

  • 基于现有业务语义基础上重用异常,使得程序更具备可读性和可维护性
  • 异常类越少,内存占用越小,转载类的时间开销也越小

4)对异常进行文档说明

​ 当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。

​ 在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景

/**
* Method description
* 
* @throws MyBusinessException - businuess exception description
*/
public void doSomething(String input) throws MyBusinessException {
   // ...
}

​ 例如此处案例,在抛出MyBusinessException 异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等

5)优先捕获最具体的异常

​ catch块的执行只有在匹配异常的第一个catch块才会被执行,因此在设计上可以优先捕获最具体的异常类,将不太具体的catch块添加到列表末尾

​ 例如此处,NumberFormatException是更具体的异常提示,它是IllegalArgumentException的子类

public void catchMostSpecificExceptionFirst() {
    try {
        doSomething("A message");
    } catch (NumberFormatException e) {
        log.error(e);
    } catch (IllegalArgumentException e) {
        log.error(e)
    }
}

6)不要捕获Throwable类

​ 如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。

​ 所以,最好不要捕获 Throwable ,除非确定自己处于一种特殊的情况下能够处理错误

7)不要忽略异常

​ 在异常处理的时候需要完善try...catch...语句,不要仅仅只是catch异常而不做任何处理。现实开发中会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。合理的做法是至少要记录异常的信息,然后再思考如何处理

public void logAnException() {
    try {
        // do something
    } catch (NumberFormatException e) {
        log.error("This should never happen: " + e); // see this line
    }
}

8)不要记录并抛出异常

​ 一些代码、类库的异常处理会有捕获异常、记录日志并且直接再次抛出的逻辑。这个逻辑看似合理,但是经常会给同一个异常输出多条日志信息,但是这些日志可能没有附加更有效的信息。

try {
    new Long("xyz");
} catch (NumberFormatException e) {
    log.error(e);
    throw e;
}

​ 正确的做法是仅仅当想要处理异常的时候才去捕获,否则只需要在方法签名中声明让调用者去处理。如果说非要throw异常,可以将异常类包装为自定义异常类,用于控制输出更多有效的异常信息

public void wrapException(String input) throws MyBusinessException {
    try {
        // do something
    } catch (NumberFormatException e) {
        throw new MyBusinessException("A message that describes the error.", e);
    }
}

9)不要在finally块中使用return

​ try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点

10)不要用异常机制处理流程控制

​ 不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能

5.常见异常处理误区

案例1

 try {
      // 业务代码
      // …
      Thread.sleep(1000L);
    } catch (Exception e) {
      // Ignore it
    }

​ 分析上述代码的不合理之处:

  • 捕获异常范围太大:不建议直接捕获Exception异常,而是catch特定异常(更具体的异常)
  • 生吞异常(swallow):catch异常之后却不做任何处理(例如做额外处理或者抛出自定义异常用于获取更详细的异常信息)

案例2

try {
       // 业务代码
       // …
    } catch (IOException e) {
        e.printStackTrace();
    }

​ 分析上述代码的不合理之处:

​ 作为实验代码没有任何问题,但是在产品代码中不允许这样处理。主要问题在于printStackTrace();,可以查看printStackTrace()文档open in new window:Prints this throwable and its backtrace to the standard error stream”。问题就在这里,在稍微复杂一点的生产系统中,标准出错(STERR)不是个合适的输出选项,因为很难判断出到底输出到哪里去了。尤其是分布式系统,如果发生异常,但是无法找到堆栈轨迹(stacktrace),这纯属是为诊断设置障碍。所以,最好使用产品日志,详细地输出到日志系统里

案例3:Throw early, catch late原则

public void readPreferences(String fileName){
    	 //...perform operations... 
    	InputStream in = new FileInputStream(fileName);
    	 //...read the preferences file...
    }

​ 基于上述代码,如果fileName是null,就会抛出NullPointerException,对于这类异常其实可以通过一些预检查操作进行校验,让这个可能存在的空指针异常(NPE)更早的暴露出来,即”throw early“。在实际的业务场景中还可能是各种情况(获取配置失败等),在发现问题的时候第一时间抛出能够更加清晰地反应问题。

public void readPreferences(String filename) {
    	Objects. requireNonNull(filename);
    	//...perform other operations... 
    	InputStream in = new FileInputStream(filename);
    	 //...read the preferences file...
    }

​ 至于catch late,也就是捕获异常后需要怎么进行处理?最常见的误区就是生吞异常(只捕获而不处理,本质上是掩盖问题),如果实在不知道如何处理,可以保留原有异常的cause信息,直接抛出或者构建新的异常抛出去(例如抛出自定义异常)。在更高的层面上有相对清晰的业务逻辑,往往会更清楚合适的处理方式是什么。

​ 从性能角度审视一下Java的异常处理机制,这里有两个可能会相对昂贵的地方:

  • try-catch代码段会产生额外的性能开销,或者换个角度说,它往往会影响JVM对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的try包住整段的代码;且不建议利用异常控制代码流程,它远比通常意义上的条件语句(if/else、switch)要低效。
  • Java每实例化一个Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。

​ 所以,对于部分追求极致性能的底层类库,有种方式是尝试创建不进行栈快照的Exception。这本身也存在争议,因为这样做的假设在于,创建异常时知道未来是否需要堆栈。问题是,实际上可能吗?小范围或许可能,但是在大规模项目中,这么做可能不是个理智的选择。如果需要堆栈,但又没有收集这些信息,在复杂情况下,尤其是类似微服务这种分布式系统,这会大大增加诊断的难度。

​ 当服务出现反应变慢、吞吐量下降的时候,检查发生最频繁的Exception也是一种思路

深入理解异常

1.JAVA处理异常的机制

​ JVM处理异常的机制:Exception Table,结合简单案例去分析

public static void simpleTryCatch() {
   try {
       testNPE();
   } catch (Exception e) {
       e.printStackTrace();
   }
}

​ 借助javap拆解class文件(需要先使用javac编译),进一步分析代码

//javap -c Main
 public static void simpleTryCatch();
    Code:
       0: invokestatic  #3                  // Method testNPE:()V
       3: goto          11
       6: astore_0
       7: aload_0
       8: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
      11: return
    Exception table:
       from    to  target type
           0     3     6   Class java/lang/Exception

异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下

  • from 可能发生异常的起始点
  • to 可能发生异常的结束点
  • target 上述from和to之前发生异常后的异常处理者的位置
  • type 异常处理者处理的异常的类信息

异常表在什么时候会被用到?=》当一个异常发生的时候

  • JVM会在当前出现异常的方法中,查找异常表,是否有合适的处理者来处理
  • 如果当前方法异常表不为空,并且异常符合处理者的from和to节点,并且type也匹配,则JVM调用位于target的调用者来处理。
  • 如果上一条未找到合理的处理者,则继续查找异常表中的剩余条目
  • 如果当前方法的异常表无法处理,则向上查找(弹栈处理)刚刚调用该方法的调用处,并重复上面的操作
  • 如果所有的栈帧被弹出,仍然没有处理,则抛给当前的Thread,Thread则会终止
  • 如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行

2.异常耗时分析

​ 说用异常慢,首先来看看异常慢在哪里?有多慢?构建测试用例简单的测试了建立对象、建立异常对象、抛出并接住异常对象三者的耗时对比

public class ExceptionTimeConsumeDemo {

    private int testTimes;

    public ExceptionTimeConsumeDemo(int testTimes) {
        this.testTimes = testTimes;
    }

    public void newObject() {
        long l = System.nanoTime();
        for (int i = 0; i < testTimes; i++) {
            new Object();
        }
        System.out.println("建立对象:" + (System.nanoTime() - l));
    }

    public void newException() {
        long l = System.nanoTime();
        for (int i = 0; i < testTimes; i++) {
            new Exception();
        }
        System.out.println("建立异常对象:" + (System.nanoTime() - l));
    }

    public void catchException() {
        long l = System.nanoTime();
        for (int i = 0; i < testTimes; i++) {
            try {
                throw new Exception();
            } catch (Exception e) {
            }
        }
        System.out.println("建立、抛出并接住异常对象:" + (System.nanoTime() - l));
    }

    public static void main(String[] args) {
        ExceptionTimeConsumeDemo test = new ExceptionTimeConsumeDemo(10000);
        test.newObject();
        test.newException();
        test.catchException();
    }

}

// 测试响应结果
建立对象:247666
建立异常对象:4084541
建立、抛出并接住异常对象:5064500

​ 建立一个异常对象,是建立一个普通Object耗时的约20倍(实际上差距会比这个数字更大一些,因为循环也占用了时间,追求精确可以再测一下空循环的耗时然后在对比前减掉这部分),而抛出、接住一个异常对象,所花费时间大约是建立异常对象的4倍

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3