java的异常处理机制

  • java的异常体系架构
  • java对异常的处理方式
  • catch中的异常声明出现子父类关系的处理
  • 调用异常对象的方法
  • try结构声明的变量不能在try结构外部引用
  • finally的使用
  • try-catch-finally内多重return语句的处理
  • finally关掉文件

java的异常体系架构

java的异常分为两种,一种为Error型异常,另一种异常为Eception 型的异常

  • Error型异常为JVM虚拟机无法解决的异常,一般为非常严重的异常,比如说StackOverflowError和OOM异常,这些异常一般不能通过修改代码来处理。Error型异常结尾都是以Error结尾。
  • Exception型的异常还分为两类,一类为编译异常(checked),另一类为运行异常(unchecked,RuntimeException),Exception型异常都是以Exception结尾。
  • 编译时异常就是编译不通过,这时候一般要通过try-catch-finally的代码块来处理异常,使其编译通过,编译通过后仍可能出现运行时异常
  • 运行时异常就是编译器通过,但运行时出现的异常,这类异常一般不通过try-catch-finally语句块处理,往往只能改代码来解决异常,如果用到了try-catch-finally语句块,仅仅只能起到错误信息提示的作用,要想真正修改异常,只能修改代码。

各类异常的一个结构体系如下图

java 流式 编程 java流式编程异常处理_Test


图片选自尚硅谷笔记

java对异常的处理方式

java虚拟机对异常的处理方式有两种,一种方法是用try-catch-finally语句块来处理异常,另一种是通过throws+异常类型 来处理异常。

  • try-catch-finally语句块是对系统抛出的异常进行捕获,当程序运行遇到异常的时候,系统会自动生成一个异常对象,该异常对象都继承自一个名为Throwable的父类。
  • 由Throwable父类会生成两个子类,分别为Error型异常和Exception型异常,这两个子类还可以作为父类,产生具体的异常子类,就是我们程序中常常遇到的异常,比如说运行时异常中的空指针异常NullPointerException和数组越界异常ArrayIndexOutOfBoundsException。
  • try-catch-finally语句块可以有多个catch语句块,来捕捉try语句块中可能出现的多个异常,finally语句块可以省略,finally语句块内放置的代码一定会被执行。try语句块中放置可能会抛出异常的代码。
  • catch语句块中用来捕捉异常,然后进行异常处理操作,异常处理操作也有可能会产生异常,因此在catch和finally语句块中可以嵌套try-catch-finally语句块。
  • 在try语句块内若第n行代码抛出异常了,则该语句块内的自n行代码以后的代码都不会被执行,系统会调到相应的catch语句块内对异常进行处理。处理结束后执行finally语句块内的代码,如果没有finally语句块,则直接执行try-catch-finally语句块后面的代码。

先来一段简单的演示代码

package com.atguigu.java;

import org.junit.Test;

public class ExceptionTest1 {

	@Test
	public void test1(){
		String str = "123";
		str = "abc";
		try {
			int num = Integer.parseInt(str);
			System.out.println("hello---1");
		} catch (NumberFormatException e) {
			System.out.println("出现了数制转换异常");
		}
		System.out.println("hello---2");
	}
}

运行结果为

出现了数制转换异常
hello—2

在JUnit内,运行结果也为绿条,说明运行正常。
可见,在try语句块内,出现异常语句后面的语句都不会被执行了,转而调到catch语句块中执行相应的代码。

catch语句块的格式为catch(异常对象 对象名){异常处理代码}

catch中的异常声明出现子父类关系的处理

假如说多个catch里面的异常对象,如果出现了子父类关系,则必须把子类异常放到前面,父类异常放到后面。子类异常如果都继承自同一父类,则彼此之间先后关系可以任意放置。类似于if-else语句,把范围小的条件放在前面,把范围大的放在后面。同理,编译器一旦捕捉到正确的异常类型,就直接执行该catch语句块内的代码,而不去执行后面的catch结构了。如果把父类的异常对象放到了子类异常对象前面,则编译器会报错,因为子类异常都变成Unreachable了

import org.junit.Test;

public class ExceptionTest1 {

