第3章 异常处理

为什么需要异常处理

生活中的异常: 做地铁过来上班,地铁晚点; 平时的地铁时间间隔3分钟,疫情的影响10分钟。吃早餐异常情况:没饭了。遇到异常情况怎么办? 解决异常: 做公交车、共享单车。想办法处理异常,生活继续,不能因为碰到一点点挫折就放弃人生。

什么是异常处理

异常的英文单词是exception,字面翻译就是“意外、例外”的意思,也就是非正常情况。事实上,异常本质上是程序上的错误,包括程序逻辑错误和系统错误。比如使用空的引用、数组下标越界、内存溢出错误等,这些都是意外的情况,背离我们程序本身的意图。错误在我们编写程序的过程中会经常发生,包括编译期间和运行期间的错误,在编译期间出现的错误有编译器帮助我们一起修正,然而运行期间的错误便不是编译器力所能及了,并且运行期间的错误往往是难以预料的。假若程序在运行期间出现了错误,如果置之不理,程序便会终止或直接导致系统崩溃,显然这不是我们希望看到的结果。因此,如何对运行期间出现的错误进行处理和补救呢?Java提供了异常机制来进行处理,通过异常机制来处理程序运行期间出现的错误。通过异常机制,我们可以更好地提升程序的健壮性。

简单来说,异常就是我们碰到的非正常的现象,可以通过解决异常进一步执行剩下业务;但是如果解决不恰当会影响系统的执行,甚至中断执行。程序中的异常:

package ch003;

import java.util.Scanner;

public class Demo1 {

	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		System.out.print("请输入第一个数:");
		int num1 = input.nextInt();
		System.out.print("请输入第二个数 :");
		int num2 = input.nextInt();
		int num3 = num1 / num2;
		System.out.println(num3);
		
		System.out.println("其他业务代码........");
		input.close();
	}

}

根据执行的结果可以看出: 出现异常之后,后续的业务代码没有执行,程序直接中断执行。如何解决异常保持程序出现异常之后可以正常执行?这就是java中的异常处理机制。

如何处理异常

异常处理机制:

①java中把所有遇到的异常用面向对象的方式处理, 把异常封装成一个对象,假如我们编写一个类描述一个异常,问题是: 这个类如何编写?

public class Error{
    private String address;//那个位置出现异常
    private Date  date;//闯红灯的时间
    private String law;//违反的规定
    
    public void showMessage(){
        //显示异常信息
    }
}

在java中有一个Exception来描述所有的异常信息,异常的父类: NullPointerException是一个具体的异常。

②整个异常处理机制包括: try—catch—finally throw和throws共计五个关键字。

try: 表示的可能出现异常的代码块
catch: 捕获异常, 路口的摄像头
finally: 不管异常是否出现都要执行的代码: 继续生活
throw: 手动抛出一个异常,自己遇到问题,抛出给别人;宝马车被盗,自己不能处理这个异常,抛给 警察叔叔。
throws: 声明一个方法抛出的异常,别人用你的电脑: 这个电脑经常死机,别人用你的车: 车刹车不灵
在这里插入图片描述

public class Throwable implements Serializable {
    
}

包括两个直接子类: Error是无法处理的异常,比如OutOfMemoryError,一般发生这种异常,JVM会选择终止程序。因此我们编写程序时不需要关心这类异常,也没有办法通过异常处理机制解决的。Exception是异常,所有异常的祖先类,可以通过异常处理机制解决的。

Exception又可以分为两大类: RuntimeException运行期异常,代码在执行过程中出现的异常,譬如NullPointerException和ArraysIndexOutOfBoundException。IOException和SQLException这些在编写代码的时候就会出现异常,非运行期异常,编译器异常,java编译器强制程序员必须进行捕获处理,比如常见的IOExeption和SQLException。对于非运行时异常如果不进行捕获或者抛出声明处理,编译都不会通过。

