JavaSE-07-异常机制

JavaSE-07-异常机制

程序出现了异常该怎么办?在Java中,有一套异常处理机制,可以很好地解决程序出现的异常,这也是Java能够简单地编写出健壮程序的原因之一!正确处理异常很重要,本文将全面深入地阐述Java异常的相关知识点,提供丰富的案例,剖析原理,在实战中掌握异常知识点。

脑图

一、异常概述

异常,就是不正常的意思。在生活中:医生说你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响。java有一套异常机制,保证程序能正常运行。在程序中异常:

指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。

在Java等面向对象的编程语言中,异常本身是一个 ,产生异常就是创建异常对象 并抛出了一个异常对象。Java处理异常的方式是中断处理

异常指的并不是语法错误!语法错了,编译不通过,不会产生字节码文件(.class文件),根本不能运行。

二、异常体系

异常机制其实是帮助我们找到 程序中的问题,异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Errorjava.lang.Exception,平常所说的异常指java.lang.Exception

Throwable体系:

  • Error:严重错误Error,无法通过程序处理的错误,只能事先避免,好比绝症。
  • Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。

Throwable中的常用方法:

  • public void printStackTrace():打印异常的详细信息。包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段 ,都得使用printStackTrace类型+原因+位置

    public void printStackTrace () { printStackTrace(System.err); // 输出系统错误流的信息,栈调用链路的过程,顺藤摸瓜 }

  • public String getMessage():获取发生异常的原因。提示给用户的时候,就提示错误原因。

    /** * Returns the detail message string of this throwable. * * @return the detail message string of this { @code Throwable} instance * (which may be { @code null}). / public String getMessage () { return detailMessage; // 详细异常信息,一般交由子类实现定制 } /* * Specific details about the Throwable. For example, for * { @code FileNotFoundException}, this contains the name of * the file that could not be found. * * @serial */ private String detailMessage;

  • public String toString():获取异常的类型和异常描述信息(重写了Object的方法)。

    public String toString () { // 输出的是异常类名+本地化异常原因 String s = getClass().getName(); String message = getLocalizedMessage(); return (message != null ) ? (s + ": " + message) : s; }

Throwable类UML图:

异常分类

通俗讲: 异常的父类:Trowable;其手下两名大将:ErrorExceptionError,一般为系统级别出错,我们的代码层处理不了。像OOM异常,这个就是了,内存溢出。

Exception,细分为:RuntimeException和非RuntimeException,前者是非检查异常,后者为检查异常;意思是:检查约等于编译期,就是你敲代码时是否会标红,编译不给你过,除非你捕获处理或者往上抛异常;

非检查,意思是不用检查,直接过,但程序运行时,该发生的异常还是会发生,所以一般要自己写代码时及时规避掉,用if判断等等;为什么要分检查和非检查呢?......为了代码不出现大篇幅的try catch,影响可读性!

常见异常:

复制代码
ArithmeticException------由于除数为 0 引起的异常; ArrayStoreException------由于数组存储空间不够引起的异常; ClassCastException---一当把一个对象归为某个类,但实际上此对象并不是由这个类 创建的,也不是其子类创建的,则会引起异常; IllegalMonitorStateException------监控器状态出错引起的异常; NegativeArraySizeException---一数组长度是负数,则产生异常; NullPointerException---一程序试图访问一个空的数组中的元素或访问空的对象中的 方法或变量时产生异常; OutofMemoryException------用 new 语句创建对象时,如系统无法为其分配内存空 间则产生异常; SecurityException------由于访问了不应访问的指针,使安全性出问题而引起异常; IndexOutOfBoundsExcention------由于数组下标越界或字符串访问越界引起异常; IOException------由于文件未找到、未打开或者I/O操作不能进行而引起异常; ClassNotFoundException------未找到指定名字的类或接口引起异常; CloneNotSupportedException------一程序中的一个对象引用Object类的clone方法,但 此对象并没有连接Cloneable接口,从而引起异常; InterruptedException---一当一个线程处于等待状态时,另一个线程中断此线程,从 而引起异常,有关线程的内容 NoSuchMethodException一所调用的方法未找到,引起异常; Illega1AccessExcePtion---一试图访问一个非 public 方法; StringIndexOutOfBoundsException------访问字符串序号越界,引起异常; ArrayIdexOutOfBoundsException---一访问数组元素下标越界,引起异常; NumberFormatException------字符的UTF代码数据格式有错引起异常; IllegalThreadException---一线程调用某个方法而所处状态不适当,引起异常; FileNotFoundException------未找到指定文件引起异常; EOFException------未完成输入操作即遇文件结束引起异常。

运行与非运行异常

运行时异常: 都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

  • 运行时异常的特点:

是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。

  • 非运行时异常 (编译异常):

RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOExceptionSQLException等以及用户自定义的Exception异常。

总结:一般情况下不自定义检查异常,需要处理麻烦。一般定义运行时异常,也就是RuntimeException子类。

调用者(main->Jvm)

接口下,有各种实现类,实现类里面有重写的方法,这些方法,有些选择把异常抛出,有些选择自己try catch消化掉一般情况,如果是针对传进来的参数有异常,则选择抛出去,让调用者自己处理。因为如果是方法内部自己消化掉了,出现异常咱也不知道,除非看源码。

一般jvm --> main线程 -->对象方法1-->方法2.。。。。

这种调用链条,需要警示,让程序员知道的,就往上抛,调用者;不需要则自己消化掉。

三、分析printStrackTrace方法(调用链)

执行下面代码:

复制代码
public class Junk { public static void main (String args[]) { try { a(); } catch (HighLevelException e) { e. printStackTrace(); } } static void a () throws HighLevelException { try { b(); } catch (MidLevelException e) { throw new HighLevelException (e); } } static void b () throws MidLevelException { c(); } static void c () throws MidLevelException { try { d(); } catch (LowLevelException e) { throw new MidLevelException (e); } } static void d () throws LowLevelException { e(); } static void e () throws LowLevelException { throw new LowLevelException (); } } class HighLevelException extends Exception { HighLevelException(Throwable cause) { super (cause); } } class MidLevelException extends Exception { MidLevelException(Throwable cause)  { super (cause); } } class LowLevelException extends Exception { }

输出异常:

复制代码
ighLevelException: MidLevelException: LowLevelException at Junk. a(Junk. java: 13 ) at Junk. main(Junk. java: 4 ) Caused by: MidLevelException: LowLevelException at Junk. c(Junk. java: 23 ) at Junk. b(Junk. java: 17 ) at Junk. a(Junk. java: 11 ) ... 1 more Caused by: LowLevelException at Junk. e(Junk. java: 30 ) at Junk. d(Junk. java: 27 ) at Junk. c(Junk. java: 21 ) ... 3 more

上述方法的调用:main->a->b->c->d->e ,最终在e(Low)抛出低级别异常,在d时转为中级别(Mid)异常,在b时转为高级别(High)异常。总结习得:

1、使用这三个异常定义都是继承Exception,编译期需要检查

2、使用throw new xxException抛出异常对象

3、处理异常的方式:选择try-catch捕获,自己消化不再往上抛,或是包装异常,如:低转中,中转高,就是如此,重新往上throw new xx异常;二是,选择throws在方法上声明,让调用者处理该异常

怎么看异常信息?

1、最上面的是每一层方法调用产生的异常原因信息拼接,是个最近方法的主要异常原因

2、最下面的方法是根源,一般是出错的根本原因,比如:LowLevelException

3、要想解决问题,就得找到根本原因,看最下面的。

四、综合案例体会(初步体会)

复制代码
public static void main (String[] args) { try { int a = 2 ; System.out.println( 2 / 0 ); } catch (ArithmeticException e) { //异常类型如果写错了,本身就会发生异常。写Exception e.printStackTrace(); } try { String b = null ; System.out.println(b.length()); } catch (Exception e) { e.printStackTrace(); } try { int [] c = new int [ 5 ]; c[ 10 ] = 12 ; } catch (Exception e) { e.printStackTrace(); System.out.println( "我错了呀" ); } System.out.println( "好家伙,能运行到这来!" ); //这是error,OOM内存溢出 StringBuffer app = new StringBuffer (); for ( int i = 0 ; i < Integer.MAX_VALUE; i++) { app.append( "a" ); } System.out.println( "我看你运行不到这儿来!" ); }

输出

复制代码
java.lang.ArithmeticException: / by zero at com.water.base.exeception.ExceptionTest.main(ExceptionTest.java: 9 ) java.lang.NullPointerException: Cannot invoke "String.length()" because "b" is null at com.water.base.exeception.ExceptionTest.main(ExceptionTest.java: 15 ) java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5 at com.water.base.exeception.ExceptionTest.main(ExceptionTest.java: 21 ) 我错了呀 好家伙,能运行到这来! Process finished with exit code 130

分析:

1、main方法,使用三个try-catch,捕获异常,并且打印异常信息,可以非常清晰看到报错信息

2、报错信息:

  • / by zero告诉你分母不能为0
  • Cannot invoke "String.length()" because "b" is null告诉你空指针异常,变量b为null
  • Index 10 out of bounds for length 5告诉你数组下标越界,压根就没有索引10

3、捕获了之后,程序可以正常往下执行,打印了"好家伙,能运行到这来!"

4、在程序最后,遍历循环StringBuilder拼接,会撑爆内存,导致OOM内存溢出,JVM发生错误中断程序,所以最后的打印信息"我看你运行不到这儿来!"是打印不出来的。

5、程序也是非正常结束,Process finished with exit code 130

五、处理throw异常

Java异常机制,就靠这几个关键字进行处理。类似功能的快捷键,程序执行到throw就知道要抛出一个创建的异常。throws就知道把异常返回给调用者,try就知道里面的代码可能有问题,得小心对待,发生异常了,就去catch匹配对应的异常,然后执行里面的处理内容。最后再看看是否有finally代码块,有的话无论怎么样都得执行,除非Jvm挂了。

关键字如下:

  • throw,动词,直接手动抛出一个异常,throw new Excepiton()
  • throws,在方法上声明,表示方法里面的代码发生异常的话,往上抛,抛给方法的调用者去处理
  • try catchtry包裹着可能发生异常的代码块,发生了,则直接跳到catch语块执行,这里会直接捕获这个异常,并且"消化掉、吞掉",这里一般可以用来做些异常日志收集,log.error(),这些操作;不吞,则手动throw出去,让调用者处理罗
  • try catch finallyfinally语块无论是否发生异常都会执行,一般用于有io流操作时,用来关闭资源close()
  • 总的来说,异常最后都是需要处理的。

抛出异常throw(产生异常)

在编写程序时,我们必须要考虑程序出现问题的情况。比如,在定义方法时,方法需要接受参数。那么,当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的方式来告诉调用者。抛异常,其实是一种和调用者(用户)交互的方式,告诉你:这么做(入参)输入是不可以的。

在java中,使用**throw**关键字抛出一个指定的异常对象。

    1. 创建一个异常对象。输出提示信息,如:年龄不能小于0,手机号码不合法,xx不能为空......
    1. 需要将这个异常对象告知给调用者。怎么告知呢?怎么将这个异常对象传递到调用者处呢?通过关键字throw抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。

使用格式:

复制代码
throw new 异常类名(参数);

例如:

复制代码
throw new NullPointerException ( "要访问的arr数组不存在" ); throw new ArrayIndexOutOfBoundsException ( "该索引在数组中不存在,已超出范围" );

示例:

复制代码
public class ThrowDemo { public static void main (String[] args) { //创建一个数组 int [] arr = { 2 , 4 , 52 , 2 }; //根据索引找对应的元素 int index = 4 ; int element = getElement(arr, index); System.out.println(element); System.out.println( "over" ); } /* * 根据 索引找到数组中对应的元素 */ public static int getElement ( int [] arr, int index) { //判断  索引是否越界 if (index< 0 || index>arr.length- 1 ){ /* 判断条件如果满足,当执行完throw抛出异常对象后,方法已经无法继续运算。 这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。 */ throw new ArrayIndexOutOfBoundsException ( "下标越界了......" ); } int element = arr[index]; return element; } }

注意:如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。

那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续将问题声明出去,使用throws声明处理。

声明异常throws(解决方式一)

声明异常:将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理(稍后讲解该方式),那么必须通过throws进行声明,让调用者去处理。

关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).

声明异常格式:

复制代码
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2...{   }

示例:

复制代码
public class ThrowsDemo { public static void main (String[] args) throws FileNotFoundException { read( "a.txt" ); } // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明 public static void read (String path) throws FileNotFoundException { // 如果是运行期异常,则不需要声明 if (!path.equals( "hello.txt" )) { //如果不是 hello.txt这个文件 // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw throw new FileNotFoundException ( "文件不存在" ); // 不使用try-catch捕获处理 } } }

throws用于进行异常类的声明,注意,如果该异常为运行期异常(RuntimeException类及其子类则不需要声明)。另外,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。文件类的异常一般为编译期异常,如:FileNotFoundException, IOException

复制代码
public class ThrowsDemo2 { public static void main (String[] args) throws IOException { read( "a.txt" ); } public static void read (String path) throws FileNotFoundException, IOException { if (!path.equals( "a.txt" )) { //如果不是 a.txt这个文件 // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw throw new FileNotFoundException ( "文件不存在" ); } if (!path.equals( "b.txt" )) { throw new IOException (); } } }

捕获异常try-catch(解决方式二)

如果异常出现的话,会立刻终止程序,所以我们得处理异常:

    1. 该方法不处理,而是声明抛出,由该方法的调用者(就是上一个方法,b方法体内调用c,则b为c的调用者)来处理(throws)。
    1. 在方法中使用try-catch的语句块来处理异常,处理异常操作:
    1. 输出错误日志便于开发者调试排查问题,
    1. 返回布尔结果,保证调用者一定获取布尔结果, true or false,不中断程序
    1. 忽视异常,一般是批量插入,记录一下异常,哪些插入失败,不影响其他数据插入
    1. 重新包装异常,然后抛出(指的是使用throw new xx)新异常,该新异常如果是运行期异常,则不需要声明throws,因此编译期异常可以被转为运行期异常的操作就在这里。比如:文件找不到异常FileNotFoundException转为MyException(继承RuntimeException

try-catch的方式就是捕获异常。

捕获异常:Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理。

捕获异常语法如下:

复制代码
try { 编写可能会出现异常的代码 } catch (异常类型  e){ 处理异常的代码 //记录日志/打印异常信息/继续抛出异常 }

try :可能产生异常的代码。

catch:捕获异常并处理。

注意:try和catch都不能单独使用,必须连用。

示例:

复制代码
public class TryCatchDemo { public static void main (String[] args) { try { // 当产生异常时,必须有处理方式。要么捕获,要么声明。 read( "b.txt" ); } catch (FileNotFoundException e) { // 括号中需要定义什么呢? //try中抛出的是什么异常,在括号中就定义什么异常类型,如果捕获错了异常,意味着异常会中断程序。 System.out.println(e); } System.out.println( "over" ); } /* * * 我们 当前的这个方法中 有异常  有编译期异常 */ public static void read (String path) throws FileNotFoundException { if (!path.equals( "a.txt" )) { //如果不是 a.txt这个文件 // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw throw new FileNotFoundException ( "文件不存在" ); } } }

捕获正确的异常,是在不清楚,那就写父类Exception,保稳。

复制代码
public static void main (String[] args) { try { int a = 2 ; System.out.println( 2 / 0 ); // 应该是捕获ArithmeticException异常 } catch (ArrayIndexOutOfBoundsException e) { //异常类型如果写错了,本身就会发生异常。写Exception e.printStackTrace(); } System.out.println( "运行到这里来" ); } // 输出, Exception in thread "main" java.lang.ArithmeticException: / by zero at com.water.base.exeception.ExceptionTest.main(ExceptionTest.java: 8 ) Process finished with exit code 1

根本没捕获到异常,2/0发生的异常是ArithmeticException异常

调整:

复制代码
public static void main (String[] args) { try { int a = 2 ; System.out.println( 2 / 0 ); // 应该是捕获ArithmeticException异常 } catch (ArithmeticException e) { //异常类型如果写错了,本身就会发生异常。写Exception e.printStackTrace(); } System.out.println( "运行到这里来" ); } // 输出 java.lang.ArithmeticException: / by zero at com.water.base.exeception.ExceptionTest.main(ExceptionTest.java: 8 ) 运行到这里来 Process finished with exit code 0

正确捕获到异常ArithmeticException,所以程序正常往下运行,打印了"运行到这里来"这个语句。如果,不清楚该异常子类,可以使用父类异常捕获,一样是ok 的。

复制代码
public static void main (String[] args) { try { int a = 2 ; System.out.println( 2 / 0 ); // 应该是捕获ArithmeticException异常 } catch (Exception e) { //异常类型如果写错了,本身就会发生异常。写Exception e.printStackTrace(); } System.out.println( "运行到这里来" ); } // 输出 java.lang.ArithmeticException: / by zero at com.water.base.exeception.ExceptionTest.main(ExceptionTest.java: 8 ) 运行到这里来 Process finished with exit code 0

如何获取异常信息?

Throwable类中定义了一些查看方法:

  • public String getMessage():获取异常的描述信息,原因(提示给用户的时候,就提示错误原因。

  • public String toString():获取异常的类型和异常描述信息(不用)。

  • public void printStackTrace():打印异常的跟踪栈信息并输出到控制台。

    包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。

在开发中呢,也可以在catch将编译期异常转换成运行期异常处理

多个异常使用捕获又该如何处理呢?

    1. 多个异常分别处理。
    1. 多个异常一次捕获,多次处理。
    1. 多个异常一次捕获一次处理。

一般我们是使用一次捕获多次处理方式,格式如下:

复制代码
try { 编写可能会出现异常的代码 } catch (异常类型A  e){  当 try 中出现A类型异常,就用该 catch 来捕获. 处理异常的代码 //记录日志/打印异常信息/继续抛出异常 } catch (异常类型B  e){  当 try 中出现B类型异常,就用该 catch 来捕获. 处理异常的代码 //记录日志/打印异常信息/继续抛出异常 } // 注意!多个异常,子类在上面,父类在下面,从上往下匹配,所以精准具体在前,宽泛抽象在后

注意:这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。

六、最终执行finally代码块

finally :有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。

什么时候的代码必须最终执行?

当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。

finally的语法:try-catch-finally。自身需要处理异常,最终还得关闭资源。

注意:finally不能单独使用。

比如在IO流中,当打开了一个关联文件的资源,最后程序不管结果如何,都需要把这个资源关闭掉。当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。

永远执行

示例:

复制代码
public class TryCatchDemo { public static void main (String[] args) { try { read( "b.txt" ); } catch (FileNotFoundException e) { //抓取到的是编译期异常  抛出去的是运行期 throw new RuntimeException (e); } finally { System.out.println( "不管程序怎样,这里都将会被执行。" ); } System.out.println( "over" ); } /* * * 我们 当前的这个方法中 有异常  有编译期异常 */ public static void read (String path) throws FileNotFoundException { if (!path.equals( "a.txt" )) { //如果不是 a.txt这个文件 // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw throw new FileNotFoundException ( "文件不存在" ); } } }

输出:

复制代码
不管程序怎样,这里都将会被执行。 Exception in thread "main" java.lang.RuntimeException: java.io.FileNotFoundException: 文件不存在 at com.water.base.exeception.ExceptionTest.main(ExceptionTest.java: 12 ) Caused by: java.io.FileNotFoundException: 文件不存在 at com.water.base.exeception.ExceptionTest.read(ExceptionTest.java: 25 ) at com.water.base.exeception.ExceptionTest.main(ExceptionTest.java: 9 ) Process finished with exit code 1

分析:

1、捕捉到编译期异常FileNotFoundException,转变为运行期异常RuntimeException抛出

2、finally块,最终执行了

3、最后的打印"over"没有执行,因为异常结束了

Jvm退出

注意!如果由于Jvm退出了,则finally不会执行,例如执行: **System.exit(1)**

JvmJava程序执行的环境,容器,环境都没了,还怎么执行?

示例:

复制代码
try { System.out.println( "开始执行。。" ); System.exit( 1 ); // 程序执行到这,已经结束了,没有下面什么事了 read( "b.txt" ); } catch (FileNotFoundException e) { //抓取到的是编译期异常  抛出去的是运行期 throw new RuntimeException (e); } finally { System.out.println( "不管程序怎样,这里都将会被执行。" ); } System.out.println( "over" );

输出:

复制代码
开始执行。。 Process finished with exit code 1

再来看一个示例,为啥异常还是抛出,finally块还是会执行?

复制代码
try { System.out.println( "开始执行。。" ); read( "b.txt" ); System.exit( 1 ); // 程序执行到这,已经结束了,没有下面什么事了 } catch (FileNotFoundException e) { //抓取到的是编译期异常  抛出去的是运行期 throw new RuntimeException (e); } finally { System.out.println( "不管程序怎样,这里都将会被执行。" ); } System.out.println( "over" );

输出:

复制代码
开始执行。。 不管程序怎样,这里都将会被执行。 Exception in thread "main" java.lang.RuntimeException: java.io.FileNotFoundException: 文件不存在 at com.water.base.exeception.ExceptionTest.main(ExceptionTest.java: 14 ) Caused by: java.io.FileNotFoundException: 文件不存在 at com.water.base.exeception.ExceptionTest.read(ExceptionTest.java: 27 ) at com.water.base.exeception.ExceptionTest.main(ExceptionTest.java: 10 ) Process finished with exit code 1

分析:

1、代码执行到read("b.txt")该语句时,发生异常,try剩余的语句不会再执行

2、发生异常,直接匹配catch捕获到异常的代码块,执行里面的内容throw new RuntimeException(e);

3、检查有没有finally代码块,有则执行System.out.println("不管程序怎样,这里都将会被执行。");

4、然后程序因为异常中断,抛了异常给调用者mainmain然后抛给JvmJvm打印出异常日志。

return vs finally

return,表示程序运行到这就会返回!

finally,表示除非程序突然崩溃,否则我肯定也会执行的。System.exit(0),0表示程序正常退出,非0表示不正常退出,那如果,return遇上finally,怎么破呢?

结果是,都要执行!finally,也是保留字,即使你return了,但我finally该执行的,还是要执行!

调用有try catch finally 语块且有返回值的方法时,分析如下:

    1. 返回的是基本数据类型,则finally最后是改变不了try中已经返回的值,相当于变量副本
    1. 返回的是引用数据类型,则finally可以改变其对象的内容
    1. 总结:值传递,值不变;引用类型,指向对象,存的是其地址值,十六进制
    1. return总是就近返回的,并且是最后一个return

    package com.water.exception; public class Demo3 { public static void main (String[] args) { int a = getNum(); System.out.println( "result:" +a); //结果为10 Animal b = getAnimal(); System.out.println(b.toString()); //结果,年龄被改成20了 } /* * 1、基本数据类型,try有return,直接返回 * 2、finally会执行,如果这里有return,则返回就近的 * 3、finally之外有return,等同try里面的return,若try没有return,则就近返回 * */ public static int getNum () { int a = 10 ; try { return a; } catch (Exception e){ } finally { a = 12 ; // return a; //返回12 } return a; } /值传递,地址不变,但对象里面的内容可以被finally改变/ public static Animal getAnimal () { Animal animal = new Animal (); animal.name = "bob" ; animal.age = 15 ; try { return animal; } catch (Exception e ){ } finally { animal.age = 20 ; } return animal; } } class Animal { public String name; public Integer age; public String toString () { return "{name:" + this .name+ ";age:" + this .age+ "}" ; } }

异常注意事项

  • 运行时异常被抛出可以不处理。即不捕获也不声明抛出。
  • 如果父类抛出了多个异常,子类覆盖父类方法时,只能抛出相同的异常或者是他的子集。
  • 父类方法没有抛出异常,子类覆盖父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出
  • 当多异常处理时,捕获处理,前边的类不能是后边类的父类
  • 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收。

七、自定义异常

为什么需要自定义异常类:

我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是SUN没有定义好的,此时我们根据自己业务的异常情况来定义异常类。,例如年龄负数问题,考试成绩负数问题。

在上述代码中,发现这些异常都是JDK内部定义好的,但是实际开发中也会出现很多异常,这些异常很可能在JDK中没有定义过,例如年龄负数问题,考试成绩负数问题.那么能不能自己定义异常呢?

什么是自定义异常类:

在开发中根据自己业务的异常情况来定义异常类.

自定义一个业务逻辑异常: LoginException。一个登陆异常类。

异常类如何定义:

    1. 自定义一个编译期异常: 自定义类 并继承于java.lang.Exception
    1. 自定义一个运行时期的异常类:自定义类 并继承于java.lang.RuntimeException。(常用)

自定义编译期异常

要求:我们模拟登陆操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。

首先定义一个登陆异常类LoginException:

复制代码
// 业务逻辑异常 public class LoginException extends Exception { // 编译期异常 /** * 空参构造 */ public LoginException () { } /** * * @param message 表示异常提示 */ public LoginException (String message) { super (message); } }

模拟登陆操作,使用数组模拟数据库中存储的数据,并提供当前注册账号是否存在方法用于判断。

复制代码
public class Demo { // 模拟数据库中已存在账号 private static String[] names = { "bill" , "hill" , "jill" }; public static void main (String[] args) { //调用方法 try { // 可能出现异常的代码 checkUsername( "nill" ); System.out.println( "注册成功" ); //如果没有异常就是注册成功 } catch (LoginException e) { //处理异常 e.printStackTrace(); } } //判断当前注册账号是否存在 //因为是编译期异常,又想调用者去处理 所以声明该异常 public static boolean checkUsername (String uname) throws LoginException { for (String name : names) { if (name.equals(uname)){ //如果名字在这里面 就抛出登陆异常 throw new LoginException ( "亲" +name+ "已经被注册了!" ); } } return true ; } }

综合自定义异常案例

针对人的年龄、手机号分别设置:运行期异常、编译期异常的校验,观察其书写格式。

定义运行期异常-年龄异常:

复制代码
package com.water.base.exeception; public class AgeException extends RuntimeException { public AgeException () { super (); } public AgeException (String message) { super (message); } }

编译期异常-手机号异常:

复制代码
package com.water.base.exeception; public class PhoneException extends Exception { public PhoneException () { super (); } public PhoneException (String message) { super (message); } }

使用异常:

复制代码
package com.water.base.exeception; public class TestException { public static void main (String[] args) { Person person = new Person (); person.setAge(- 10 ); try { person.setPhone( "98" ); } catch (PhoneException e) { throw new RuntimeException (e); } System.out.println(person); } } class Person { private Integer age; private String phone; public Integer getAge () { return age; } public void setAge (Integer age) { if (age == null || age <= 0 || age > 120 ) { throw new AgeException ( "年龄不能为空,其要大于0,小于120" ); } this .age = age; } public String getPhone () { return phone; } public void setPhone (String phone) throws PhoneException { if (phone == null || phone.length() != 11 ) { throw new PhoneException ( "手机号不能为空,且要11位数" ); } this .phone = phone; } @Override public String toString () { return "Person{" + "age=" + age + ", phone='" + phone + '\'' + '}' ; } }

输出:

复制代码
Exception in thread "main" com.water.base.exeception.AgeException: 年龄不能为空,其要大于 0 ,小于 120 at com.water.base.exeception.Person.setAge(TestException.java: 32 ) at com.water.base.exeception.TestException.main(TestException.java: 8 ) Process finished with exit code 1

分析:

    1. AgeException为运行期异常在setAge时检查年龄抛出,但不用处理,因为运行时会选择抛出给调用者,最终由Jvm打印异常信息,其实也可以在setAge方法上throws,但没必要,因为说明了是运行期异常

    public void setAge (Integer age) throws AgeException{ // throws AgeException可有可无 if (age == null || age <= 0 || age > 120 ) { throw new AgeException ( "年龄不能为空,其要大于0,小于120" ); } this .age = age; }

    1. 为啥只是输出了年龄异常?手机号异常呢?这是因为执行到年龄setAge时已经发生异常,程序不再往下执行!所以压根也不会触发手机异常。
    1. PhoneException异常为编译期异常,在setPhone时校验手机号抛出异常,那么必须处理,否则编译不通过(冒红,生产不了.class文件),处理方式:try-catch套餐一,throws套餐二。这里是选择套餐二。注意!如果选择套餐一,try-catch处理异常,不要吞掉异常,处理了之后要不重新包装个异常,要不就重新抛出来,因为不抛出,调用者感知不了,以为这样输入手机号是ok的!切记,要重新抛出。如下:选择套餐一,错误处理,没有重新抛出异常,相当于吞掉了异常。

    public void setPhone (String phone) { try { if (phone == null || phone.length() != 11 ) { throw new PhoneException ( "手机号不能为空,且要11位数" ); } } catch (PhoneException e) { e.printStackTrace(); // 仅仅打印异常信息,没有把异常重新抛出去,调用者难以感知 } this .phone = phone; }

正确:

复制代码
public void setPhone (String phone) { try { if (phone == null || phone.length() != 11 ) { throw new PhoneException ( "手机号不能为空,且要11位数" ); } } catch (PhoneException e) { e.printStackTrace(); throw new RuntimeException (e.getMessage()); } this .phone = phone; }

像上面这样重新包装了一下,同时可以将编译期异常,转为运行期异常,是正确处理方式。但是一般如果是成员方法处理异常,一般都是选择套餐二:throws将异常抛给调用者,往上抛,更容易理解。main方法中手机号异常选择不处理,继续往上抛给调用者,而这个终极调用者就是JVM,也是可以的。

复制代码
public static void main (String[] args) throws PhoneException { // 手机号异常往上抛 Person person = new Person (); person.setAge(- 10 ); person.setPhone( "98" ); System.out.println(person); }

结论:

    1. 一般自定义运行期异常,可以不用满屏都是try-catch,然后throws也可以省掉,就会比较清爽
    1. 如果要定义编译期异常,其成员方法内一般选择不处理,抛给调用者
    1. 遵循【谁调用,谁处理】的原则
    1. 虽然JVM为其兜底,但是在业务层那一层处理异常,还是需要合理划分的,一般而言,异常都是从底层往上传递,在距离用户最近的那一层进行处理
    1. 使用try-catch处理异常时,除了打印错误日常等操作,不要随随便便吞掉异常!要重新抛出异常给调用者,否则这个异常处理得没那么好,特别是别人调用你开发的类库、方法时,你冷不丁把该抛出的异常吞掉了,怎么让调用者排查问题呢?

八、总结

正确使用异常机制处理异常是写出健壮代码关键的一步。异常不要随便吞掉,记得合理处理,该抛出的抛出。另外finally模块针对一些必须要执行的代码,如:关闭资源连接,一定要安排到位,当然JDK7以后可以使用try-with-resource直接声明让jvm关闭资源了,更优雅,也不用写满屏的try-catch-finally

总得来说,异常中我们使用最多的是运行期异常RuntimeException,自定义的异常也通常是这个,减少显式声明throws,或try-catch,不是不处理哦,最终JVM还是会处理的,但真到了那一步,程序也就是中断停止了,所以一般是离调用者最近的地方统一处理。如:使用spring框架,一般会设置全局异常处理,也就是service层的异常直接往上抛就好了,在controller层有一个全局异常处理器,会统一处理所有捕获的异常,具体到spring篇章再详聊。

当然,编译期不是说没用,其实一般在一些第三方sdk ,在设计接口时,也会选择throws声明一些异常,而这些自定义的异常,一般是编译期异常,因为运行期异常是可以不用显式展现的,这是为了告诉实现类,调用者需要去解决这个异常。另外一些IO流操作,涉及的异常基本也是编译期受检异常。Java异常简单而强大,多看多思多写,才能掌握异常知识点。

假如将异常类比生活:应对生活的意外发生,你有什么备案呢?也就是:一旦发生xx意外(异常),你的应对措施是什么? (即使不捕获也不行,麻烦来了),所以平时可以提前做好相关意外发生的措施,如:水灾、地震、火灾等等自然灾害的处理方法;如:发烧、感冒、手机丢了等等这些身边事宜的解决方案。预防永远是解决问题最有效的手段之一。

编程源于生活,但比生活严谨,1就是1,2就是2,及时给你反馈。生活没有彩排,永远都在向前,意外来临不用慌,平时有所准备,心态平稳,关键时刻也能像程序一样冷静。

相关推荐
OxyTheCrack1 小时前
【Golang】简述make与new内置函数以及两者的区别
开发语言·golang
适应规律1 小时前
【无标题】
人工智能·python·算法
Rain5091 小时前
mini-cc 的 MCP 协议:给 AI 装个 USB-C 接口
c语言·开发语言·前端·人工智能·架构·node.js·ai编程
XLYcmy1 小时前
全链路验证测试系统:一个针对智能代理(Agent)系统全链路能力的自动化验证脚本
分布式·python·http·网络安全·ai·llm·agent
有味道的男人2 小时前
电商效率翻倍:京东全量商品信息抓取
python
华科大胡子2 小时前
AI开发者的网络卡点:Anthropic连接超时
开发语言·php
_Aaron___2 小时前
Spring AI 接入 MCP:工具调用不是“能调就行”,关键是边界治理
java·人工智能·spring
原来是猿2 小时前
博客系统自动化测试实战总结
python
我是一颗柠檬2 小时前
【MySQL全面教学】MySQL性能优化实战Day13(2026年)
数据库·后端·sql·mysql·性能优化·database