	@Test
	public void test1(){
		String str = "123";
		str = "abc";
		try {
			int num = Integer.parseInt(str);
			System.out.println("hello---1");
		} catch (NumberFormatException e) {
			System.out.println("出现了数制转换异常");
		} catch (NullPointerException e){
			System.out.println("出现了空指针异常");
		} catch (Exception e){
			System.out.println("出现了异常");
		}
		System.out.println("hello---2");
	}
}

分析以上代码,数值转换异常和空指针异常都是运行异常这一父类下的,彼此构成互斥的关系,谁先谁后无所谓,但是Exception异常是运行异常的父类了,不能够把Exception异常放到数制转换异常和空指针异常前面,否则编译器报错。

调用异常对象的方法

因为catch的格式为catch(异常对象 对象名){异常处理代码} 因此,异常对象的类肯定包含了方法的,可以在catch语句块内部通过变量名来调用java库中处理异常的方法。

  • 方法一:getMessage()方法,打印相应的异常信息,该方法有返回值,返回值类型为字符串类型,常放置于输出语句内
  • 方法二:printStackTrace()方法,该方法无返回值,直接调用。该方法内调用了getMessage()方法。
    示例代码如下
import org.junit.Test;

public class ExceptionTest1 {

	@Test
	public void test1(){
		String str = "123";
		str = "abc";
		try {
			int num = Integer.parseInt(str);
			System.out.println("hello---1");
		} catch (NumberFormatException e) {
			System.out.println(e.getMessage());
		} catch (NullPointerException e){
			System.out.println("出现了空指针异常");
		} catch (Exception e){
			System.out.println("出现了异常");
		}
		System.out.println("hello---2");
	}
}

运行结果为

For input string: “abc”
hello—2

import org.junit.Test;

public class ExceptionTest1 {

	@Test
	public void test1(){
		String str = "123";
		str = "abc";
		try {
			int num = Integer.parseInt(str);
			System.out.println("hello---1");
		} catch (NumberFormatException e) {
//			System.out.println(e.getMessage());
			e.printStackTrace();
		} catch (NullPointerException e){
			System.out.println("出现了空指针异常");
		} catch (Exception e){
			System.out.println("出现了异常");
		}
		System.out.println("hello---2");
	}
}

运行结果为

java.lang.NumberFormatException: For input string: “abc”
 at java.lang.NumberFormatException.forInputString(Unknown Source)
 at java.lang.Integer.parseInt(Unknown Source)
 at java.lang.Integer.parseInt(Unknown Source)
 at com.atguigu.java.ExceptionTest1.test1(ExceptionTest1.java:12)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Unknown Source)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
 at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
 hello—2

以上代码其实是正常运行了的,但是表现的形式却像是没有正常运行,这是因为printStackTrace()方法会打印出一系列信息,这些信息就包括了getMessage()方法返回值的内容。

因为在catch语句块中常常使用printStackTrace()方法,因此对于运行时异常这一类的异常,加不加try-catch语句块结果都看起来差不多,这一点后面会提现到。

try结构声明的变量不能在try结构外部引用

演示代码如下

@Test
	public void test1(){
		String str = "123";
		str = "abc";
		try {
			int num = Integer.parseInt(str);
			System.out.println("hello---1");
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} catch (NullPointerException e){
			System.out.println("出现了空指针异常");
		} catch (Exception e){
			System.out.println("出现了异常");
		}
//		System.out.println(num);
	}

注释掉的部分是编译器报错的代码,因为try结构里声明的变量是不能够被try结构以外使用的,要想改变这个现状,就只有把try结构里面的变量声明在try结构外侧,并且注意,一定要在外侧初始化。如果不初始化,编译器也会报错。

修改代码为:

@Test
	public void test1(){
		String str = "123";
		str = "abc";
		int num = 0;
		try {
			num = Integer.parseInt(str);
			System.out.println("hello---1");
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} catch (NullPointerException e){
			System.out.println("出现了空指针异常");
		} catch (Exception e){
			System.out.println("出现了异常");
		}
		System.out.println(num);
	}

这样即可正常运行了,之所以还要在外侧初始化,是因为如果不在外侧初始化,选在内侧初始化,try结构内的语句一旦抛出异常,num的赋值语句可能并不能被正常执行,到时候num就是没有被初始化过的值,也就不能被输出了。