try {
    FileOutputStream fos = new FileOutputStream("xxx");
} catch (FileNotFoundException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

检查型异常(Checked Exception)与非检查型异常(Unchecked Exception)区别?

  • 所有的检查性异常都继承自java.lang.Exception;所有的非检查性异常都继承自java.lang.RuntimeEx ception。
  • 检查性异常和非检查性异常最主要的区别在于其处理异常的方式:检查性异常必须使用try catch或者throws等关键字进行处理,否则编译器会报错;非检查性异常一般是程序代码写的不够严谨而导致的问题,可以通过修改代码来规避。
  • 常见的运行时异常:空指针异常(NullPointerException)、除零异常(ArithmeticException)、数组越界异常(ArrayIndexOutOfBoundsException)等;
  • 常见的检查性异常:输入输出异常(IOException)、文件不存在异常(FileNotFoundException)、SQL语句异常(SQLException)等。

try–catch

①正常执行

package ch003;

import java.util.Scanner;

public class Demo2 {

	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		try {
			System.out.print("请输入第一个数:");
			int num1 = input.nextInt();
			System.out.print("请输入第二个数 :");
			int num2 = input.nextInt();
			int num3 = num1 / num2;
			System.out.println(num3);
		} catch (Exception e) {
			e.printStackTrace();//打印堆栈信息
			System.err.println("出现异常信息:"+e.getMessage());
		}
		
		System.out.println("其他业务代码........");
		input.close();
	}

}

执行结果:

请输入第一个数:100
请输入第二个数 :10
10
其他业务代码........

代码正常执行,没有出现异常,catch代码块不执行的。

②出现异常、成功捕获

package ch003;

import java.util.Scanner;

public class Demo2 {

	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		try {
			System.out.print("请输入第一个数:");
			int num1 = input.nextInt();
			System.out.print("请输入第二个数 :");
			int num2 = input.nextInt();
			int num3 = num1 / num2;
			System.out.println(num3);
		} catch (ArithmeticException e) {
			e.printStackTrace();//打印堆栈信息
			System.err.println("出现异常信息:"+e.getMessage());
		}
		
		System.out.println("其他业务代码........");
		input.close();
	}

}

执行结果:

请输入第一个数:100
请输入第二个数 :0
java.lang.ArithmeticException: / by zero
其他业务代码........
	at ch003.Demo2.main(Demo2.java:14)
出现异常信息:/ by zero

出现算术异常,执行了catch代码块,异常处理成功;后续业务也得到执行。

③出现异常、捕获失败

package ch003;

import java.util.Scanner;

public class Demo2 {

	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		try {
			System.out.print("请输入第一个数:");
			int num1 = input.nextInt();
			System.out.print("请输入第二个数 :");
			int num2 = input.nextInt();
			int num3 = num1 / num2;
			System.out.println(num3);
		} catch (ArithmeticException e) {
			e.printStackTrace();//打印堆栈信息
			System.err.println("出现异常信息:"+e.getMessage());
		}
		
		System.out.println("其他业务代码........");
		input.close();
	}

}

执行结果:

请输入第一个数:xxxx
Exception in thread "main" java.util.InputMismatchException
	at java.util.Scanner.throwFor(Unknown Source)
	at java.util.Scanner.next(Unknown Source)
	at java.util.Scanner.nextInt(Unknown Source)
	at java.util.Scanner.nextInt(Unknown Source)
	at ch003.Demo2.main(Demo2.java:11)

出现的异常和捕获的异常类型不匹配,系统直接终止执行。闯红灯: 判刑50年,这个处理就不匹配;你还去不去上班,后续业务就不会执行。

多个catch

package ch003;

import java.util.InputMismatchException;
import java.util.Scanner;

