文章目录
- 一、异常的概念与体系结构
- 1.异常的概念
- 2.异常的体系结构
- 3.异常的分类
- 二、异常的处理
- 1.防御式编程
- 2. 异常的抛出
- 3.异常的捕获
- 4.异常处理流程总结
- 三、自定义异常类
一、异常的概念与体系结构
1.异常的概念
在生活中,当我们发现朋友表现出不舒服的情况,出于关心,我们都会问是不是病了,要不要去看医生。
程序也是如此,程序员是一群办事比较严谨的人,在日常开发之中,绞尽脑汁将代码尽可能写的尽善尽美,但是在运行过程中,常常会出现各种奇奇怪怪的问题。 例如:数据格式不对、网络不通畅,内存警报等。
在 Java 中,程序在执行过程中出现的不正常行为称之为异常。
通常遇到的情况如下:
- 算数异常
System.out.println(10/0);
- 数组越界异常
int[] array = {1,2,3};
System.out.println(array[4]);
从上述的部分异常中不难看出,java中的不同类型的异常,都有与之对应的类来进行描述。
2.异常的体系结构
异常的种类繁多,为了对不同的异常或者错误进行更好的分类管理,Java 内部维护了一个异常的体系结构:
- Throwable:是异常体系的顶层类,其派生出两个重要的子类,Error 和 Exception。
- Error:指的是 Java 虚拟机无法解决的严重问题,比如:**JVM的内部错误、资源耗尽等,**典型代表:Stack OverflowError 和 OutOfMemoryError,一旦发生回力乏术。
- Exception:异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。就是我们平时说的异常。
3.异常的分类
- 编译时异常
在程序编译时发生的异常,称之为编译时的异常,也称之为受检查异常。
private String name;
private String gender;
int age;
public abnormal clone(){
return (abnormal)super.clone();
}
- 运行时异常
在程序执行期间发生的异常,称之为运行时异常,也称之为非受检查异常。
注:编译时出现的语法性错误,不能称之为异常。
二、异常的处理
1.防御式编程
- LBYL: Look Before You Leap. 在操作之前就做充分的检查. 即:事前防御型
代码示例:
boolean ret = false;
ret = 登陆游戏();
if (!ret) {
处理登陆游戏错误;
return;
}
ret = 开始匹配();
if (!ret) {
处理匹配错误;
return;
}
ret = 游戏确认();
if (!ret) {
处理游戏确认错误;
return;
}
**缺陷:**上述代码中不难发现,正常的程序流程和错误处理的代码混和在了一起,代码整体的可读性下降。
- EAFP: It’s Easier to Ask Forgiveness than Permission. “事后获取原谅比事前获取许可更容易”. 也就是先操作, 遇到问题再处理. 即:事后认错型
代码示例:
try {
登陆游戏();
开始匹配();
游戏确认();
} catch (登陆游戏异常) {
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
} catch (游戏确认异常) {
处理游戏确认异常;
}
**优势:**正常流程和错误流程是分离开来的,程序员更关注正常的流程,代码更加清晰,更加容易理解代码。
在 Java 中,异常处理的5各主要关键字:throw、try、catch、final、throws.
2. 异常的抛出
在 Java 中,可以借助 throw 关键字,抛出一个指定的异常对象,并将错误信息告知调用者。
语法如下:
- throw new XXXException(“异常产生的原因”)
代码示例:
public static int getElement(int[] array,int index){
if(array==null){
throw new NullPointerException("传递的数组为null");
}
if(index<0 || index >= array.length){
throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
}
return array[index];
}
public static void main(String[] args) {
int[] array = {1,2,3};
getElement(array,3);
}
注:
- throw 必须写在方法体内部。
- 爆出的对象必须是 Exception 或者 Exception 的子类对象。
- 如果抛出的是 RunTimeException 或者 RunTimeException 的子类,这无需处理可以交给 JVM 来处理。
- 如果抛出的是编译时的异常,用户必须处理,否则无法通过编译。
- 异常一旦抛出,其后面的代码就不会执行。
3.异常的捕获
异常的捕获,即异常的具体处理方式,主要有两种:异常声明 throws 以及 try-catch 捕获处理。
-
异常声明 throws
当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助 throws 将异常抛给方法的调用者来处理。即,当前方法不处理异常,提醒方法的调用者处理异常。
代码实现:
需求,加载指定的文件 config.ini.
class Config {
File file;
public void OpenConfig(String filename) throws IOException{
if(!filename.endsWith(".ini")){
//判断如果是ini文件返回 true
throw new IOException("文件不是.ini文件");
}
if(!filename.equals("config.ini")){
//判断文件夹名称是否一致,一致返回 true
throw new FileNotFoundException("配置文件名字不对");
}
// 打开文件
}
public void readConfig(){
}
}
main方法:
情况1:文件准确无误
public static void main(String[] args) throws IOException {
Config config = new Config();
config.OpenConfig("config.ini");
}
情况 2:文件后缀有误
config.OpenConfig("config.zip");
情况 3:文件名称有误
config.OpenConfig("another.ini");
注:
- throws必须跟在方法的参数列表之后
- 声明的异常必须是 Exception 或者 Exception的子类。
- 调用声明抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用 throws 抛出。
2.try-catch 捕获并处理
代码实现
需求:读取配置文件,如果配置文件不是指定名字,抛出异常,调用者进行异常处理。
class Config {
File file;
public void OpenConfig(String filename) throws IOException{
if(!filename.endsWith(".ini")){
throw new IOException("文件不是.ini文件");
}
// 打开文件
}
public void readConfig(){
}
}
public static void main(String[] args) {
Config config = new Config();
try{
config.OpenConfig("config.txt");
System.out.println("文件打开成功");
}catch(IOException e){
e.printStackTrace(); //打印全面的异常信息
}
//一旦异常被捕获处理,后续代码会执行
System.out.println("后续代码");
}
注意事项:
- try 块内的异常抛出后未被处理,之后的代码就不会被执行。
- 如果抛出的异常类型与 catch 时的异常类型不匹配,那么异常将不会被成功捕获。
public static void main(String[] args) {
try{
int[] array = {0,1,2};
System.out.println(array[3]); // 此处会抛出数组越界异常
}catch(NullPointerException e){
e.printStackTrace(); //捕获时捕获的为空指针异常,真正的异常无法捕获到
}
System.out.println("后续代码");
}
- try 中可能会抛出很多个不同的异常,则必须要用多个 catch 来捕获—— 即,多种异常,多次捕获。
public static void main(String[] args) {
int[] arr = {1,2,3};
try{
System.out.println("before");
System.out.println(arr[100]);
System.out.println("after");
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("这是数组下标异常");
e.printStackTrace();
}catch (NullPointerException e){
System.out.println("这是空指针异常");
e.printStackTrace();
}
System.out.println("after try catch");
}
- 如果在捕获异常的类中有父子关系,一定要子类异常在前 catch,父类异常在后 catch,否则语法错误!
public static void main(String[] args) {
int[] arr = {1,2,3};
try{
System.out.println("before");
arr = null;
System.out.println("after");
}catch (Exception e){ //捕获所有异常
e.printStackTrace();
}catch (NullPointerException e){ //已经被前面捕获
e.printStackTrace();
}
}
总结:由于 Exception 类是所有异常类的父类. 因此可以用这个类型表示捕捉所有异常.
- finally关键字
在我们写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源: 网络连接、数据库连接等。在程序正常或者异常退出时,必须要对资源进行回收。另外,因为异常引发的程序跳转,可能会导致有的语句执行不到, finally 就是用来解决这个问题。
语法格式:
try{
// 可能会发生异常的代码
}catch(异常类型 e){
// 对捕获到的异常进行处理
}finally{
// 此处的语句无论是否发生异常,都会被执行到
}
// 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
public static void main(String[] args) {
try{
int[] arr = {1,2,3};
arr[0] = 10;
System.out.println(arr[100]);
}catch(Exception e){
e.printStackTrace();
}finally {
System.out.println("finally语句后的代码实现");
}
System.out.println("如果没有抛出异常,或者异常被处理了,try-catch后的代码也会执行");
}
}
这里会有一个问题:既然 finally 和 try-catch-finally 后的代码都会执行,呢为啥还要有 finally 呢?
举例解释,需求:实现 getData 方法,内部输入一个整形数字,然后将改数字返回,并且在 main 方法中的进行打印。
代码实现:
class TestFinally{
public static int getData(){
Scanner sc = null;
try {
sc = new Scanner(System.in);
int data = sc.nextInt();
return data;
}catch (InputMismatchException e){
e.printStackTrace();
}finally {
System.out.println("finally中的代码");
}
System.out.println("try-catch-finally之后代码");
if(null != sc){
sc.close();
}
return 0;
}
}
public static void main(String[] args) {
TestFinally testFinally = new TestFinally();
int data = testFinally.getData();
System.out.println(data);
}
正常输入时:
非正常输入:
从上面的程序我们不难看出,如果输入正确,成功接收程序后就会返回,try-catch-finally之后的代码根本就没有执行,即,sc.close() 就没有执行,输入流没有被释放,会造成资源的泄露。
注意: finally 中的代码一定会执行,一般情况下 finally 中的代码会用来做一些资源清理的工作。
4.异常处理流程总结
- 程序先执行 try 中的代码
- 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配.
- 如果找到匹配的异常类型, 就会执行 catch 中的代码
- 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.
- 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).
- 如果上层调用者也没有处理的了异常, 就继续向上传递.
- 一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止.
三、自定义异常类
在 Java 中虽然已经内置了丰富的异常类,但是,在我们的日常开发中所遇到的一些异常,并不能被完全覆盖到,此时我们就需要创建异常类来维护我们实际情况的代码。
举例:实现一个用户登录系统。
- 首先,先定义两个异常类
// 密码错误时的异常类
public class PasswordException extends Exception{
public PasswordException(String massage){
super(massage); //这里的super类是用来改变异常输出语句
}
}
// 用户名错误是的异常类
public class UserNameException extends Exception{
public UserNameException(String massage){
super(massage);
}
}
class Login{
// 内部设定好用户名、密码
private String userName = "admin";
private String password = "123456";
public void logins(String UserName,String Password) throws UserNameException,PasswordException{
if (!UserName.equals(userName)){
// 修改捕获错误时的报错原因
throw new UserNameException("用户名错误");
}
if (!Password.equals(password)){
throw new PasswordException("密码错误");
}
System.out.println("登录成功");
}
}
public static void main(String[] args) {
Login login = new Login();
try{
login.logins("admin","123456");
}catch(UserNameException e){
e.printStackTrace();
}catch (PasswordException e){
e.printStackTrace();
}
}
情况展现:
- 正常登录
- 用户名错误
- 密码错误
注意事项
- 自定义异常通常会继承自 Exception 或者 RuntimeException
- 继承自 Exception 的异常默认是受查异常
- 继承自 RuntimeException 的异常默认是非受查异常
到此, 文章结束, 如有不足, 欢迎提出. 如有错误, 欢迎指正!