finally的使用

  • finally是可选的,并不是一定要有finally结构
  • finally主要用在try语句块内有返回值的情况,或者catch语句块内有返回值的情况,使得函数在返回值之前,执行finally内的语句
  • finally也用于catch语句块内出现异常的情况,一般情况如果catch语句块内出现了异常,如果不嵌套使用try-catch来处理异常的话,程序会终止的,这时候在程序终止之前,执行finally结构内的语句。(这时候程序还是会终止的,finally结构内的语句并不能处理掉异常,只是会被执行而已,执行结束后程序就被终止了)

演示代码如下

@Test
	public void test1(){
		try {
			int a = 10;
			int b = 0;
			System.out.println(a/b);
		} catch(ArithmeticException e){
			e.printStackTrace();
		}catch (Exception e) {
			
			e.printStackTrace();
		}
		finally{
			System.out.println("我好帅");
		}
	}

执行结果如图

java 流式 编程 java流式编程异常处理_System_02


控制台内,可见程序是正常执行了的,尽管报了一堆红字,不过这只是e.printStackTrace()输出的结果而已,程序依然正常执行了,只不过看起来像是没正常执行也有。

在以上的演示代码中,因为try-catch结构块内没返回值,catch结构块内也不会抛出异常,因此,以上代码的finally结构是不必要的,其效果和下面的代码是一样的。

@Test
	public void test1(){
		try {
			int a = 10;
			int b = 0;
			System.out.println(a/b);
		} catch(ArithmeticException e){
			e.printStackTrace();
		}catch (Exception e) {
			
			e.printStackTrace();
		}
//		finally{
//			System.out.println("我好帅");
//		}
		System.out.println("我好帅");
	}

要想体现出finally的优越性,假如长catch结构内抛出异常了,如下

@Test
	public void test1(){
		try {
			int a = 10;
			int b = 0;
			System.out.println(a/b);
		} catch(ArithmeticException e){
			e.printStackTrace();
			int[] arr = new int[10];
			System.out.println(arr[10]);
		}catch (Exception e) {
			
			e.printStackTrace();
		}
		finally{
			System.out.println("我好帅");
		}
//		System.out.println("我好帅");
	}

此次运行结果和上一次的是一样的,但是注意JUnit内变为了红条,说明程序没有正常运行,程序没能正常处理数值越界异常,但是在finally的作用下,在程序被终止之前,仍然执行了finally内的语句,虽然看起来和上面的结果是一样的,但上面的代码是正常运行了的,而这次的代码其实是没有正常运行的。

finally的优越性还能体现在try结构或者catch结构内有返回值的情况

@Test
	public void testMethod(){
		int num = method();
		System.out.println(num);
	}
	
	public int method(){
		try {
			int[] arr = new int[10];
			System.out.println(arr[10]);
			return 1;
		} catch (ArrayIndexOutOfBoundsException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return 2;
		}finally{
			System.out.println("我一定会被执行");
		}
	}

输出结果为

java 流式 编程 java流式编程异常处理_System_03


分析代码,程序被正常执行了,返回值为2.

但是这里有个注意点,我们只在try结构内写上返回值return语句是不对的,因为try结构的return语句可能不会被执行,所以还要在catch结构内写上返回值。以上的代码就没有执行try结构内的返回值,因为try结构一旦抛出异常,就不会执行异常语句后面的代码。
如果仅仅只在try结构内写上return语句而不在catch结构内写上return语句,编译器会报错。

try-catch-finally内多重return语句的处理

注意一个细节,当try-catch结构内出现返回值的时候,即函数有返回值的时候,编译器只会检查try结构和catch结构内是否会有返回值。但是这样不够,因为catch结构内也可能抛出异常,因此有时候最好在finally结构内也加上一个return语句,防止没有返回值的情况出现。但是编译器不会对finally结构的return做检查,只检查try和catch结构内是否有return语句。换句话说,编译器处理这种情况是不能检查出运行异常的,只能检查出编译异常。

演示代码如下

