九、 异常处理
9.1 异常概述与异常体系结构
9.1.1 异常的概念
- 异常就是希望程序出现问题时不要中断,而是捕获到异常。然后继续执行下面的代码。
- 注意:编程时的逻辑错误、语法错误等不算是异常。
9.1.2 异常体系结构
异常最高的的类是 java.lang.Throwable. 其有 2 2 2个子类:Error和Exception。
异常体系结构分为两大类:
- Error:是Java虚拟机都无法解决的错误。如栈溢出、堆溢出等。一般无法通过异常处理解决,需要修改代码。
- Exception:因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。其又分为
2
2
2类:编译时异常和运行时异常。例如:
- 空指针访问;
- 试图读取不存在的文件;
- 网络连接中断;
- 数组角标越界;
9.1.3 编译时异常和运行时异常
-
捕获异常最理想的是在编译期间,但有的错误只有在运行时才会发生。如:除数为0、数组角标越界。
-
对于异常,最理想的解决方法是:程序员在编写程序时,就考虑到错误的检测、错误消息的提示,以及错误的处理。
-
如下图所示:红色的是编译时异常,蓝色的是运行时异常。
9.2 常见异常
9.2.1 运行时异常
( 1 1 1) 空指针异常 (NullPointerException)
@Test
public void test1() {
int[] arr = null;
System.out.println(arr[2]);
}
@Test
public void test2() {
// String str = "abc";
String str = null;
System.out.println(str.charAt(1));
}
( 2 2 2) 数组角标越界异常 (ArrayIndexOutOfBoundsException)
@Test
public void test3() {
int[] arr = new int[3];
System.out.println(arr[3]);
}
( 3 3 3) 字符串角标越界异常(StringIndexOutOfBoundsException)
@Test
public void test4() {
String str = "abcde";
System.out.println(str.charAt(5));
}
( 4 4 4) 类型转换异常 (ClassCastException)
@Test
public void test5() {
Object obj = new Date();
String str = (String) obj;
}
( 5 5 5) 数值转换异常 (NumberFormatException)
@Test
public void test6() {
String str = "abc";
int num = Integer.parseInt(str);
}
( 6 6 6) 输入不匹配异常 (InputMismatchException)
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入学生成绩:");
int score = scanner.nextInt();
System.out.println("学生成绩为:" + score);
}
请输入学生成绩:abc
Exception in thread "main" java.util.InputMismatchException
( 7 7 7) 算术异常 (ArithmeticException)
@Test
public void test7() {
int a = 10;
int b = 0;
int c = a / b;
System.out.println(c);
}
9.2.2 编译时异常
( 1 1 1) 输入输出异常 (IOException)
@Test
public void test8() {
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();
}
以上代码编译会不通过,不会生成字节码文件 java.exe。原因是没有处理文件找不到的异常。
9.3 异常处理机制一:try-catch-finally
在编写程序时,经常要在可能出现错误的地方加上检测的代码。如进行 x / y x/y x/y 运算时,要检测分母是否为 0 0 0 、数据是否为空、输入的不是数据而是字符等。 过多的 if-else 分支会导致程序的代码 加长、臃肿、可读性差。因此采用异常处理机制 。
Java异常处理
Java采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁、优雅,并易于维护。主要分为一下两类:
- 方式一:try-catch-finally:捕获异常,自己解决。
- 方式二:throws+异常类型:自己没能力解决,层层上报。直到有一层可以解决。最终到main方法这一层还没解决,就挂了。
9.3.1 抓抛模型
-
过程一:“抛”:程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象。并将此对象抛出。一旦抛出对象以后,其后的代码就不再执行。
关于异常对象的产生:① 系统自动生成的异常对象② 手动的生成一个异常对象,并抛出(throw)
-
过程二:"抓":可以理解为异常的处理方式:① try-catch-finally ② throws
-
9.3.2 try-catch-finally的使用
使用的格式:
try {
可能出现异常的代码;
}catch(异常类型1 变量名1) {
异常处理的方式1;
}catch(异常类型2 变量名2) {
异常处理的方式2;
}catch(异常类型3 变量名3) {
异常处理的方式3;
}
...
finally {
一定会执行的代码;
}
9.3.3 try-catch-finally的说明
-
finally 是可选的。
-
使用try将可能出现异常代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中进行匹配。
-
一旦try中的异常对象匹配到某一个catch时,就进入catch中进行异常的处理。一旦处理完成,就跳出当前的try-catch结构(在没有写finally的情况)。继续执行其后的代码。
-
catch中的异常类型如果没有子父类关系,则谁声明在上,谁声明在下无所谓。catch中的异常类型如果满足子父类关系,则要求子类一定声明在父类的上面。否则,报错。
-
常用的异常对象处理的方式: ① String getMessage() ② printStackTrace()。
-
在try结构中声明的变量,再出了try结构以后,就不能再被调用。解决办法:在try结构外声明这个变量。
-
try-catch-finally结构可以嵌套。
例子1:
@Test
public void test6() {
String str = "abc";
try {
int num = Integer.parseInt(str);
System.out.println("此条能执行1");
} catch (NullPointerException e) {
System.out.println("出现空指针异常了,不要着急...");
} catch (NumberFormatException e) {
System.out.println("出现数值转换异常了,不要着急...");
} catch (Exception e) {
System.out.println("出现异常了,不要着急...");
}
System.out.println("此条能执行2");
}
输出:
出现数值转换异常了,不要着急...
此条能执行2
9.3.4 finally的使用
- finally是可选的。
- finally中声明的是一定会被执行的代码。即使catch中又出现异常了,try中有return语句,catch中有return语句等情况。
- 像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动的回收的,我们需要自己手动的进行资源的释放。此时的资源释放,就需要声明在finally中。
- 程序先执行finally中的语句,再执行try或者catch中的return。倘若finally中有return,则执行finally中的return退出。
9.3.5 try-catch-finally的体会
- 使用try-catch-finally处理编译时异常,是得程序在编译时就不再报错,但是运行时仍可能报错。相当于我们使用try-catch-finally将一个编译时可能出现的异常,延迟到运行时出现。
- 开发中,由于运行时异常比较常见,所以我们通常就不针对运行时异常编写try-catch-finally了。运行时异常直接修改代码即可。
- 针对于编译时异常,我们说一定要考虑异常的处理。
例子2:
public void test8() {
try {
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();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
输出:
java.io.FileNotFoundException: ..\hello.txt (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at ch09.Exception1.TryCatchFinally.test8(TryCatchFinally.java:41)
9.4 异常处理机制二:throws
9.4.1 throws的使用
-
"throws + 异常类型"写在方法的声明处。指明此方法执行时,可能会抛出的异常类型。一旦当方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足throws后异常类型时,就会被抛出。异常代码后续的代码,就不再执行!
例子:
public static void method1() throws FileNotFoundException, IOException { 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(); }
-
throws 向上抛给该方法的调用者。一般最迟抛到main方法就要处理了。
9.4.2 throws的体会
- try-catch-finally才是真正将异常给处理掉了。
- throws的方式只是将异常抛给了方法的调用者。并没有真正将异常处理掉。
9.4.3 异常的继承
-
子类重写方法抛出的异常类型不能大于父类被重写的方法抛出的异常类型。
例:下列会报错,因为子类重写的方法抛出的异常类型 Exception 大于父类被重写方法抛出的异常类型 IOException。
class SuperClass { public void method() throws IOException { } } class SubClass extends SuperClass { @Override public void method() throws Exception {//报错! } }
9.4.4开发中如何选择使用try-catch-finally 还是使用throws?
-
如果父类中被重写的方法没有throws方式处理异常,则子类重写的方法也不能使用throws,意味着如果子类重写的方法中有异常,必须使用try-catch-finally方式处理。
-
执行的方法A中,先后又调用了另外的几个方法,这几个方法是递进关系执行的。我们建议这几个方法使用throws的方式进行处理。而执行的方法A可以考虑使用try-catch-finally方式进行处理。
9.5 手动抛出异常:throw
9.5.1 throw 的使用
-
有时我们需要手动创建异常对象。
-
手动创建异常对象的格式:
throw new RuntimeException("您输入的数据非法!");
例子:
class Student { private int id; public void register(int id) { if (id > 0) { this.id = id; } else { throw new RuntimeException("您输入的数据非法!"); } } public int getId() { return id; } }
测试:
public static void main(String[] args) { Scanner scanner = new Scanner(System.in); Student s = new Student(); System.out.print("请输入学号:"); int id = scanner.nextInt(); s.register(id); System.out.println("学号为:" + s.getId()); }
输出:
请输入学号:-1 Exception in thread "main" java.lang.RuntimeException: 您输入的数据非法!
-
或者,把上述代码的
register()
方法抛出给main方法的调用者。如下所示:class Student { private int id; public void register(int id) throws Exception{ if (id > 0) { this.id = id; } else { throw new RuntimeException("您输入的数据非法!"); } } public int getId() { return id; } }
测试:
public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { Student s = new Student(); System.out.print("请输入学号:"); int id = scanner.nextInt(); s.register(id); System.out.println("学号为:" + s.getId()); } catch (Exception e) { System.out.println(e.getMessage()); } }
输出:
请输入学号:-1 您输入的数据非法!
9.5.2 throw和throws的区别
throw:手动生成一个异常对象,并抛出。使用在方法内部。<-> 与自动抛出异常对应。
throws:处理异常的方式。<-> 与try-catch-finally对应。
“上游排污,下游治污”: throw是throws的前一个环节,若没有throw抛出异常对象,则无法进行throws异常处理。
9.6 用户自定义异常类
9.6.1 自定义异常类的方法
- 继承于现有的异常结构:RuntimeException 、Exception
- 提供全局常量:serialVersionUID。在网络传输的时候供校验用。
- 提供重载的构造器。一般只需要写空参构造器和显示异常信息的message构造器即可。
例子:
public class MyException extends RuntimeException {
static final long serialVersionUID = -7014887293256938L;
public MyException() {
}
public MyException(String msg) {
super(msg);
}
}
9.6.2 自定义异常类的使用
public class ThrowTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try {
Student s = new Student();
System.out.print("请输入学号:");
int id = scanner.nextInt();
s.register(id);
System.out.println("学号为:" + s.getId());
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
class Student {
private int id;
public void register(int id) throws Exception{
if (id > 0) {
this.id = id;
} else {
// throw new RuntimeException("您输入的数据非法!");
throw new MyException("不能输入负数!");
}
}
public int getId() {
return id;
}
}
输出:
请输入学号:-1
不能输入负数!
9.7 异常处理练习
9.7.1 异常的执行顺序
public class ReturnExceptionDemo {
static void methodA() {
try {
System.out.println("进入方法A");
throw new RuntimeException("制造异常");
} finally {
System.out.println("用A方法的finally");
}
}
static void methodB() {
try {
System.out.println("进入方法B");
return;
} finally {
System.out.println("调用B方法的finally");
}
}
public static void main(String[] args) {
try {
methodA();
} catch (Exception e) {
System.out.println(e.getMessage());
}
methodB();
}
}
输出:
进入方法A
用A方法的finally
制造异常
进入方法B
调用B方法的finally
9.7.2 综合练习
题目:
编写应用程序EcmDef.java,接收命令行的两个参数,要求不能输入负数,计算两数相除。
对数据类型不一致(NumberFormatException)、缺少命令行参数(ArrayIndexOutOfBoundsException、除0(ArithmeticException)及输入负数(EcDef 自定义的异常)进行异常处理。
提示:
(1)在主类(EcmDef)中定义异常方法(ecm)完成两数相除功能。
(2)在main()方法中使用异常处理语句进行异常处理。
(3)在程序中,自定义对应输入负数的异常类(EcDef)。
(4)运行时接受参数 java EcmDef 20 10 //args[0]=“20” args[1]=“10”
(5)Interger类的static方法parseInt(String s)将s转换成对应的int值。
如:int a=Interger.parseInt(“314”); //a=314;
- 老实说,我没看懂题目在说什么…
我的首次答案:EcmDef类
public class EcmDef {
public static void main(String[] args) {
try {
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = 0;
c = ecm(a, b);
System.out.println("得数为:" + c);
} catch (NumberFormatException e) {
System.out.println("数据类型不一致!");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("缺少命令行参数!");
} catch (ArithmeticException e) {
System.out.println("除数不能为0!");
} catch (EcDef e) {
System.out.println(e.getMessage());
}
}
public static int ecm(int a, int b) throws EcDef {
if (a < 0 || b < 0) {
throw new EcDef("不能输入负数!");
}
return a / b;
}
}
自定义异常EcDef类:
public class EcDef extends Exception{
static final long serialVersionUID = -7014887293256931L;
public EcDef() {
}
public EcDef(String message) {
super(message);
}
}
( 1 \mathbf{1} 1) 传入形参:
15 abc
输出:
数据类型不一致!
( 2 \mathbf{2} 2) 传入形参:
15
输出:
缺少命令行参数!
( 3 \mathbf{3} 3) 传入形参:
15 0
输出:
除数不能为0!
( 4 \mathbf{4} 4) 传入形参:
15 -3
输出:
不能输入负数!
9.8 项目三实操技巧心得
9.8.1 NameListService部分
-
当用数字代表某些具体事物时,可以在属性部分添加全局常量变量名对应这些数字,在代码中用这些数字代表的变量名取代单纯用数字,提高代码的可读性。
例:Data类
public static final int EMPLOYEE = 10; public static final int PROGRAMMER = 11; public static final int DESIGNER = 12; public static final int ARCHITECT = 13; public static final int PC = 21; public static final int NOTEBOOK = 22; public static final int PRINTER = 23; //Employee : 10, id, name, age, salary //Programmer: 11, id, name, age, salary //Designer : 12, id, name, age, salary, bonus //Architect : 13, id, name, age, salary, bonus, stock public static final String[][] EMPLOYEES = { {"10", "1", "马 云", "22", "3000"}, {"13", "2", "马化腾", "32", "18000", "15000", "2000"}, {"11", "3", "李彦宏", "23", "7000"}, {"11", "4", "刘强东", "24", "7300"}, {"12", "5", "雷 军", "28", "10000", "5000"}, {"11", "6", "任志强", "22", "6800"}, {"12", "7", "柳传志", "29", "10800","5200"}, {"13", "8", "杨元庆", "30", "19800", "15000", "2500"}, {"12", "9", "史玉柱", "26", "9800", "5500"}, {"11", "10", "丁 磊", "21", "6600"}, {"11", "11", "张朝阳", "25", "7100"}, {"12", "12", "杨致远", "27", "9600", "4800"} };
NameListService类中的应用:
switch (type) { case EMPLOYEE: employees[i] = new Employee(id, name, age, salary); break; case PROGRAMMER: eq = createEquipment(i); employees[i] = new Programmer(id, name, age, salary, eq); break; case DESIGNER: eq = createEquipment(i); bonus = Integer.parseInt(EMPLOYEES[i][5]); employees[i] = new Designer(id, name, age, salary, eq, bonus); break; case ARCHITECT: eq = createEquipment(i); bonus = Integer.parseInt(EMPLOYEES[i][5]); stock = Integer.parseInt(EMPLOYEES[i][6]); employees[i] = new Architect(id, name, age, salary, eq, bonus, stock); break; }
-
当读进构造器特别长的类时,可以不必在构造器括号里硬写很长的代码,只需要在上方用简短的变量名声明即可。
例:我的原始代码:
switch (Data.EMPLOYEES[i][0]) { case "10": employees[i] = new Employee(Integer.parseInt(Data.EMPLOYEES[i][1]), Data.EMPLOYEES[i][2], Integer.parseInt(Data.EMPLOYEES[i][3]), Double.parseDouble(Data.EMPLOYEES[i][4])); break; case "11": employees[i] = new Programmer(Integer.parseInt(Data.EMPLOYEES[i][1]), Data.EMPLOYEES[i][2], Integer.parseInt(Data.EMPLOYEES[i][3]), Double.parseDouble(Data.EMPLOYEES[i][4])); break; case "12": employees[i] = new Designer(Integer.parseInt(Data.EMPLOYEES[i][1]), Data.EMPLOYEES[i][2], Integer.parseInt(Data.EMPLOYEES[i][3]), Double.parseDouble(Data.EMPLOYEES[i][4]), Double.parseDouble(Data.EMPLOYEES[i][5])); break; case "13": employees[i] = new Architect(Integer.parseInt(Data.EMPLOYEES[i][1]), Data.EMPLOYEES[i][2], Integer.parseInt(Data.EMPLOYEES[i][3]), Double.parseDouble(Data.EMPLOYEES[i][4]), Double.parseDouble(Data.EMPLOYEES[i][5]), Integer.parseInt(Data.EMPLOYEES[i][6])); }
老师的优雅代码:
for (int i = 0; i < employees.length; i++) { // 获取通用的属性 int type = Integer.parseInt(EMPLOYEES[i][0]); int id = Integer.parseInt(EMPLOYEES[i][1]); String name = EMPLOYEES[i][2]; int age = Integer.parseInt(EMPLOYEES[i][3]); double salary = Double.parseDouble(EMPLOYEES[i][4]); // Equipment eq; double bonus; int stock; switch (type) { case EMPLOYEE: employees[i] = new Employee(id, name, age, salary); break; case PROGRAMMER: eq = createEquipment(i); employees[i] = new Programmer(id, name, age, salary, eq); break; case DESIGNER: eq = createEquipment(i); bonus = Integer.parseInt(EMPLOYEES[i][5]); employees[i] = new Designer(id, name, age, salary, eq, bonus); break; case ARCHITECT: eq = createEquipment(i); bonus = Integer.parseInt(EMPLOYEES[i][5]); stock = Integer.parseInt(EMPLOYEES[i][6]); employees[i] = new Architect(id, name, age, salary, eq, bonus, stock); break; } }
-
如果在实例化对象时,一个属性 (如equipment) 在一些类(如Employee)中没有,在一些类(Programmer、Designer和Architect)中有,在实例化前可以采用只声明不赋值的方法。
例:
-
不同类打印输出有差异的信息时,去对应的类中重写toString() 方法即可。若这些类中有继承关系时,想沿用顶级父类中的 toString() 方法但没办法越级调用时,可以把顶级父类的toString() 方法写成两个,一个是 toString() 方法,另一个是 getDetail()方法。
例:顶级父类Employee
protected String getDetails() { return id + "\t" + name + "\t" + age+ "\t" +salary; } @Override public String toString() { return getDetails(); }
一级子类Programmer:
@Override public String toString() { return getDetails() + "\t程序员\t" + status + "\t\t\t" + equipment.getDescription() ; }
二级子类Designer:
@Override public String toString() { return getDetails() + "\t设计师\t" + getStatus() + "\t" + getBonus() +"\t\t" + getEquipment().getDescription(); }
三级子类Architect:
@Override public String toString() { return getDetails() + "\t架构师\t" + getStatus() + "\t" + getBonus() + "\t" + getStock() + "\t" + getEquipment().getDescription(); }
9.8.2 TeamService部分
-
如果添加的数组本身就是父类的话,没有必要细分子类、强转、再添加进数组。因为有多态性。
例:我的冗余写法
if (p instanceof Architect) { isArchitectMax(); //memberID加1,赋值 if (p.getMemberID() == 0) { p.setMemberID(counter++); } //状态改为BUSY p.setStatus(Status.BUSY); Architect architect = (Architect) p; team[total++] = architect; numOfArchitect++; return; } else if (p instanceof Designer) { isDesignerMax(); //memberID加1,赋值 if (p.getMemberID() == 0) { p.setMemberID(counter++); } //状态改为BUSY p.setStatus(Status.BUSY); Designer designer = (Designer) p; team[total++] = designer; numOfDesigner++; return; } isProgrammerMax(); //memberID加1,赋值 if (p.getMemberID() == 0) { p.setMemberID(counter++); } //状态改为BUSY p.setStatus(Status.BUSY); team[total++] = p; numOfProgrammer++;