public class Demo2 {

	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		try {
			System.out.print("请输入第一个数:");
			int num1 = input.nextInt();
			System.out.print("请输入第二个数 :");
			int num2 = input.nextInt();
			int num3 = num1 / num2;
			System.out.println(num3);
		}catch (ArithmeticException e) {
			e.printStackTrace();//打印堆栈信息
			System.err.println("算术异常信息:"+e.getMessage());
		}catch (InputMismatchException e) {
			e.printStackTrace();//打印堆栈信息
			System.err.println("输入不匹配异常:"+e.getMessage());
		}catch (Exception e) {
			e.printStackTrace();//打印堆栈信息
			System.err.println("未知异常信息:"+e.getMessage());
		}
		
		System.out.println("其他业务代码........");
		input.close();
	}
}

try可以跟多个catch代码块; catch的顺序: 从小到大,从具体类到一般,从子类到父类。另外一种简化的写法:

package ch003;

import java.util.InputMismatchException;
import java.util.Scanner;

public class Demo3 {

	public static void main(String[] args)   {
		Scanner input = new Scanner(System.in);
		try {
			System.out.print("请输入第一个数:");
			int num1 = input.nextInt();
			System.out.print("请输入第二个数 :");
			int num2 = input.nextInt();
			int num3 = num1 / num2;
			System.out.println(num3);
		}catch (ArithmeticException|InputMismatchException|NullPointerException e) {
			e.printStackTrace();//打印堆栈信息
			System.err.println("异常信息:"+e.getMessage());
		}catch (Exception e) {
			System.err.println("异常信息:"+e.getMessage());
		}
		
		System.out.println("其他业务代码........");
		input.close();
	}

}

后面跟一个Exception是为了保证能够处理未知异常,catch捕获了已知的异常,有个兜底的处理。

异常方法

下面的列表是 Throwable 类的主要方法:

序号 方法及说明
1 public String getMessage()
返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了。
2 public Throwable getCause()
返回一个Throwable 对象代表异常原因。
3 public String toString()
使用getMessage()的结果返回类的串级名字。
4 public void printStackTrace()
打印toString()结果和栈层次到System.err,即错误输出流。
5 public StackTraceElement [] getStackTrace()
返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。
6 public Throwable fillInStackTrace()
用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中。

try-catch-finally

finally表示最终的,不管是否出现异常都会执行的代码, IO流的关闭,数据库连接的关闭。

package ch003;

import java.util.Scanner;

public class Demo4 {

	public static void main(String[] args)   {
		Scanner input = new Scanner(System.in);
		try {
			System.out.print("请输入第一个数:");
			int num1 = input.nextInt();
			System.out.print("请输入第二个数 :");
			int num2 = input.nextInt();
			int num3 = num1 / num2;
			System.out.println(num3);
		}catch (Exception e) {
			System.err.println("异常信息:"+e.getMessage());
		}finally {
			System.out.println("===finally===");
		}
		
		System.out.println("其他业务代码........");
		input.close();
	}

}

没有异常:

请输入第一个数:100
请输入第二个数 :50
2
===finally===
其他业务代码........

出现异常:

请输入第一个数:100
请输入第二个数 :0
异常信息:/ by zero
===finally===
其他业务代码........

finally块和return

  • 首先一个不容易理解的事实:在 try块中即便有return,break,continue等改变执行流的语句,finally也会执行
  • finally中的return 会覆盖 try 或者catch中的返回值,如果只修改变量的值没有return语句,不会改变变量的值。
  • finally中的return或异常会抑制(消灭)前面try或者catch块中的异常。
package com.hanker.oop7;
public class Demo1 {
	public static void main(String[] args) {
		System.out.println(add(1,2));
	}
	public static int add(int a,int b) {
		int c = 0;
		try {
			c = a + b;
			return c;
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}finally {
			c = 1000;//2.没有返回语句不会改变变量c的值,还是返回 3
			System.out.println("finally");
			//return c;//1.有返回语句会改变返回值
		}
	}
}

总结: 整体异常处理的流程
在这里插入图片描述

throw和throws

throw是手动抛出一个异常信息,一般会用于程序出现某种逻辑时程序员主动抛出某种特定类型的异常。throw只会出现在方法体中,当方法在执行过程中遇到异常情况时,将异常信息封装为异常对象,然后throw出去。throw关键字的一个非常重要的作用就是异常类型的转换。

