欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java学习笔记(三)——Java异常处理

程序员文章站 2024-03-21 16:58:22
...

一、错误处理

(一)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,因此它也拥有自己的继承体系:
Java学习笔记(三)——Java异常处理

(1)必须捕获的异常:
Java语言规定:我们必须捕获的异常包括Exception以及它的子类,但不包括RuntimeException及其子类(Checked Exception)
Java学习笔记(三)——Java异常处理
(2)不需要捕获的异常:
①Error及其子类
②RuntimeException及其子类
Java学习笔记(三)——Java异常处理

那么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是一个组件化设计的日志系统:
Java学习笔记(三)——Java异常处理
①Filter用来过滤哪些Log需要被输出而哪些Log不需要被输出
②Layout用来格式化Log信息
③在实际使用过程中不需要关心Log4j的内部API

(2)Commons Logging可以自动使用Log4j模块

①使用Log4j是需要把Log4j2.xml和相关jar放入classpath
②如果要更换Log4j,只需要移除Log4j.xml和相关jar
③只有扩展Log4j时,才需要引用Log4j的接口

相关标签: Java 学习心得