目录
常见异常代码演示(这些代码运行都会出现异常,为了编译运行通过,在远程库中代码全部被注销了)
开发中如何选择使用try-catch-finally还是使用“throws”?
异常概述与异常体系结构
说明:
在使用计算机语言进行项目开发的过程中,即使程序员把代码写得尽善尽美,在系统的运行过程中仍然会遇到一些问题,因为很多问题不是靠代码能够避免的,比如:客户输入数据的格式,读取文件是否存在,网络是否始终保持通畅等等。
在Java语言中,将程序执行中发生的不正常情况称为“异常”。(开发过程中的语法错误和逻辑错误不是异常)
Java 程序在执行过程中所发生的异常事件可分为两类
- Error:Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowError和OOM。一般不编写针对性的代码进行处理,例如:
- main(args);//栈溢出:java.lang.StackOverflowError
- Integer[] arr = new Integer[1024*1024*1024];//堆溢出:java.lang.OutOfMemoryError
- Exception:其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如:
- 空指针访问
- 试图读取不存在的文件
- 网络连接中断
- 数组角标越界
异常解决方案
- 一是遇到错误就终止程序的运行。
- 另一种方法是由程序员在编写程序时,就考虑到错误的检测、错误消息的提示,以及错误的处理。(捕获错误最理想的是在编译期间,但有的错误只有在运行时才会发生。比如:除数为0,数组下标越界等)
异常分类
- 运行时异常
- 是指编译器不要求强制处置的异常。一般是指编程时的逻辑错误,是程序员应该积极避免其出现的异常。java.lang.RuntimeException类及它的子类都是运行时异常。
- 对于这类异常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。
- 运行时异常:执行java.exe命名时,可能出现的异常
- 编译时异常
- 是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。编译器要求Java程序必须捕获或声明所有编译时异常。
- 对于这类异常,如果程序不处理,可能会带来意想不到的结果。
- 编译时异常:执行javac.exe命名时,可能出现的异常。
- 下图为Java异常体系结构
常见异常代码演示(这些代码运行都会出现异常,为了编译运行通过,在远程库中代码全部被注销了)
package javase9;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.util.Date;
import java.util.Scanner;
public class Exception1 {
/*运行时异常:编译不会报错,只有运行时才会报错*/
//NullPointerException空指针异常
@Test
public void test1(){
int[] arr = null;
System.out.println(arr[3]);//打印没有声明的的下标
String s1 = null;
System.out.println(s1.charAt(0));//打印没有字符串的String对象
}
// ArrayIndexOutOfBoundsException下标越界异常
@Test
public void test2() {
// int[] arr = new int[10];
// System.out.println(arr[10]);
// String str = "abc";
// System.out.println(str.charAt(3));
}
// ClassCaseException数据类型错误
@Test
public void test3() {
Object obj = new Date();
String str = (String)obj;
}
// NumberFormatException编号格式错误
@Test
public void test4() {
String str = "123";
str = "abc";
int num = Integer.parseInt(str);
}
// InputMismatchException输入不匹配异常
@Test
public void test5() {
Scanner scanner = new Scanner(System.in);
int score = scanner.nextInt();
System.out.println(score);
scanner.close();
}
// ArithmeticException算数异常
@Test
public void test6() {
int a = 10;
int b = 0;
System.out.println(a / b);
}
/*编译时异常*/
@Test
public void test7() {
File file = new File("hello.txt");
FileInputStream fis = new FileInputStream(file);//未处理的java.io.FileNotFoundException,文件路径可能找不到,因此这个异常必须要捕获处理
int data = fis.read();//未处理的java.io.IOException,这个异常必须要捕获处理
while(data != -1){
System.out.print((char)data);
data = fis.read();//java.io.IOException,这个异常必须要捕获处理
}
fis.close();//java.io.IOException,这个异常必须要捕获处理
}
}
异常处理(两种机制)
说明:
在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行x/y运算时,要检测分母为0,数据为空,输入的不是数据而是字符等。过多的if-else分支会导致程序的代码加长、臃肿,可读性差。
因此Java采用异常处理机制。是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁、优雅,并易于维护。
Java处理异常的方式
Java提供的是异常处理的抓抛模型
- 过程一:“抛”:程序在征程执行过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象并将此对象抛出。一旦抛出对象以后,其后的代码就不再执行。
- 过程二:“抓”:可以理解为异常的处理方式:① try-catch-finally ② throws
处理方式
-
try-catch-finally:
-
try{}:
-
捕获异常的第一步是用try{…}语句块选定捕获异常的范围,将可能出现异常的代码放在try语句块中。
-
- catch(异常类型){}:
- 在catch语句块中是对异常对象进行处理的代码。每个try语句块可以伴随0到多个catch语句,用于处理可能产生的不同类型的异常对象。也就是说组合可以使trycatch也可以是tryfinally
- 捕获异常的有关信息:与其它对象一样,可以访问一个异常对象的成员变量或调用它的方法。
- getMessage() 获取异常信息,返回字符串
- printStackTrace() 获取异常类名和异常信息,以及异常出现在程序中的位置,在命令行打印异常信息在程序中出错的位置及原因。返回值void。
-
finally{}:
-
捕获异常的最后一步是通过finally语句为异常处理提供一个统一的出口,使得在控制流转到程序的其它部分以前,能够对程序的状态作统一的管理。
-
不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句中是否有return,finally块中的语句都会被执行。
-
finally语句是可选的写和不写
-
finally中声明的是一定会被执行的代码。即使catch中又出现异常了,try中有return语句,catch中有return语句等情况。
-
catch语句是至少写一个
try{ //可能出现异常的代码 }catch(异常类型1 变量名1){ //处理异常的方式1 }catch(异常类型2 变量名2){ //处理异常的方式2 } ...... finally{ //一定会执行的代码 }
-
-
完整流程:
-
使用try将可能出现异常代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象
-
try下面的代码不用再执行,立马根据此对象的类型,去catch中进行匹配
-
一旦try中的异常对象匹配到某一个catch时,就进入catch中进行异常的处理。一旦处理完成,就跳出当前的try-catch结构(在没有写finally的情况)。继续执行其后的代码
-
catch中的异常类型如果没有子父类关系,则谁声明在上,谁声明在下无所谓。
catch中的异常类型如果满足子父类关系,则要求子类一定声明在父类的上面。否则,报错.因为代码执行是由上到下,在多态的原则下父类捕获的异常对象更多,那子类在父类之下就没有存在的意义了 -
任何一个catch(){}方法执行完后下一步就是到finally{}了,如果catch{}里面出现代码异常也不会再捕获交给下一个catch(){}处理
-
常用的异常对象处理方法:① String getMessage(), ② void printStackTrace()
-
try结构里面的是局部变量,除了try方法体就不能被调用了
-
try-catch-finally结构可以嵌套
-
我们使用try-catch-finally将一个编译时可能出现的异常,延迟到运行时出现。
-
开发中,由于运行时异常比较常见,所以我们通常就不针对运行时异常编写try-catch-finally了。针对于编译时异常,我们说一定要考虑异常的处理
像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动的回收的,我们需要自己手动的进行资源的 * 释放。此时的资源释放,就需要声明在finally中
-
-
package javase9;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Exception2 {
//测试1
@Test
public void test1() {
try {
int a = 10;
int b = 0;
System.out.println(a / b);//try{}出现异常后会生成一个对应的异常对象,这里的异常对象为算数异常对象
} catch (ArithmeticException e) {//捕获算数异常对象为算术异常对象
e.printStackTrace();//打印算数异常所有信息到控制台
int[] arr = new int[10];
System.out.println(arr[10]);//一个catch(){}方法执行完后就直接到finally{}了,这里的下标越界异常并没有被捕获
} catch (Exception e) {
e.printStackTrace();//也就意味着这个catch()方法不能被执行
}
// System.out.println("我是最后执行的,但我不在finally里");//不写在finally的代码出现代码运行异常时可能不会被执行
finally {
System.out.println("我是最后执行的,我在finally里");
}
}
//测试二
@Test
public void testMethod() {
int num = method();
System.out.println(num);//num3
}
public int method() {
try {
int[] arr = new int[10];
System.out.println(arr[10]);//出现下标越界异常对象
return 1;//return 1;并没有被执行
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();//打印异常到控制台
return 2;//该语句被执行,只不过被return 3覆盖了
} finally {
System.out.println("我一定会被执行");
return 3;//返回值参考finally,因为他是最后被执行的
}
}
//测试3
@Test
public void test2() {
FileInputStream fis = null;
try {
File file = new File("hello1.txt");//新的File实例,文件文件可能不存在,而出现异常
fis = new FileInputStream(file);//打开与实际文件的连接创建,文件可能不存在,会返回null因此会抛出异常 FileNotFoundException
int data = fis.read();//从输入流中读取一个字节的数据。如果没有可用的输入,此方法将阻塞。这个方法可能出现异常java.io.IOException
while (data != -1) {//如果-1就是没有值就会循环终止
System.out.print((char) data);//0~255的数字强转成char类型字符
data = fis.read();//读下一个数
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
-
throws + 异常类型
-
如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。
-
在方法的(形参列表)后声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类.
指明此方法执行时,可能会抛出的异常类型。一旦当方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足throws后异常类型时,就会被抛出。异常代码后续的代码,就不再执行! -
关于异常对象的产生:① 系统自动生成的异常对象 ② 手动生成一个异常对象,并抛出(throw)
-
throws与try-catch-finally区别:
-
try-catch-finally:真正的将异常给处理掉了
-
throws的方式只是将异常抛给了方法的调用者。 并没有真正将异常处理掉。
-
-
package javase9;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Exception3 {
//方法1,再用try-catch-finally处理,而是throws给调用方法处理,FileNotFoundException变灰是因为IOException是它的父类
public static void method1() throws FileNotFoundException, IOException {
File file = new File("hello1.txt");
FileInputStream fis = new FileInputStream(file);
int data = fis.read();
while(data != -1){
System.out.print((char)data);
data = fis.read();
}
fis.close();
System.out.println("hahaha!");
}
//方法二调用方法一时,必须处理方法二产生的异常,没有处理手段只能继续抛出
public static void method2() throws IOException {
method1();
}
//方法三调用方法二,必须处理方法二产生的异常,处理手段是trycatch
public static void method3(){
try {
method2();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//使用main方法执行方法二时再进行trycatch处理
try {
method2();
} catch (IOException e) {
e.printStackTrace();
}
//method3处理了就不用main方法处理
method3();
}
}
上面代码图解
开发中如何选择使用try-catch-finally还是使用“throws”?
如果父类中被重写的方法没有用throws的方式抛出去异常,则子类重写的方法也不能使用throws,意味着如果子类重写的方法有异常,必须使用try-catch-finally方式处理而不能抛出(接触通过实现Callable接口创建线程时就会接触到)
执行的方法aaa中,先后又调用了另外的几个方法,这几个方法又是递进关系(a方法得出的结果需要供b方法使用,b方法得出的结果需被c方法使用..)执行的。我们建议这几个方法使用throws的方式进行处理,而执行的方法aaa可以考虑使用try-catch-finally的方式进行处理(方法没人调用时,方法自己处理,有人调方法时,被调用的方法可以抛出,哪个方法调用哪个方法处理(一般用try-catch-finally)。
用户自定义异常类
-
继承现有的异常结构:RunTimeException、Exception(Exception是RunTimeException的父类,Exception是编译异常,RunTimeException是运行时异常)
-
提供全局常量 serialVersionUID,是一个序列号.是被static修饰的,因此与自定义类是共存的,也是类的一个标识,区分类、抛出异常等。后面学IO流的时候,对数据进行序列化反序列化时需要用的
-
提供重载的构造器,分别为无参和含参构造器。异常类的无参构造,构造一个无需传入参数的新的运行时异常。异常类的含参构造,形参列表固定为String类型参数写法,调用父类含参构造方法传入指定的详细信息msg构造一个新的运行时异常
package javase9;
import java.util.Scanner;
/**
* 自定义的异常类
*/
public class Exception4 extends Exception {//一般地,用户自定义异常类都是RuntimeException的子类
static final long serialVersionUID = 4425106133048118111L;//自定义异常类的唯一标识
//提供重载构造器
public Exception4() {//异常类的无参构造,构造一个无需传入参数的新的运行时异常
}
public Exception4(String msg) {
super(msg);//异常类的含参构造,形参列表固定为String类型参数写法,调用父类含参构造方法传入指定的详细信息msg构造一个新的运行时异常
}
}
/**
* 使用自定义异常类
*/
class MyException {
public static void main(String[] args) {
try {
new Student().setId(-100);//调用方法时,这个函数会抛出一个异常对象Exception4,然后被catch的Exception通过多态方式接受
} catch (Exception e) {
System.out.println(e.getMessage());//输入有误,e.getMessage()就是获取这个异常对象的信息,返回的是String类型字符串
}
}
}
/**定义一个学生类*/
class Student {
private int id;
public int getId() {
return id;
}
public void setId(int id) throws Exception4 {//意味着这个方法也选择了抛出异常,叫处理异常交给了调用方法处理
if (id > 0) {
this.id = id;
}else{
throw new Exception4("输入有误");//这里选择了抛出异常,构造一个新的编译时时异常
}
}
}
自定义异常处理练习
需求:
- 接受控制台两个参数,两个数相除
- 要求不能为负数(提示:自定义异常)
- 数据类型只能是除了boolean以外的基本数据类型(提示:数据类型不一致异常NumberFormatException)
- 不能除0(提示:算数异常ArithmeticException)
package javase9;
import java.util.Scanner;
public class Exception5 {
public static void main(String[] args) {
try {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入第一个数");
double i = Double.parseDouble(scanner.next());
System.out.println("请输入第二个数");
double j = Double.parseDouble(scanner.next());
double result = method(i, j);
System.out.println(result);
} catch (NumberFormatException e) {
System.out.println("数据类型不一致");
} catch (ArithmeticException e) {
System.out.println("不能除0");
} catch (Exception4 e) {
System.out.println(e.getMessage());
}
}
public static double method(double i, double j) throws Exception4 {
if(i<0 || j<0)throw new Exception4("不允许输入负数");
/*Java float和double类型,就像其他任何语言(以及几乎任何硬件FP单元)一样,实现了浮点数学的IEEE 754标准,
它要求除以零以返回特殊的“无穷大”值。抛出异常实际上会违反该标准。
因此在浮点运算除0时,JVM将显示Infinity
因此我们需要再为器添加一条判断语句*/
if(j==0)throw new ArithmeticException();
return i/j;
}
}