Exceptions and Handling in Java

Introduction

When the program is running, an unexpected event occurs that prevents the program from executing normally as the programmer intended, which is called an exception. When an exception occurs, is the program left to fend for itself, exit and terminate immediately, or output an error to the user? Or in C style: use function return value as execution state? .

Java provides a better solution: exception handling mechanism.

The exception handling mechanism enables the program to deal with the exception in a targeted manner according to the pre-set exception handling logic of the code when an exception occurs, so that the program can return to normal and continue to execute as much as possible, and keep the code clear.
Exceptions in Java can be raised when a statement in a function is executed, or manually thrown by a programmer through a throw statement. As long as an exception occurs in a Java program, an exception object of the corresponding type will be used to encapsulate the exception. The JRE will then try to find an exception handler to handle the exception.

The Throwable class is the top-level parent class of Java exception types. An object is an exception object only if it is a (direct or indirect) instance of the Throwable class, and can be recognized by the exception handling mechanism. There are some commonly used exception classes built in JDK, and we can also customize exceptions.

Classification and class structure diagram of Java exceptions

There are some common exceptions built into Java standard pants, and these classes have Throwable as the top-level parent class.

Throwable in turn derives the Error class and the Exception class.

Errors: Instances of the Error class and its subclasses represent errors in the JVM itself. Errors cannot be handled by programmers through code, and Errors rarely occur. Therefore, programmers should pay attention to various exception classes under the branch where Exception is the parent class.

Exception: Exception and its subclasses represent various unexpected events sent when the program is running. It can be used by the Java exception handling mechanism and is the core of exception handling.

In general, we divide exception classes into two categories according to Javac's handling requirements for exceptions.

Unchecked exceptions (unckecked exceptions): Error and RuntimeException and their subclasses. When javac compiles, it will not prompt and find such exceptions, and it is not required to handle these exceptions in the program. So we can write code to handle (using try...catch...finally) exceptions like this if we want, or not. For these exceptions, we should fix the code instead of going through the exception handler. The reason for such an exception is mostly a problem with the code writing. Such as division by 0 error ArithmeticException, wrong cast error ClassCastException, array index out of bounds ArrayIndexOutOfBoundsException, using a null object NullPointerException and so on.

Checked exception (checked exception): In addition to Error and RuntimeException other exceptions. javac forces the programmer to prepare for such exceptions (using try...catch...finally or throws). Either catch it with a try-catch statement in the method and handle it, or declare it with a throws clause, otherwise the compilation will not pass. Such exceptions are generally caused by the operating environment of the program. Because the program may be run in various unknown environments, and the programmer cannot interfere with how the user uses the program he wrote, the programmer should be prepared for such an abnormal moment. Such as SQLException, IOException, ClassNotFoundException and so on.

To be clear: checked and unchecked are for javac, so it is easy to understand and distinguish.

first-time exception

The following code will demonstrate 2 exception types: ArithmeticException and InputMismatchException. The former is caused by integer division by 0, and the latter is caused by the input data cannot be converted to int type.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

package com.example;

import java. util .Scanner ;

public class AllDemo

{

      public static void main (String [] args )

      {

            System . out. println( "----欢迎使用命令行除法计算器----" ) ;

            CMDCalculate ();

      }

      public static void CMDCalculate ()

      {

            Scanner scan = new Scanner ( System. in );

            int num1 = scan .nextInt () ;

            int num2 = scan .nextInt () ;

            int result = devide (num1 , num2 ) ;

            System . out. println( "result:" + result) ;

            scan .close () ;

      }

      public static int devide (int num1, int num2 ){

            return num1 / num2 ;

      }

}