@Test
	public void testMethod(){
		int num = method();
		System.out.println(num);
		int a = 10;
		int b = 2;
		System.out.println(a/b);
	}
	
	public int method(){
		try {
			int[] arr = new int[10];
			System.out.println(arr[10]);
			return 1;
		} catch (ArrayIndexOutOfBoundsException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			int a = 10;
			int b = 0;
			System.out.println(a/b);
			return 2;
		}finally{
			System.out.println("我一定会被执行");
			return 3;
		}
	}

运行结果为

java.lang.ArrayIndexOutOfBoundsException: 10
 at com.atguigu.java.FinallyTest.method(FinallyTest.java:19)
 at com.atguigu.java.FinallyTest.testMethod(FinallyTest.java:9)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Unknown Source)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
 at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
 我一定会被执行
 3
 5

这时候发现,JUnit内为绿条,testMethod()正常接收到了返回值。这儿虽然运行正常,但奇怪的是,程序并没有处理掉ArithmeticException异常。
如果不在finally加上return语句,JUnit为红条,程序不能正常运行。

假如说catch结构内没有抛出异常

@Test
	public void testMethod(){
		int num = method();
		System.out.println(num);
	}
	
	public int method(){
		try {
			int[] arr = new int[10];
			System.out.println(arr[10]);
			return 1;
		} catch (ArrayIndexOutOfBoundsException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
//			int a = 10;
//			int b = 0;
//			System.out.println(a/b);
			return 2;
		}finally{
			System.out.println("我一定会被执行");
			return 3;
		}
	}

返回结果为3,当执行到catch语句块内的return 2的时候,意味着要出try-catch结构了,这时候转到finally结构内执行语句,直接执行掉了return 3,出了try-catch结构,return 2就不会被执行了。

finally关掉文件

像数据库连接,IO流等资源,一旦被打开,JVM是不能够自动回收的,需要手动回收,一旦程序运行时抛出异常了,有时候就不能执行手动回收的语句了,因此要在finally内加上手动回收的语句。

import java.io.File;
import java.io.FileInputStream;

import org.junit.Test;

public class FinallyTest {
	
	@Test
	public void test2(){
		File file = new File("hello.txt");
		FileInputStream fis = new FileInputStream(file);
		
		int data = fis.read();
		while(data != -1){
			System.out.print((char)data);
			data = fis.read();
		}
		fis.close();
	}

假如说写了以上的代码,编译器会报错,错误类型为FileNotFoundException和IOException,很明显,这是一个编译时异常,我们可以用try-catch来处理异常。
ps:try-catch的快捷操作位,选自要包住的部分,右键,点击Surround With,选自try/catch block即可。

用try-catch语句块来处理异常

@Test
	public void test2(){
		File file = new File("hello.txt");
		try {
			FileInputStream fis = new FileInputStream(file);
			
			int data = fis.read();
			while(data != -1){
				System.out.print((char)data);
				data = fis.read();
			}
			fis.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

以上的处理方法虽然编译器不会报错了,但处理方法存在严重缺陷,因为fis.read()可能抛出异常,导致fis.close()不会被执行,这是不能被允许的,因此要对代码进行修改。

修改后的代码为

@Test
	public void test2(){
		File file = new File("hello.txt");
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(file);
			
			int data = fis.read();
			while(data != -1){
				System.out.print((char)data);
				data = fis.read();
			}
			
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			try {
				fis.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

此时便不会有编译时异常了,但有可能存在运行时异常。可见,try-catch处理编译时异常的时候,有可能只是把编译时异常延伸到了运行时异常而已。

分析代码,将fis.close();语句放到finally内,保证fis.close();语句一定被执行。但是此时编译器报错了,错误原因为try结构内定义的变量不能在try结构外引用,把fis定义在try结构外并且初始化即可,但此时还会报错,因为fis.close();本身就有可能出现IOException异常,因此要在finally结构内嵌套try-catch来处理IOException异常。

此时运行会出现运行异常,异常类型为空指针异常
可以加一句if判断语句来解决问题。

finally{
			try {
				if(fis != null)
				fis.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

运行结果就正常了,注意,这儿解决空指针异常使用if语句解决的,当然也可以选择用嵌套try-catch来处理,但是try-catch一般不处理运行时异常,因为try-catch处理运行异常,用到e.printStackTrace()报一堆红字,处理完后结果感觉也差不太多,运行异常一般只能改代码了。