package ch003;

public class Student {

	private String sex;

	public String getSex() {
		return sex;
	}

	public void setSex(String sex) throws Exception {
		if( sex.equals("男") || sex.equals("女") ) {
			this.sex = sex;
		}else {
			System.out.println("输入错误");
			throw new Exception("性别只能是男或女");
		}
	}
}

我们通过throw抛出一个异常信息,表示该方法有可能出现异常,问题来了: 别人在调用你的方法时如何了解你是否抛出异常了? 所以throw会配合另一个关键字: throws。throws在方法声明的地方指定抛出的异常信息,当别的同事看到你的方法声明的时候就可以了解你的方法出现什么异常,一般不会到你的代码里阅读。再次强调:throws出现在方法的声明中,表示该方法可能会抛出的异常,然后交给上层调用它的方法程序处理,允许throws后面跟着多个异常类型.

声明方法抛出异常带来的问题: 方法调用者如何处理你抛出的异常;两个办法:

①继续抛出,抛给上一级

package ch003;

public class StudentTest {
	//main方法抛给jvm
	public static void main(String[] args) throws Exception {
		Student s = new Student();
		s.setSex("xxx");
	}
}

②处理异常,try–catch

package ch003;

public class StudentTest {
	public static void main(String[] args) {
		Student s = new Student();
		try {
			s.setSex("x");
		} catch (Exception e) {
			System.out.println("=出现异常="+e.getMessage());
		}
	}
}

自定义异常

上面我们抛出的异常用的类型是什么?Exception类型的,但是它是所有异常的跟类,可以理解为抽象类(但不是抽象类)描述的是所有的异常类型,不是具体的异常类。所以我们可以自定义一个异常类型,来描述性别的异常信息。如何自定义异常类型?

按照国际惯例,自定义的异常应该总是包含如下的构造函数:

一个无参构造函数
一个带有String参数的构造函数,并传递给父类的构造函数。
一个带有String参数和Throwable参数,并都传递给父类构造函数
一个带有Throwable 参数的构造函数,并传递给父类的构造函数。
下面是IOException类的完整源代码,可以借鉴。

package java.io;
 
public class IOException extends Exception {
    static final long serialVersionUID = 7818375828146090155L;
 
    public IOException() {
		super();
    }
 
    public IOException(String message) {
		super(message);
    }
 
    public IOException(String message, Throwable cause) {
        super(message, cause);
    }
 
    public IOException(Throwable cause) {
        super(cause);
    }
}

自定义的异常

package ch003;

public class SexException extends Exception {
    
    public SexException() {
		super();
    }
 
    public SexException(String message) {
		super(message);
    }
 
    public SexException(String message, Throwable cause) {
        super(message, cause);
    }
 
    public SexException(Throwable cause) {
        super(cause);
    }
}

使用自定义的异常类型:

package ch003;

public class Student {
	private String sex;
	public String getSex() {
		return sex;
	}
	public void setSex(String sex) throws SexException {
		if( sex.equals("男") || sex.equals("女") ) {
			this.sex = sex;
		}else {
			System.out.println("输入错误");
			throw new SexException("性别只能是男或女");
		}
	}
}

测试类:

package ch003;

public class StudentTest {
	//main方法抛给jvm
	public static void main(String[] args)  {
		Student s = new Student();
		try {
			s.setSex("xxx");
		} catch (SexException e) {
			e.printStackTrace();
		}
	}
}

方法重写如何抛出异常

子类重写父类方法的时候,如何确定异常抛出声明的类型。下面是三点原则:

(1)父类的方法没有声明异常,子类在重写该方法的时候不能声明异常;
(2)如果父类的方法声明一个异常exception1,则子类在重写该方法的时候声明的异常不能是exception1的父类;
(3)如果父类的方法声明的异常类型只有非运行时异常(运行时异常),则子类在重写该方法的时候声明的异常也只能有非运行时异常(运行时异常),不能含有运行时异常(非运行时异常)。
在这里插入图片描述