/*****************************************

 

----欢迎使用命令行除法计算器----

0

Exception in thread "main" java.lang.ArithmeticException : / by zero

     at com.example.AllDemo.devide( AllDemo.java:30 )

     at com.example.AllDemo.CMDCalculate( AllDemo.java:22 )

     at com.example.AllDemo.main( AllDemo.java:12 )

 

----欢迎使用命令行除法计算器----

r

Exception in thread "main" java.util.InputMismatchException

     at java.util.Scanner.throwFor( Scanner.java:864 )

     at java.util.Scanner.next( Scanner.java:1485 )

     at java.util.Scanner.nextInt( Scanner.java:2117 )

     at java.util.Scanner.nextInt( Scanner.java:2076 )

     at com.example.AllDemo.CMDCalculate( AllDemo.java:20 )

     at com.example.AllDemo.main( AllDemo.java:12 )

*****************************************/

异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因为,只要一个函数发生了异常,那么他的所有的caller都会被异常影响。当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈。

异常最先发生的地方,叫做异常抛出点。

从上面的例子可以看出,当devide函数发生除0异常时,devide函数将抛出ArithmeticException异常,因此调用他的CMDCalculate函数也无法正常完成,因此也发送异常,而CMDCalculate的caller——main 因为CMDCalculate抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的caller中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由main函数抛给JRE,导致程序终止。

上面的代码不使用异常处理机制,也可以顺利编译,因为2个异常都是非检查异常。但是下面的例子就必须使用异常处理机制,因为异常是检查异常。

代码中我选择使用throws声明异常,让函数的调用者去处理可能发生的异常。但是为什么只throws了IOException呢?因为FileNotFoundException是IOException的子类,在处理范围内。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

@Test

public void testException() throws IOException

{

    //FileInputStream的构造函数会抛出FileNotFoundException

    FileInputStream fileIn = new FileInputStream("E:\\a.txt");

     

    int word;

    //read方法会抛出IOException

    while((word =  fileIn.read())!=-1)

    {

        System.out.print((char)word);

    }

    //close方法会抛出IOException

    fileIn.clos

}

异常处理的基本语法

在编写代码处理异常时,对于检查异常,有2种不同的处理方式:使用try…catch…finally语句块处理它。或者,在函数签名中使用throws 声明交给函数调用者caller去解决。

try…catch…finally语句块

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

try{

     //try块中放可能发生异常的代码。

     //如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。

     //如果发生异常,则尝试去匹配catch块。

 

}catch(SQLException SQLexception){

    //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。

    //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。

    //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。

    //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。

    //如果try中没有发生异常,则所有的catch块将被忽略。

 

}catch(Exception exception){

    //...

}finally{

    

    //finally块通常是可选的。

   //无论异常是否发生,异常是否匹配被处理,finally都会执行。

   //一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。

  //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。

}

需要注意的地方

1、try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。

2、每一个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个catch块都有存在的意义。

3、java中,异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理catch代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的catch代码块”后面接着执行。
有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 )
而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)

1

2

3

4

5

6

7

8

9

10

11

public static void main(String[] args){

        try {

            foo();

        }catch(ArithmeticException ae) {

            System.out.println("处理异常");

        }

}

public static void foo(){

        int a = 5/0//异常抛出点

        System.out.println("为什么还不给我涨工资!!!");  //////////////////////不会执行

}

throws 函数声明

throws声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过。

throws是另一种处理异常的方式,它不同于try…catch…finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。

采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。

1

2

3

4

public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN

{

     //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。

}

finally块

finally块不管异常是否发生,只要对应的try执行了,则它一定也执行。只有一种方法让finally块不执行:System.exit()。因此finally块通常用来做资源释放操作:关闭文件,关闭数据库连接等等。

良好的编程习惯是:在try块中打开资源,在finally块中清理释放这些资源。

需要注意的地方:

1、finally块没有处理异常的能力。处理异常的只能是catch块。

2、在同一try…catch…finally块中 ,如果try中抛出异常,且有匹配的catch块,则先执行catch块,再执行finally块。如果没有catch块匹配,则先执行finally,然后去外面的调用者中寻找合适的catch块。

3、在同一try…catch…finally块中 ,try发生异常,且匹配的catch块中处理异常时也抛出异常,那么后面的finally也会执行:首先执行finally块,然后去外围调用者中寻找合适的catch块。

