Java学习笔记(三)——Java异常处理
一、错误处理
(一)Java的异常
在计算机程序运行的过程中,经常会出现错误,例如:用户输入错误、读写文件错误、网络错误、内存耗尽等等。所以在计算机程序运行的过程中,错误时不可避免的。
如果我们的方法调用出错,那么调用方该如何知道这个错误呢?
可以约定返回错误码,但是处理它却会很困难,我们需要编写大量的if-else以及switch语句来处理不同的错误。而且由于Java只允许返回一个字符,如果我们既要返回错误码又要返回正常结果,就只能使用JavaBean来进行包装,所以Java引用了一种新的机制用来表示程序中出现的错误,也就是异常。
1 . 异常的概念
(1)Java使用异常来表示错误
(2)Java的异常是class,本身带有类型信息,并且从Throwable继承
(3)异常可以在任何地方抛出
(4)异常只需要在上层捕获,与方法调用分离
在下面这段代码中,processFile可能会抛出任何异常,通过catch语句捕获异常
try {
String s = processFile("C:\\test.txt");
//OK
}catch (FileNotFoundException e) {
//file not found:
}catch (SecurityException e) {
//no read permission:
}catch (IOException e) {
//io error:
}catch (Exception e) {
//other error:
}
2 . Java的异常体系
由于Java的异常也是一种class,因此它也拥有自己的继承体系:
(1)必须捕获的异常:
Java语言规定:我们必须捕获的异常包括Exception以及它的子类,但不包括RuntimeException及其子类(Checked Exception)
(2)不需要捕获的异常:
①Error及其子类
②RuntimeException及其子类
那么Java为什么要这样规定异常的继承体系呢?
(1)Error是指发生了严重的错误,Java程序对此无能为力。例如:
OutOfMemoryError(内存耗尽),NoClassDefFoundError(虚拟机没有找到class文件),*Error…
(2)Exception是指发生了运行时的逻辑错误,Java程序应当去捕获异常并进行处理。
①某些错误是预期可能会发生的错误,例如:IOException(读写错误),NumberFormatException(用户输入格式错误)…
②某些程序是代码的编写逻辑存在BUG,例如:NullPointException,IndexOutOfBoundsException…
3 . 捕获异常
在Java中我们使用try{…}catch(){…}语句捕获异常,将可能发生异常的语句放在try{…}代码块中执行,之后用catch语句捕获对应的Exception及其子类
public static void main(String[] args) {
try {//将可能发生异常的语句放入try代码块中执行
process1();
process2();
process3();
}catch(IOException e) {//使用catch语句捕获对应的异常以及它的子类
System.out.println(e);
}
}
4 . 申明异常
如果某个方法调用抛出Checked Exception,必须捕获这个异常并进行处理,例如:
static byte[] toGBK(String s) {
try {
return s.getBytes("GBK");
}
catch (UnsupportedEncodingException e) {
System.out.println(e);
}
}
如果我们在这个方法中不去捕获这个异常,那么我们就必须通过throws来声明这个异常,通过throws声明的异常在这个异常中没有被捕获,但是它依然需要在上层进行捕获
static byte[] toGBK(String s) {
throws UnsupportedEncodingException {//申明异常暂时不进行捕获
return s.getBytes("GBK");
}
public static void main(String[] args) {
try {//在上层代码进行异常捕获
return s.getBytes("GBK");
}
catch (UnsupportedEncodingException e) {
System.out.println(e);
}
}
所以,通过throws声明异常,只是推迟了异常的处理,最终还是要在某一层捕获这个异常
由于main方法是Java程序调用的第一个代码,因此main()是最后捕获Exception的机会。如果一个Exception被抛出,而在main方法中并没有被捕获到,JVM将会发生报错,并退出程序。
RuntimeException无需强制捕获,非RuntimeException(CheckException)需强制捕获,或者用throws声明
(二)捕获异常
1 . 分配catch语句
在前面我们了解到,我们可以使用try{…}catch(){…}语句捕获异常,将可能发生异常的语句放在try{…}代码块中执行,之后用catch语句捕获对应的Exception及其子类。
我们还可以使用多个catch子句进行捕获异常
(1)每个catch捕获对应的Exception及其子类
(2)从上到下分配,分配到某个catch后不再继续匹配
例如:
public static void main(String[] args) {
try {//将可能发生异常的语句放入try代码块中执行
process1();
process2();
process3();
}catch(IOException e) {
System.out.println(e);
}catch(NumberForamtException e) {
System.out.println(e);
}
}
(3)catch的分配顺序非常重要,必须将子类写在前面
public static void main(String[] args) {
try {//将可能发生异常的语句放入try代码块中执行
process1();
process2();
process3();
}catch(UnsupportedEncodingException e) {
System.out.println(e);
}catch(IOException e) {
System.out.println(e);
}
}
2 . finally语句
如何需要编写无论错误是否发生都必须执行的语句,我们需要用到finally语句,finally语句可以保证无论错误是否发生都执行程序。
(1)finally不是必须的
(2)finally总在最后执行
public static void main(String[] args) {
try {//将可能发生异常的语句放入try代码块中执行
process1();
process2();
process3();
}catch(UnsupportedEncodingException e) {
System.out.println(e);
}catch(IOException e) {
System.out.println(e);
}finally {
System.out.println("END");
}
}
3 . mulit-catch
如果某些异常的处理逻辑相同,但是并不存在继承关系,就必须编写更多的catch子句,在这种情况下,我们可以使用或操作符“|”将多种异常写在一起,从而简化编写过程,例如:
public static void main(String[] args) {
try {
process1();
process2();
process3();
}catch(IOException | NumberForamtException e) {//合并相同逻辑的异常
System.out.println("Bad input");
}catch(Exception e) {
System.out.println("Unknown error");
}
}
(三)抛出异常
1 . 异常的传播
当某个方法抛出异常时,如果当前方法没有捕获,异常就被抛到上层调用方法,直到某个try…catch被捕获。
printStackTrace()可以打印出方法的调用栈,它对于调试错误非常有用。
2 . 如何抛出异常
在Java中,我们需要捕获异常是因为某些方法可以抛出异常。如果我们想要自行抛出异常该如何编写代码呢?
由于异常本身也是一个class,所以,当我们要抛出异常时:
(1)创建某个Exception的实例
(2)用throw语句抛出
例如:如果我们想要抛出IllegalArgumentException,只要将throw语句和创建的实例一起编写就可以了
static void process1(String s) {
throw new IllegalArgumentException();
}
3 . 转换异常
(1)如果一个方法捕获了某个异常之后,又在catch语句中抛出新的异常,就相当于把原来抛出的异常类型转换了
public static void main(String[] args) {
process1("");
}
static void process1(String s) {
try {
process2();
}catch(NullPointException e) {
threw new IllegalArgumentException();
}
}
static void process2(String s) {
threw new NullPointException();
}
但是在这时,新的异常丢失了原始的异常信息,我们只能追踪到IllegalArgumentException,而无法追踪到原来的NullPointException。
因此,我们需要把原有的异常NullPointException传入IllegalArgumentException的实例e,从而让新的Exception可以持有原始类型信息。
public static void main(String[] args) {
process1("");
}
static void process1(String s) {
try {
process2();
}catch(NullPointException e) {
threw new IllegalArgumentException(e);//将原有异常传入实例
}
}
static void process2(String s) {
threw new NullPointException();
}
(2)需要特别注意的是,在抛出异常之前,finally语句会保证执行。例如:
public static void main(String[] args) {
try{
process1("");
}catch(Exception e){//catch Exception
System.out.println("catched");//打印catched
throw new RuntimeException(e);
//在抛出新的异常之前会执行finally语句
}finally{//
System.out.println("finally");//打印出finally
}
}
static void process1(String s) {
threw new IllegalArgumentException();
}
(3)如果finally语句抛出了异常,那么catch语句将不再执行。例如:
public static void main(String[] args) {
try{
process1("");
}catch(Exception e){//catch Exception
System.out.println("catched");
throw new RuntimeException(e);
//由于在finally语句中已经抛出了异常,catch语句中的异常将不会被运行
}finally{//
System.out.println("finally");
throw new NullPointException();//在finally中抛出新的异常
}
}
static void process1(String s) {
threw new IllegalArgumentException();
(4)没有被抛出的异常称为被屏蔽的异常(suppressed exception)
由于Java的异常处理机制使得它只能传播一个异常,后抛出的异常会将前面抛出的但是没有捕获的异常覆盖掉,所以前面抛出的异常就丢失了。
4 . 如何保存异常信息
(1)用origin变量保存原始变量
(2)如果存在原始异常用addSuppressed()添加新异常
(3)如果存在原始异常或新异常,最后在finally中抛出
Exception origin = null;
try {
process1("");
}catch (Exception e) {
origin = e;
throw new RuntimeException(e);
}finally {
try {
throw new NullPointException();
}catch(Exception e) {
if(origin != null){
origin.addSuppressed(e);
}else{
origin = e;
}
}
if(origin != null){
throw origin;
}
}
5 . 如何获取所有异常信息
(1)用getSuppressed()获取所有Suppressed Exception
(2)处理Suppressed Exception要求JDK版本>=1.7
try {
somethingWrong("");
}catch(Exception e){
e.printSrackTrace();
for(Throwalbe t : e.getSuppressed()){
t.printSrackTrace();
}
}
(四)自定义异常
1 . JDK已有的异常
(1)JDK定义的常用异常:
——RuntimeException
NullPointerException
IndexOutOfBoundsException
SecurityException
IllegalArgumentException
NumberFormatException
——IOException
UnsupportedCharsetException,FileNotFoundException,SocketException
——ParseException,GeneralSecurityException,SQLException,TimeoutException
(2)当进行抛出异常时,尽量使用JDK已经定义的异常
(3)可以定义新的异常类型:
定义一个自定义的异常类型需要从合适的异常类型中派生,推荐通过RuntimeException派生,这样就不必强制去捕获异常,也不需要在方法中去申明需要抛出的异常
public class BadFileFormatException extends IOException {
}
public class UserNotFoundException extends RuntimeException {
}
(4)可以定义新的异常关系树:
①从合适的Exception中派生BaseException
②其他Exception从BaseException派生
public class BaseException extends RuntimeException {
}
public class UserNotFoundException extends BaseException {
}
public class LoginFailedException extends BaseException {
}
(5)自定义异常应当提供多种构造方法:
提供多个构造方法的目的,是为了在catch语句中,如果我们需要抛出新的BaseException,需要把原有的异常信息传入到BaseException中。
可以使用IDE根据父类快速创建构造方法。
二、断言与日志
(一)断言Assertion
1 . 断言的概念
断言是一种调试方式
(1)使用assert关键字
(2)断言条件预期为true
(3)如果运行中结果为false,那么断言失败,将会抛出AssertionError
static double abs(double d) {
return d >= 0 ? d : -d;
}
public static void main(String[] args) {
double x = abs(-123.45);
assert >= 0;
System.out.println(x);
}
(4)可以添加可选的断言消息,在断言失败时,随AssertionError一同抛出
static double abs(double d) {
return d >= 0 ? d : -d;
}
public static void main(String[] args) {
double x = abs(-123.45);
assert >= 0 : "x must >= 0";
System.out.println(x);
}
2 . 断言的特点
(1)断言失败时会抛出AssertionError导致程序直接退出
(2)对于可恢复的错误不能使用断言(此时应该抛出异常)
(3)只能在开发和测试阶段启用断言
(4)断言很少使用,更好的方法是使用单元测试
例如:我们不能用assert来判断变量arr是否为空:
void sort(int[] arr) {
assert arr != null;
}
我们应当抛出异常并在上层捕获异常:
void sort(int[] arr) {
if(x == null) {
throw new IllegalArgumentException("arr cannot be null");
}
}
3 . 启用断言
JVM默认情况是关闭断言指导的,会忽略所有的assert语句。
(1)给Java虚拟机传递-ea参数启用断言
(2)可以指定特定的类启用断言:-ea:xxxx.xxxx.xxxx.main(完整类名)
(3)可以指定特定的包启用断言:-ea:xxxx.xxxx(完整包名)
4 . 在Eclipse中启用断言
(1)选择Object中的Main.java
(2)选择Run As
(3)选择Run Configurations
(4)选择Arguments
(5)选择VM Arguments
(6)添加启动参数和完整类名-ea:xxxx.xxxx.xxxx.main
(7)保存并运行
(二)日志Logging
1 . 日志的概念
(1)取代System.out.println()语句
(2)可以设置输出样式
(3)可以设置输出级别,禁止某些级别的输出
(4)可以被重定向到文件 可以按包名控制日志级别
(5)日志可以存档,方便追踪问题
(6)可以根据配置文件调整日志,无需修改代码
2 . JDK自带的Logging
JDK内置的Logging储存在java.util.logging这个包内部,这个日志系统很少使用。
(1)如何使用JDK Logging
import java.util.logging.Level;//导入level类
import java.util.logging.Logger;//导入logger类
public class Hello {
public static void main(String[] args) {
Logger logger = Logger.getGlobal();//调用getGlobal()方法获得logger实例
logger.info("start...");//调用info()方法输出基本信息
logger.log(Level.WARNING,"warning...");//调用log()方法输出警告级别
logger.warning("start...");//调用warning()方法输出警告信息
}
}
(2)JDK Logging的日志级别
Java的JDK Logging设置了7个级别:
①SEVERE
②WARNING
③INFO(默认级别)
④CONFIG
⑤FINE
⑥FINER
⑦FINEST
如果我们设置了一个级别时(例如INFO),将只输出INFO和INFO以上的级别
(3)JDK Logging的局限性
①JVM在启动时必须读取配置文件并完成初始化
②JVM启动后无法修改配置
③需要在JVM启动时传递参数:
-Djava.util.logging.config.file=config-file-name
3 . Commons Logging
(1)Commons Logging的概念
Commons Logging是Apache创建的日志模块:
①可以挂接不同的日志系统
②可以通过配置文件指定挂接的日志系统
③可以自动搜索并使用Log4j
④若Log4j不存在则自动使用JDK Logging(JDK >= 1.4)
public class Main {
public static void main(String[] args) {
Log log = LogFactory.getLog(Main.class);//获取Log实例,传入class
logger.info("start...");调用info()方法输出基本信息
logger.warn("end...");调用warn()方法输出警告信息
}
}
(2)Commons Logging的日志级别
Commons Logging定义了6个日志级别:
①FATAL(非常严重的错误)
②ERROR(错误)
③WARNING(警告)
④INFO(默认)
⑤DEBUG(调试信息)
⑥TRACE(底层调试信息)
//在静态方法中引用Log:
public class Main {
static final Log log = LogFactory.getLog(Main.class);
}
//在实例方法中引用Log:
public class Person {
final Log log = LogFactory.getLog(getClass());
}
//在父类中实例化Log:
public abstract class Base {
protected final Log log = LogFactory.getLog(getClass());
}
(3)Commons Logging的作用
①Commons Logging是使用最广泛的日志模块
②Commons Logging的API非常简单
③Commons Logging可以自动使用其他日志模块
4 . Log4j
(1)Log4j的概念
Log4j是目前最流行的日志框架,拥有2个版本:
①1.x:Log4j
②2.x:Log4j2
Log4j是一个组件化设计的日志系统:
①Filter用来过滤哪些Log需要被输出而哪些Log不需要被输出
②Layout用来格式化Log信息
③在实际使用过程中不需要关心Log4j的内部API
(2)Commons Logging可以自动使用Log4j模块
①使用Log4j是需要把Log4j2.xml和相关jar放入classpath
②如果要更换Log4j,只需要移除Log4j.xml和相关jar
③只有扩展Log4j时,才需要引用Log4j的接口
上一篇: Java-深拷贝与浅拷贝
下一篇: Java实现带头结点的单链表