异常处理和设计的几个建议

1.只在必要使用异常的地方才使用异常,不要用异常去控制程序的流程
2.切忌使用空catch块
3.检查异常和非检查异常的选择,建议尽量避免检查异常的使用,如果确实该异常情况的出现很普遍,需要提醒调用者注意处理的话,就使用检查异常;否则使用非检查异常。
4.注意catch块的顺序
5.不要将提供给用户看的信息放在异常信息里
比如下面这段代码:

public class Main {
    public static void main(String[] args) {
        try {
            String user = null;
            String pwd = null;
            login(user,pwd);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
         
    }
     
    public static void login(String user,String pwd) {
        if(user==null||pwd==null)
            throw new NullPointerException("用户名或者密码为空");
        //...
    }
}

6.避免多次在日志信息中记录同一个异常
7.异常处理尽量放在高层进行
8.在finally中释放资源
如果有使用文件读取、网络操作以及数据库操作等,记得在finally中释放资源。这样不仅会使得程序占用更少的资源,也会避免不必要的由于资源未释放而发生的异常情况。

日志处理

为什么需要日志系统,核心是可以跟踪业务代码的执行,特别是系统出现问题,我们可以跟据日志找到出现问题的 类甚至第几行;有了日志我们就可以快速定位系统问题,进而可以帮助我们解决问题。视频监控也是一种日志形式,但是系统中都是日志文件: ELK分析日志: ElasticSearch+LogBack+Kibana; 日志处理有很多框架: jdk自带有日志处理的类库,常见的Log4j,

①添加log4j的jar包

在这里插入图片描述

②编写属性文件

就是日志的输出级别,输出的目的地,输出的格式:

### 把日志信息输出到控制台  ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n

### 把日志信息输出到文件 ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l  %m%n

### 设置优先级别、以及输出源 ###
log4j.rootLogger=debug, stdout,file

注意文件位置:直接放到src下面,不是包里面。
在这里插入图片描述

属性文件的解释:

stdout: standard out 标准输出流, 就是输出到控制台

log4j.appender.file.File=mylog.log 指定输出的日志的文件名称

%d{yyyy-MM-dd HH:mm:ss} %l %m%n

%d 表示日期占位符, 大括号里是格式

%l 表示location第几行输出的日志

%m表示输出日志信息 message

%n表示输出换行

日志的级别:

debug: 最低级别,调试日志

info: 一般信息日志

warn: 警告日志信息

error: 错误日志信息

fatal: 致命错误日志信息

debug < info < warn < error < fatal 如果指定warn级别则输出的包括 debug,info,warn

log4j.rootLogger=error, stdout,file 输出的日志是 >= error级别的。

③在程序中编写日志代码

package ch003;

import org.apache.log4j.Logger;

public class Student {

	private Logger log = Logger.getLogger(Student.class);
	private String sex;
	
	public Student() {
		log.debug("创建Student对象");
	}

	public String getSex() {
		return sex;
	}
	//声明抛出
	public void setSex(String sex) throws SexException {
		log.info("设置性别: "+ sex);
		if( sex.equals("男") || sex.equals("女") ) {
			this.sex = sex;
			log.info("成功设置性别");
		}else {
			log.error("输入有误,性别只能是男或女.....");
			//手动抛出
			throw new SexException("性别只能是男或女");
		}
	}
}

测试类

package ch003;

import org.apache.log4j.Logger;

public class StudentTest {
	private static Logger log = Logger.getLogger(StudentTest.class);
	//main方法抛给jvm
	public static void main(String[] args)  {
		log.debug("测试类开始执行....");
		Student s = new Student();
		try {
			s.setSex("xxx");
		} catch (SexException e) {
			e.printStackTrace();
		}
	}
}

注意查看文件之前选择项目,右键刷新即可看到日志文件。
在这里插入图片描述

发布了91 篇原创文章 · 获赞 43 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/kongfanyu/article/details/105684044