异常
Java 异常处理
1.引入:了解理解为主
- 异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
- 比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error;如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出 java.lang.ArithmeticException 的异常。
- 异常发生的原因有很多,通常包含以下几大类:1.用户输入了非法数据。2.要打开的文件不存在。3.网络通信时连接中断,或者JVM内存溢出。这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的
- 掌握以下三种类型的异常:
(1)检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
(2)运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
(3)错误:** 错误不是异常,而是脱离程序员控制的问题。**错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
Exception 类的层次(了解)
java.lang.Exception 类是java.lang. Throwable 类的子类。Throwable 类的父类就是java.lang.Object类,除了Exception类外,Throwable还有一个子类Error 。
异常类有两个主要的子类:IOException 类和 RuntimeException 类。
所有RuntimeException 类及其子类都是运行时异常,“运行时异常”在编写程序时可以对这种异常进行处理,也可以不处理,发生概率低
运行时异常又称非受检异常或非受控异常,
所有Exception 类的直接子类IOException 类都是编译时异常,“编译时异常”不是编译阶段发生的,是在编写程序时预先对这种异常进行处理,如果不处理,编译器报错,此异常发生概率高
编译时异常又称受检异常或受控异常,又称检查性异常
所有异常都是发生在运行阶段的(因为异常是类和对象,显示异常就是底层进行异常的new异常对象)
Error表示不可处理,Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。结果就是终止程序,退出JVM
不管是错误还是异常,都是可抛出的
所有的异常类是从 java.lang.Exception 类继承的子类。 Java 异常以类的形式存在,每一个异常类都可以创建异常对象,异常类就是一个模板,异常对象是实例化的个体,例如:发生火灾,和 小红家发生火灾,
平时控制台显示异常:实际上是JVM在执行到此处的时候,会new相应异常对象,然后JVM将new的异常对象抛出,打印输出信息到控制台了
了解UML:UML是一种统一建模语言,一种图标式语言(画图的),UML不只是在java中使用的,只要是面向对象的编程语言,都有UML,一般画UML图的都是软件架构师或者系统分析师这些人使用的
在java中使用。可以描述类和类之间的关系,程序的执行流程,对象的状态等
在开发中,软件架构师或者系统分析师设计类,java软件开发人员要能看懂
可以用软件 StarUML 来画图,像其中的空心的三角形指向的箭头就是表示继承关系
常见的运行时异常:
- ArithmeticException 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。
- ArrayIndexOutOfBoundsException 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。
- IndexOutOfBoundsException 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
- ClassCastException 当试图将对象强制转换为不是实例的子类时,抛出该异常。
- NullPointerException 当应用程序试图在需要对象的地方使用 null 时,抛出该异常
- NumberFormatException 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
常见的编译时异常:
- ClassNotFoundException 应用程序试图加载类时,找不到相应的类,抛出该异常。
- NoSuchFieldException 请求的变量不存在
- NoSuchMethodException 请求的方法不存在
"运行时异常":
public static void main(String[] args) {
System.out.println(100/0);
// 此处发生"运行时异常":java.lang.ArithmeticException: / by zero
//底层new了一个ArithmeticException异常对象,然后抛出,抛给调用者main方法,没有处理,抛给了JVM,JVM最终终止了程序的执行
//ArithmeticException异常属于运行时异常,在编译阶段不需要对这种异常进行预先的处理
//不再往下执行了
System.out.println("nihao");
}
异常对象的常用方法:printStackTrace() 和 getMassage()
- public String getMessage()
返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了 - public void printStackTrace()
打印toString()结果和栈层次到System.err,即错误输出流。
public static void main(String[] args) {
// 1. getMassage()方法 获取异常的简单描述信息
//此处仅仅new了异常对象,但没有将此异常抛出,JVM会认为这仅仅是一个普通的java对象
NullPointerException a = new NullPointerException("此处是空指针异常");
System.out.println(a.getMessage());
//输出:此处是空指针异常
// 2.printStackTrace()方法 打印异常追踪的堆栈信息
NullPointerException b = new NullPointerException("此处是空指针异常");
b.printStackTrace();
//输出:java.lang.NullPointerException: 此处是空指针异常
2. Java 的“编译时异常”处理两种方式,是在编译阶段预先进行处理;
public static void main(String[] args) {
//当在main方法中调用doSome()方法时,因为doSome()方法声明的位置上有编译时异常,
//在调用这个方法时必须对这种编译时异常进行预先处理
// doSome();
/*如果不处理,直接调用,编译器报错Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Unhandled exception type ClassNotFoundException*/
}
//表示这个doSome()方法在执行的过程中,有可能会出现“类没找到异常”ClassNotFoundException
//“类没找到异常”ClassNotFoundException直接父类是Exception,属于编译时异常
public static void doSome() throws ClassNotFoundException{
System.out.println("hei");
}
第一种:在方法声明的位置上,使用throws关键字,进行异常的上抛,上抛给调用者
throws 关键字放在方法签名的尾部。
一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。
public class Yichang {
public static void main(String[] args)throws ClassNotFoundException {
doSome();
}
public static void doSome() throws ClassNotFoundException{
System.out.println("hei");
}
}
一般不建议在main方法上使用throws,因为Java中异常发生后,如果一直上抛,最终抛给了调用者main方法,若继续上抛,抛给了JVM,JVM知道此异常,会终止java程序的执行
异常处理机制的作用就是要增强程序的健壮性
第二种:使用try...catch 语句进行异常的捕捉
使用 try 和 catch 关键字可以捕获异常。try/catch 代码块放在异常可能发生的地方。
try
{
// 程序代码
}catch(ExceptionName e)
{
//Catch 块
}
Catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。
如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数到方法是一样。java运行时系统会自动将catch括号中的Exception e 初始化,也就是实例化Exception类型的对象。e是此对象引用名称。
catch后面的小括号中的类型可以是具体的异常类型,也可以是该类型的父类型
public class Yichang {
public static void main(String[] args) {
try {
doSome();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
/*当try语句中出现异常是时,会执行catch中的语句,java运行时系统会自动将catch括号中的Exception e 初始化,也就是实例化Exception类型的对象。e是此对象引用名称。然后e(引用)会自动调用Exception类中指定的方法,也就出现了e.printStackTrace() ;。
printStackTrace()方法的意思是:在命令行打印异常信息在程序中出错的位置及原因。(这是白话解释,比较容易理解)*/
}
public static void doSome() throws ClassNotFoundException{
System.out.println("hei");
}
}
也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。
public static void main(String[] args) {
doSome();
System.out.println("抛出“运行时异常”后,程序仍然正常执行吗?");
}
public static void doSome(){
throw new NullPointerException();
// System.out.println(); 此处写入语句,提示Remove,就是不允许写入语句
}
}
/*输出结果:Exception in thread "main" java.lang.NullPointerException*/
/*分析:按住ctrl键,查看源代码,查找到空指针异常NullPointerException,其父类是RuntimeException
* 说明它是“运行时异常” , 在编译阶段,虽然方法中已经抛出异常,但不会报错
*/
多重捕获块:
一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。
要求自上而下的异常类型若父类已经捕捉过了,往下不要再次捕捉了(异常类型是从小到大的原则)
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}catch(异常类型3 异常的变量名3){
// 程序代码
}
如果保护代码中发生异常,异常被抛给第一个 catch 块。
如果抛出异常的数据类型与 ExceptionType1 匹配,它在这里就会被捕获。
如果不匹配,它会被传递给第二个 catch 块。
如此,直到异常被捕获或者通过所有的 catch 块。
3. finally关键字
- finally 关键字用来创建在 try 代码块后面执行的代码块。
- 无论是否发生异常,finally 代码块中的代码总会被执行。
- 在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。
- finally 代码块出现在 catch 代码块最后,语法如下:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}finally{
// 程序代码
}
catch 不能独立于 try 存在。
在 try/catch 后面添加 finally 块并非强制性要求的。
try 代码后不能既没 catch 块也没 finally 块。
try, catch, finally 块之间不能添加任何代码。
public class Yichang {
public static void main(String[] args) {
int a[] = new int[2];
try{
System.out.println("Access element three :" + a[3]);
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("Exception thrown :" + e);
}
finally{
a[0] = 6;
System.out.println("First element value: " +a[0]);
System.out.println("The finally statement is executed");
}
/*运行结果:
Exception thrown :java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 2
First element value: 6
The finally statement is executed
*/
finally面试题:
public class Yichang {
public static void main(String[] args) {
int s = m();
System.out.println(s);
//输出结果:100
}
public static int m() {
int i = 100;
try {
return i;
} finally {
i++;
}
}
}
/*因为java有规则:(不可以违背)
* 1. 方法体中的代码必须自上而下,依次逐行执行
* 2. 一旦return 语句执行,整个方法必须结束,
* 反编译的执行:
* public static int m() {
int i = 100;
int j = i;
i++;
return j;
}
此处的finally是执行了,return 也最后执行了*/
final 和 finally 和 finalize 之间的区别:
- final 是一个关键字,表示最终的,不可变的
- finally是一个关键字,和try联合使用,使用在异常机制中,在finally语句块中的代码一定会执行
- finalize()是Object类中的一个方法,作为方法名出现,是一个标识符
4.声明自定义异常:
编写自己的异常类时需要记住下面的几点。
-
所有异常都必须是 Throwable 的子类。
-
如果希望写一个编译时异常类,则需要继承 Exception 类。(一般用于异常发生概率比较高时)
-
如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
-
编写需要两步:1. 编写一个类继承Exception 类或继承 RuntimeException 类。2. 提供有参和无参的构造方法
-
以下实例是一个银行账户的模拟,通过银行卡的号码完成识别,可以进行存钱和取钱的操作。
InsufficientFundsException.java 文件代码:
自定义异常类,继承Exception类
package InsufficientFundsException;
public class InsufficientFundsException extends Exception{
//此处的amount用来储存当出现异常(取出的钱多于现有余额时)所缺乏的钱
private double amount;
//有参构造
public InsufficientFundsException(double amount)
{
this.amount = amount;
}
public double getAmount()
{
return amount;
}
}
CheckingAccount 类中包含一个 withdraw() 方法抛出一个 InsufficientFundsException 异常。
CheckingAccount.java 文件代码:
package InsufficientFundsException;
public class CheckingAccount {
//balance为余额,number为卡号
private double balance;
private int number;
//有参构造
public CheckingAccount(int number)
{
this.number = number;
}
//方法:存钱
public void deposit(double amount)
{
balance += amount;
}
//方法:取钱 要取的钱数:amount
public void withdraw(double amount) throws InsufficientFundsException
{//此处要加 throws InsufficientFundsException
//没有会提示 Unhandled exception type InsufficientFundsException 未处理的异常类型不足FundsException
if(amount <= balance)
{
balance -= amount;
}
else
{
double needs = amount - balance;
throw new InsufficientFundsException(needs);
/*相当于以下语句合并了:
*InsufficientFundsException e = new InsufficientFundsException(needs);
*throw e;*/
}
/*在此处用throw抛出一个异常,因为是编译时异常,在主方法调用他之前必须对他在编译阶段进行处理,因为或者throws上抛 或者try catch进行捕捉异常,
但此处若采用try catch进行捕捉异常,就是自己抛出,自己抓,没有意义,
所以采取throws上抛*/
}
}
//方法:返回余额
public double getBalance()
{
return balance;
}
//方法:返回卡号
public int getNumber()
{
return number;
}
}
BankDemo 程序示范了如何调用 CheckingAccount 类的 deposit() 和 withdraw() 方法。
package InsufficientFundsException;
public class BankDemo {
public static void main(String [] args)
{
CheckingAccount c = new CheckingAccount(20211127);//调用CheckingAccount类的有参构造,传过去卡号
System.out.println("存入 $500...");
c.deposit(500.00);//调用存钱方法
try
{
System.out.println("取出 $100...");
c.withdraw(100.00);//调用取钱方法
System.out.println("取出 $600...");
c.withdraw(600.00);//调用取钱方法
}catch(InsufficientFundsException e)
{
System.out.println("Sorry, but you are short $" + e.getAmount());
e.printStackTrace();//打印异常的信息
}
}
}
/*运行结果:
存入 $500...
取出 $100...
取出 $600...
Sorry, but you are short $200.0
InsufficientFundsException.InsufficientFundsException
at InsufficientFundsException.CheckingAccount.withdraw(CheckingAccount.java:30)
at InsufficientFundsException.BankDemo.main(BankDemo.java:14)
*/
在上述异常处理中有非常常用的:
在某种不合理的条件下可能出现异常时,自己手动抛出异常,并采取throws上抛的处理方式,而不是,自己抛出,自己抓的try catch的处理方式
在测试程序的main方法中,不建议再往上抛,所以采取try catch的捕捉的处理方式
IllegalNameException.java(自定义“编译时异常”)
package 异常;
//自定义“编译时异常”
public class IllegalNameException extends Exception {
//无参构造
public IllegalNameException() {
}
//有参构造
public IllegalNameException(String s) {
super(s);
}
}
/*解释有参构造中的使用super(参数):
* 源代码:
* public Exception(String message) {
super(message);
}
public Throwable(String message) {
fillInStackTrace();
detailMessage = message;
}
private String detailMessage;
public String getMassage(){
return detailMessage;
}
执行以上代码就是构造异常对象时,将new的异常对象的信息传过去
后来之所以我们可以调用异常类Exception 中的getMassage()方法 获取异常的简单描述信息
*/
Use.java(在主类中出现异常)
package 异常;
/*用户类:*/
public class User {
//注册时(判断用户名是否合法)方法
public void register(String username,String password) throws IllegalNameException {
if(username==null||username.length()<6||username.length()>14) {
throw new IllegalNameException("用户名不合法,长度必须在【6到14】之间");
}
else {
System.out.println("用户名合法,注册成功,欢迎["+username+"]");
}
}
}
/*知识点:
* 1.username==null 条件写在最前面,有助于防止出现空指针异常
* 2. 此处用到“||” 可以有短路运算逻辑,一旦第一个出现空指针异常,见假为假,直接输出null不再继续往下判断了
* 3.用到String类中的方法 length() 来获取字符串的长度*/
Test.java(测试运行阶段)
package 异常;
public class Test {
public static void main(String[] args) {
//创建用户对象
User u = new User();
try {
u.register("xiao", "2021");
} catch (IllegalNameException e) {
System.out.println(e.getMessage());
// e.printStackTrace();
}
User s = new User();
try {
s.register("xiaotong", "2021");
} catch (IllegalNameException e) {
System.out.println(e.getMessage());
}
}
/*输出结果:
用户名不合法,长度必须在【6到14】之间
用户名合法,注册成功,欢迎[xiaotong]
*/
}
子类在方法的重写时,要求不能抛出比父类更多的异常,可以更少;
在实际开发中直接将父类的方法头复制过来用就行,一般不会去分析
class Animal {
public void doSome() {
}
}
class cat extends Animal {
// public void doSome() throws Exception {
// } 因为父类没有抛出异常,但子类却抛出,编译错误
}
class Animal01 {
public void doSome()throws Exception {
}
}
class cat01 extends Animal01 {
public void doSome() throws NullPointerException {
//子类抛出了父类异常的子类异常也是可以的
}
}