目录
一.异常概述和体系
异常概述
Java异常是指在程序运行时可能发生的错误或异常情况,比如错误输入、文件不存在、网络连接中断等等。
Java提供了一套强大的异常处理机制,使得程序能够在发生异常时进行适当的处理,避免程序崩溃或者数据丢失。
Java程序在执行过程中所发生的异常事件可分为两类:
Error:Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowError(栈溢出)和OOM(内存溢出)。一般不编写针对性的代码进行处理。
Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如:
空指针访问
试图读取不存在的文件
网络连接中断
数组角标越界
Error代码示例:
public class Test {
public static void main(String[] args) {
/*
* 1、栈溢出:java.lang.StackOverflowError
* 原因 : 函数调用栈太深了,注意代码中是否有了循环调用方法而无法退出的情况
* StackOverflowError 是一个java中常出现的错误:在jvm运行时的数据区域中有一个java虚拟机栈,当执行java方法时会进行压栈弹栈的操作。在栈中会保存局部变量,操作数栈,方法出口等等。jvm规定了栈的最大深度,当执行时栈的深度大于了规定的深度,就会抛出StackOverflowError错误。
* */
main(args);
/*
* 2、堆溢出:java.lang.OutOfMemoryError
* 原因:Java中所有的对象都存储在堆中,通常如果JVM无法再分配新的内存,内存耗尽,垃圾回收无法及时回收内存,就会抛出OutOfMemoryError。
* */
Integer[] arr = new Integer[1024 * 1024 * 1024];
}
}
Exception代码示例:
import java.io.FileInputStream;
public class Test {
public static void main(String[] args) {
/*
* 1、运行时异常:java.lang.ArithmeticException
* 原因:ArithmeticException
* */
int a = 10;
int b = 0;
System.out.println(a / b);
/*
* 2、编译期异常:java.io.FileNotFoundException
* 原因:文件找不到异常通常是两种情况:1、系统找不到指定的路径 2、拒绝访问(指定的是目录时,就会报拒绝访问异常)
* */
FileInputStream fis = new FileInputStream("a.txt");
}
}
异常发生时,立刻退出终止,或是输出错误给用户?或者用C语言风格:用函数返回值作为执行状态?
Java提供了更加优秀的解决办法:异常处理机制。异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
Java中的异常可以是函数中的语句执行时引发的,也可以是程序员通过throw 语句手动抛出的,只要在Java程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE就会试图寻找异常处理程序来处理异常。
Throwable类是Java异常类型的顶层父类,一个对象只有是 Throwable 类的(直接或者间接)实例,他才是一个异常对象,才能被异常处理机制识别。JDK中内建了一些常用的异常类,我们也可以自定义异常。
异常体系
Java标准库内建了一些通用的异常,以Throwable为顶层父类。
Throwable又派生出Error类和Exception类。
错误:指Error类以及它的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及它的子类,代表程序运行时发生的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
Error和Exception的区别:
Error用于表示系统级别的错误,而Exception则表示可以被程序本身处理的异常情况。
Error通常是由JVM自身引起的,通常会导致程序崩溃或无法恢复的情况。这些错误无法通过程序员的代码来预测和处理,因为它们通常是由底层操作系统、硬件故障或者JVM自身的内部问题引起的。例如,OutOfMemoryError是一种Error,表示JVM无法分配足够的内存空间导致程序崩溃,这是由于系统资源耗尽造成的,程序无法通过代码手段避免或解决。
Exception,也就是我们经常见到的一些异常情况,表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
异常体系结构图:
异常分类:
广义上的异常是包括Exception和Error;
Java的异常(包括Exception和Error)从广义上分为检查异常(checked exceptions)和非检查的异常(unchecked exceptions)。
其中根据Exception异常进行划分,可分为运行时异常和非运行时异常。
注意:检查和非检查是对于javac来说的,这样就很好理解和区分了。
检查异常(checked exception)
检查异常(Checked Exception)是指在编译时就能被检查出来的异常,必须在代码中进行处理,否则程序将无法通过编译。
Checked异常通常由Java代码所引起,表示可能会发生的异常情况,如IO异常、SQL异常、ClassNotFoundException等等。这些异常都继承自Exception类,因此也叫作“受检查的异常”。
检查异常的设计考虑到了程序的正确性和可靠性,强迫程序员在代码中显式地进行异常处理,以保证程序的稳定和正确运行。Java的Io和数据库操作等常常会出现检查异常,提醒开发者在进行这些操作时,一定需要考虑依赖库的抛出的异常情况。
总之,Java中的检查异常通常是由Java代码所引起的,表示可能会发生的异常情况,必须在代码中进行处理或者声明抛出异常。这种异常的出现是为了保证程序的可靠性和正确性。
怎样处理检查异常
1、继续抛出,消极的方法,一直可以抛到java虚拟机来处理,就是通过throws Exception抛出。
2、用try...catch捕获
注意,对于检查的异常必须处理,或者必须捕获或者必须抛出
非检查异常(unchecked exception )
编译器不要求强制处置的异常,虽然你有可能出现错误,但是编译器不会在编译的时候检查,没必要,也不可能。
javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try...catch...finally)这样的异常,也可以不处理。
对于这些异常,我们应该修正代码,而不是去通过异常处理器处理。这样的异常发生的原因多半是代码写的有问题:如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。
怎样处理非检查异常
1、用try...catch捕获
2、继续抛出
3、不处理
4、通过代码处理
一般我们是通过代码处理的,因为你很难判断会出什么问题,而且有些异常你也无法运行时处理,比如空指针,需要人手动的去查找。
况且,捕捉异常并处理的代价远远大于直接抛出。
Exception异常划分
运行时异常:
是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是非检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
编译期异常:
是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不要自定义检查异常。
初识异常与常见异常
初识异常
下面的代码会演示2个异常类型:ArithmeticException 和 InputMismatchException。
前者由于整数除0引发,后者是输入的数据不能被转换为int类型引发。
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;
}
}
/*****************************************
----欢迎使用命令行除法计算器----
2
0
Exception in thread "main" java.lang.ArithmeticException: / by zero
at AllDemo.devide(AllDemo.java:19)
at AllDemo.CMDCalculate(AllDemo.java:13)
at AllDemo.main(AllDemo.java:6)
----欢迎使用命令行除法计算器----
----欢迎使用命令行除法计算器----
1
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 AllDemo.CMDCalculate(AllDemo.java:12)
at AllDemo.main(AllDemo.java:6)
*****************************************/
异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因为,只要一个函数发生了异常,那么他的所有的caller都会被异常影响。当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈。
异常最先发生的地方,叫做异常抛出点。
上面的代码不使用异常处理机制,也可以顺利编译,因为2个异常都是非检查异常。但是下面的例子就必须使用异常处理机制,因为异常是检查异常。
代码中我选择使用throws声明异常,让函数的调用者去处理可能发生的异常。为什么只throws了IOException?因为FileNotFoundException是IOException的子类
import java.io.FileInputStream;
import java.io.IOException;
public class ExceptionTest {
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.close();
}
}
常见异常
java.lang.RuntimeException: 运行时异常
ClassCastException: 类类型转换异常,当试图将对象强制转换为不是实例的子类时,抛出该异常;
ArrayIndexOutOfBoundsException: 数组下标越界异常,当你使用不合法的索引访问数组时会抛出该异常;
NullPointerException: 空指针异常,通过null进行方法和属性调用会抛出该异常;
ArithmeticException: 算术运算异常,除数为0,抛出该异常;
NumberFormatException: 数字转换异常,当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常;
InputMismatchException: 输入不匹配异常,输入的值数据类型与设置的值数据类型不能匹配。
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Date;
import java.util.Scanner;
public class ExceptionTest {
//ClassCastException
public void test1() {
Object obj = new Date();
String str = (String) obj;
}
//IndexOutOfBoundsException
public void test2() {
//ArrayIndexOutOfBoundsException
int[] arr = new int[10];
System.out.println(arr[10]);
//StringIndexOutOfBoundsException
String str = "abc";
System.out.println(str.charAt(3));
}
//NullPointerException
public void test3() {
int[] arr = null;
System.out.println(arr[3]);
String str = "abc";
str = null;
System.out.println(str.charAt(0));
}
//ArithmeticException
public void test4() {
int a = 10;
int b = 0;
System.out.println(a / b);
}
//NumberFormatException
public void test5() {
String str = "123";
str = "abc";
int num = Integer.parseInt(str);
}
//InputMismatchException
public void test6() {
Scanner scanner = new Scanner(System.in);
int score = scanner.nextInt();
System.out.println(score);
scanner.close();
}
}
java.io.IOExeption: 输入输出异常
FileNotFoundException: 文件找不到异常,通常是两种情况:
1、系统找不到指定的路径
2、拒绝访问(指定的是目录时,就会报拒绝访问异常)
EOFException: 文件已结束异常,抛出EOFException一定是因为连接断了还在继续read;
java.lang.ClassNotFoundException: 类找不到异常,当我们通过配置文件去查找一个类的时候,如果配置路径写错,就会抛出该异常,比如:web.xml文件中根本就不存在该类的配置或者配置的路径写错;(比较常见)
java.sql.SQLException: SQL异常,数据库的各种信息的异常;
import java.io.File;
import java.io.FileInputStream;
public class ExceptionTest {
public void test7() {
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();
}
}
三、异常处理的处理机制
-
抛出异常(throw):当程序执行的过程中,遇到了异常情况,可以使用throw关键字来抛出异常。
-
捕获异常(catch):使用try-catch语句来捕获和处理异常。在try块中,程序会尝试执行一段可能会抛出异常的代码;如果发生异常,Java会从try块中跳出,并在catch块中处理这个异常。
-
处理异常(finally):finally语句块可以在try-catch语句执行之后,无论是否抛出异常,都会执行其中的代码。通常用来做一些资源清理的操作。如果不处理异常,程序就会在运行中断,finally语句块也不会被执行。
try { // 可能会出现异常的代码段 } catch (ExceptionType e) { // 处理异常的代码段 } finally { // 执行任何情况下都需要执行的代码段 }
在try块中,我们写入可能会抛出异常的代码。如果异常发生,则程序不再执行try块中的代码,并将异常传递给与之匹配的catch块。catch块中的代码会根据异常类型进行处理,比如打印日志、再次抛出异常等。最后,无论是否抛出异常,finally块中的代码都会被执行,通常用来实现资源的释放或清理。
在处理Java异常时,程序员应该尽可能地了解异常类型、异常发生的原因,并且根据具体情况编写错误处理代码,以保证程序的正确性和稳定性。
总之,Java中异常的处理机制主要由throw、catch和finally三部分组成。在程序发生异常时,程序员应该根据异常类型进行处理,同时也需要注意资源的释放和清理。
四、手动抛出异常:throw
Java异常类对象除在程序执行过程中出现异常时由系统自动生成并抛出,也可根据需要使用人工创建并抛出。
首先,要生成异常类对象,然后通过throw语句实现抛出操作(提交给Java运行环境)。
throw exceptionObject
程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面可以抛出的异常必须是Throwable或其子类的实例。
throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别。
public class StudentTest {
public static void main(String[] args) {
try {
Student s = new Student();
s.regist(-1001);
System.out.println(s);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
class Student {
private int id;
public void regist(int id) throws Exception {
if (id > 0) {
this.id = id;
} else {
// System.out.println("您输入的数据非法!");
//手动抛出异常对象
// throw new RuntimeException("您输入的数据非法!");
throw new Exception("您输入的数据非法!");
//错误的
// throw new String("不能输入负数");
}
}
@Override
public String toString() {
return "Student [id=" + id + "]";
}
}
throws和throw的区别:
在Java中,throws和throw是异常处理中两种非常重要的关键字,它们虽然看起来相似,但含义不同,作用也不同,具体区别如下:
-
throws用于声明方法可能抛出的异常类型,表示该方法可能会抛出指定的异常类或其子类。这个异常类型在方法声明的后面使用throws关键字指定,表示如果该方法抛出了这种类型的异常,那么这个异常就会被传递到调用该方法的代码处,由调用者来处理这个异常。
-
throw用于手动抛出异常,表示当前方法已经发生了异常,需要将异常抛出给调用者进行处理。throw语句需要抛出一个Throwable类型的实例对象,可以是Java标准库中提供的异常类对象,也可以是程序员自己定义的异常类对象。
简而言之,throws用于声明方法可能抛出的异常类型,而throw用于手动抛出异常对象。
下面是一个简单的示例代码:
public void method1() throws ExceptionType1, ExceptionType2 {
// 方法可能会抛出ExceptionType1和ExceptionType2类型的异常
}
public void method2() {
try {
// 可能会出现异常的代码段
throw new ExceptionType1(); // 手动抛出一个类型为ExceptionType1的异常
} catch (ExceptionType1 e1) {
// ExceptionType1类型异常的处理代码段
} catch (ExceptionType2 e2) {
// ExceptionType2类型异常的处理代码段
} finally {
// 必须执行的清理代码段
}
}
在method1方法中,使用throws关键字声明该方法可能抛出ExceptionType1和ExceptionType2类型的异常,因此在调用该方法时,调用方必须处理这些异常情况。
在method2方法中,通过throw关键字手动抛出一个类型为ExceptionType1的异常,在catch块中针对不同类型的异常进行处理,并在finally块中执行必须执行的清理操作。
总之,throws用于声明方法可能抛出的异常类型,表示该方法可能会抛出指定的异常类或其子类,而throw用于手动抛出异常对象,表示当前方法已经发生了异常,需要将异常抛出给调用者进行处理。
五、异常的链化
异常链化(Chained Exception)是一种将多个异常信息链接在一起的技术,以便诊断和分析问题。当一个异常被抛出时,如果我们希望将它与其他的异常关联在一起,就可以利用异常链技术在异常对象上设置一个cause(原因)对象,从而产生一个异常链,这样可以将一个异常的起因追溯到它的根源,方便我们查找和解决问题。
在Java中,Throwable类提供了一个getCause()方法,可以获取Throwable对象的原因(cause)异常。如果不将一个异常与其原因联系在一起,则该方法将返回null值。
try {
// 可能会发生异常的代码段
} catch (ExceptionType1 e1) {
// 处理ExceptionType1类型异常的代码段
throw new ExceptionType2("普通异常信息", e1); // 将异常e1作为ExceptionType2的原因
}
在这个代码中,当出现ExceptionType1类型的异常时,我们将这个异常对象作为构造函数参数传递给ExceptionType2类,并将它作为ExceptionType2的原因异常,从而形成了一个异常链。如果我们将此异常对象打印出来,可以看到异常对象的cause属性被设置为前一个异常对象e1。
异常链技术可以提高程序的可维护性和易读性,并且有助于我们快速诊断和解决问题。但是,在使用异常链技术时需要注意遵循规范,比如异常的传递方向应当是从低层到高层,捕获异常应当是从高层到低层等等。
总之,Java中异常链化是一种针对多个异常信息进行链接的技术,它可以帮助我们追溯和分析问题。在使用异常链技术时需要遵守一些规范,以便保证代码的可维护性和可读性。
六、自定义异常
自定义异常规则
如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自RuntimeException。
按照国际惯例,自定义的异常应该总是包含如下的构造函数:
一个无参构造函数
一个带有String参数的构造函数,并传递给父类的构造函数。
一个带有String参数和Throwable参数,并都传递给父类构造函数。
一个带有Throwable 参数的构造函数,并传递给父类的构造函数。
自定义异常构建
首先写一个自定义异常,继承Exception,代码如下:
public class MyException extends Exception {
public MyException() {
super();
}
public MyException(String message) {
super(message);
}
public MyException(String message, Throwable cause) {
super(message, cause);
}
public MyException(Throwable cause) {
super(cause);
}
}
使用自定义异常
如果自定义异常是为了提示,在使用的时候,一定要用try..catch,不要直接用throw往外抛。
public class Test {
public static void main(String[] args) {
A a = new A();
try {
a.show(-2);
} catch (MyException e) {
System.out.println(e.getMessage());
}
}
}
class A {
public void show(int num) throws MyException {
if (num < 0) {
MyException me = new MyException("异常:" + num + "不是正数");
throw me;//抛出异常,结束方法show()的执行
}
System.out.println(num);
}
}
七、异常的注意事项
-
异常捕获的顺序很重要:当一个方法抛出异常时,Java会在当前方法中查找一个能够处理异常的catch块,如果找不到,则这个异常将被传递到上层调用者中,直到找到一个能够处理该异常的catch块或者程序中止。因此,在写try-catch块时,需要将最具体的异常类型放在前面,最一般的异常类型放在最后。
-
避免空指针异常:空指针异常是程序开发中最常见的异常之一。在编写程序时,需要进行空指针检查,避免程序出现异常情况。如果必须使用null值,请谨慎处理,避免出现异常情况。
-
不要吞噬异常:在编写程序时,应该避免使用空的catch块,以避免吞噬异常。吞噬异常是指在程序中捕获异常,但没有进行处理或者处理不当的现象,导致程序中的异常没有得到有效处理,继而导致了更严重的问题。
-
资源管理和异常处理:在释放程序所持有资源时,尽量使用finally语句块来确保资源的释放。在异常处理过程中,要注意程序中的资源是否已经被释放。否则,就会导致资源泄漏和程序崩溃的问题。
-
自定义异常:在Java中,可以根据自己的需要定义异常类,使得程序更加方便维护和理解。自定义异常类需要继承Exception类或者RuntimeException类,并重写父类中的一些方法。
-
异常的链化:如果异常发生了多次,我们可以使用异常链的技术将多个异常信息链接在一起,可以方便追溯和分析问题。
-
异常的抛出:在使用throw抛出异常时,需要根据异常的类型、原因和上下文等来描述异常,使得异常信息更加明确、易于定位和解决问题。
总之,在Java中,异常处理是程序设计中必不可少的一部分。在编写程序时,需要注意异常的处理,避免出现异常情况或者应对异常情况,使得程序更加可靠、易于维护和扩展。
八、finally块和return
finally块是一个非常重要的代码块,通常用于执行一些必须在try或catch块中处理的清理代码,比如文件或网络连接的关闭等操作。finally块在try或catch块后执行,并无论是try块执行成功与否,都会执行其中的代码。
需要注意的是,在finally块中执行的语句也可以是return语句,这会导致try或catch块中的return语句失效,直接返回finally块中的return语句所指定的值。这种情况下,finally块中的return语句会覆盖try块和catch块中的return语句。
以下是一个简单的示例代码,演示finally块和return语句的关系:
public static int calculateSum(int[] numbers) {
int sum = 0;
try {
for (int index = 0; index <= numbers.length; index++) {
sum += numbers[index];
}
return sum;
} catch (Exception e) {
System.out.println("Exception encountered: " + e.getMessage());
return sum;
} finally {
sum += 100;
System.out.println("The sum in finally block is: " + sum);
// 下面这行代码会直接返回 1000 而不是 try 或 catch 中的 sum
return sum;
}
}
在这个示例代码中,我们操作了一个整数数组,并使用一个try-catch语句块来计算数组中数字的总和。在finally块中,我们进行了一些必须执行的清理工作,并添加了一个return语句来返回新的总和1000,这将覆盖try块和catch块中的return语句。
在运行上述代码时,将输出以下内容:
Exception encountered: null
The sum in finally block is: 100
因为try块中存在数组越界异常,程序在执行try块代码时会抛出异常,然后转到catch块中执行异常处理代码,并返回一个值为0的sum。接着,因为finally块中存在return语句,程序将跳过try块和catch块中的return语句,直接返回finally块中的新的总和1000,这个值将成为方法的最终返回值。
因此,需要注意在使用try-catch-finally语句块时,finally块中的return语句可能会覆盖try或catch块中的return语句,程序返回finally块中return语句所指定的值。在实际应用中,需要按需使用finally块和return语句,以确保程序的正确性和稳定性。