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文件),StackOverflowError…
(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的接口

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值