这是正常的情况,但是也有特例。关于finally有很多恶心,偏、怪、难的问题,我在本文最后统一介绍了,电梯速达->:finally块和return

throw 异常抛出语句

throw exceptionObject

程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面必须是一个异常对象。

throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别。

1

2

3

4

5

6

7

public void save(User user)

{

      if(user  == null)

          throw new IllegalArgumentException("User对象为空");

      //......

         

}

异常的链化

在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B也将不能完成而发生异常,但是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。

异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。

查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此形成链也是自然的了。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class Throwable implements Serializable {

    private Throwable cause = this;

    

    public Throwable(String message, Throwable cause) {

        fillInStackTrace();

        detailMessage = message;

        this.cause = cause;

    }

     public Throwable(Throwable cause) {

        fillInStackTrace();

        detailMessage = (cause==null ? null : cause.toString());

        this.cause = cause;

    }

     

    //........

}

下面是一个例子,演示了异常的链化:从命令行输入2个int,将他们相加,输出。输入的数不是int,则导致getInputNumbers异常,从而导致add函数异常,则可以在add函数中抛出

一个链化的异常。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

public static void main(String[] args)

{

     

    System.out.println("请输入2个加数");

    int result;

    try

    {

        result = add();

        System.out.println("结果:"+result);

    } catch (Exception e){

        e.printStackTrace();

    }

}

//获取输入的2个整数返回

private static List<Integer> getInputNumbers()

{

    List<Integer> nums = new ArrayList<>();

    Scanner scan = new Scanner(System.in);

    try {

        int num1 = scan.nextInt();

        int num2 = scan.nextInt();

        nums.add(new Integer(num1));

        nums.add(new Integer(num2));

    }catch(InputMismatchException immExp){

        throw immExp;

    }finally {

        scan.close();

    }

    return nums;

}

 

//执行加法计算

private static int add() throws Exception

{

    int result;

    try {

        List<Integer> nums =getInputNumbers();

        result = nums.get(0)  + nums.get(1);

    }catch(InputMismatchException immExp){

        throw new Exception("计算失败",immExp);  /////////////////////////////链化:以一个异常对象为参数构造新的异常对象。

    }

    return  result;

}

 

/*

请输入2个加数

r 1

java.lang.Exception: 计算失败

    at practise.ExceptionTest.add(ExceptionTest.java:53)

    at practise.ExceptionTest.main(ExceptionTest.java:18)

Caused by: java.util.InputMismatchException

    at java.util.Scanner.throwFor(Scanner.java:864)

    at java.util.Scanner.next(Scanner.java:1485)

    at java.util.Scanner.nextInt(Scanner.java:2117)

    at java.util.Scanner.nextInt(Scanner.java:2076)

    at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)

    at practise.ExceptionTest.add(ExceptionTest.java:48)

    ... 1 more

 

*/

自定义异常

如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自RuntimeException。

按照国际惯例,自定义的异常应该总是包含如下的构造函数:

  • 一个无参构造函数
  • 一个带有String参数的构造函数,并传递给父类的构造函数。
  • 一个带有String参数和Throwable参数,并都传递给父类构造函数
  • 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。

下面是IOException类的完整源代码,可以借鉴。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public class IOException extends Exception

{

    static final long serialVersionUID = 7818375828146090155L;

 

    public IOException()

    {

        super();

    }

 

    public IOException(String message)

    {

        super(message);

    }

 

    public IOException(String message, Throwable cause)

    {

        super(message, cause);

    }

 

     

    public IOException(Throwable cause)

    {

        super(cause);

    }

}

异常的注意事项

1、当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。这是为了支持多态。

例如,父类方法throws 的是2个异常,子类就不能throws 3个及以上的异常。父类throws IOException,子类就必须throws IOException或者IOException的子类。

至于为什么?我想,也许下面的例子可以说明。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

class Father

{

    public void start() throws IOException

    {

        throw new IOException();

    }

}

 

class Son extends Father

{

    public void start() throws Exception

    {

        throw new SQLException();

    }

}

/**********************假设上面的代码是允许的(实质是错误的)***********************/

class Test

{

    public static void main(String[] args)

    {

        Father[] objs = new Father[2];

        objs[0] = new Father();

        objs[1] = new Son();

 

        for(Father obj:objs)

        {

        //因为Son类抛出的实质是SQLException,而IOException无法处理它。

        //那么这里的try。。catch就不能处理Son中的异常。

        //多态就不能实现了。

            try {

                 obj.start();

            }catch(IOException)

            {

                 //处理IOException

            }

         }

   }

}

2、Java程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常 会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。

也就是说,Java中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。

finally块和return

首先一个不容易理解的事实:在 try块中即便有return,break,continue等改变执行流的语句,finally也会执行。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public static void main(String[] args)

{

    int re = bar();

    System.out.println(re);

}

private static int bar()

{

    try{

        return 5;

    } finally{

        System.out.println("finally");

    }

}

/*输出:

finally

*/

很多人面对这个问题时,总是在归纳执行的顺序和规律,不过我觉得还是很难理解。我自己总结了一个方法。用如下GIF图说明。

也就是说:try…catch…finally中的return 只要能执行,就都执行了,他们共同向同一个内存地址(假设地址是0×80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。

finally中的return 会覆盖 try 或者catch中的返回值。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

public static void main(String[] args)

    {

        int result;

         

        result  =  foo();

        System.out.println(result);     /////////2

         

        result = bar();

        System.out.println(result);    /////////2

    }

 

    @SuppressWarnings("finally")

    public static int foo()

    {

        trz{

            int a = 5 / 0;

        } catch (Exception e){

            return 1;

        } finally{

            return 2;

        }

 

    }

 

    @SuppressWarnings("finally")

    public static int bar()

    {

        try {

            return 1;

        }finally {

            return 2;

        }

    }

finally中的return会抑制(消灭)前面try或者catch块中的异常

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

class TestException

{

    public static void main(String[] args)

    {

        int result;

        try{

            result = foo();

            System.out.println(result);           //输出100

        } catch (Exception e){

            System.out.println(e.getMessage());    //没有捕获到异常

        }

         

         

        try{

            result  = bar();

            System.out.println(result);           //输出100

        } catch (Exception e){

            System.out.println(e.getMessage());    //没有捕获到异常

        }

    }

     

    //catch中的异常被抑制

    @SuppressWarnings("finally")

    public static int foo() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }catch(ArithmeticException amExp) {

            throw new Exception("我将被忽略,因为下面的finally中使用了return");

        }finally {

            return 100;

        }

    }

     

    //try中的异常被抑制

    @SuppressWarnings("finally")

    public static int bar() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }finally {

            return 100;

        }

    }

}

finally中的异常会覆盖(消灭)前面try或者catch中的异常

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

class TestException

{

    public static void main(String[] args)

    {

        int result;

        try{

            result = foo();

        } catch (Exception e){

            System.out.println(e.getMessage());    //输出:我是finaly中的Exception

        }

         

         

        try{

            result  = bar();

        } catch (Exception e){

            System.out.println(e.getMessage());    //输出:我是finaly中的Exception

        }

    }

     

    //catch中的异常被抑制

    @SuppressWarnings("finally")

    public static int foo() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }catch(ArithmeticException amExp) {

            throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常");

        }finally {

            throw new Exception("我是finaly中的Exception");

        }

    }

     

    //try中的异常被抑制

    @SuppressWarnings("finally")

    public static int bar() throws Exception

    {

        try {

            int a = 5/0;

            return 1;

        }finally {

            throw new Exception("我是finaly中的Exception");

        }

         

    }

}

上面的3个例子都异于常人的编码思维,因此我建议:

  • 不要在fianlly中使用return。
  • 不要在finally中抛出异常。
  • 减轻finally的任务,不要在finally中做一些其它的事情,finally块仅仅用来释放资源是最合适的。
  • 将尽量将所有的return写在函数的最后面,而不是try … catch … finally中。

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325316085&siteId=291194637