Java笔记七——异常处理

Java的异常

错误类型

  • 用户造成的:输入了不匹配的数据类型、程序想要读取文件时用户已经删除了
  • 随机出现、不可避免:断网无法连接服务器、没有打印机、内存耗尽
    Java语言内置了一套异常处理机制,总是用异常来处理错误
    异常是一种class,可以在任何地方跑出,只需要在上层捕获,异常继承机制如下:
    在这里插入图片描述

Error

Error表示严重错误

  • OutOfMemoryError:内存耗尽
  • NoClassDefFoundError:无法加载某个Class
  • StackOverflowError:栈溢出

Exception

Exception表示运行时错误
有些异常是编写程序时候一定要处理的一部分(可以预见的异常)例如

  • NumberFormatException:数值类型的格式错误
  • FileNotFoundException:未找到文件
  • SocketException:读取网络失败
    有些异常时程序逻辑编写不对(程序员的锅):好好改代码吧!例如:
  • NullPointerException:对某个null对象调用方法或者字段
  • IndexOutOfBoundsException:数组索引越界

Exception分为两大类:

  • RuntimeException以及它的子类——不强制捕获
    RuntimeException表示运行时异常:JVM正常运行期间可以抛出(throw)的异常。未检查的异常(不强制try-catch,编译也可以通过)
  • 非RuntimeException(包括IOException等)——必须捕获的异常(Checked Exception)

捕获异常

使用try…catch语句,可能发生的异常放在try块中,catch块捕获对应的Exception及其子类

// try...catch
import java.io.UnsupportedEncodingException;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        byte[] bs = toGBK("中文");
        System.out.println(Arrays.toString(bs));
    }

    static byte[] toGBK(String s) {
        try {
            // 用指定编码转换String为byte[]:
            return s.getBytes("GBK");
        } catch (UnsupportedEncodingException e) {
            // 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException:
            System.out.println(e); // 打印异常信息
            return s.getBytes(); // 尝试使用用默认编码
        }
    }
}

很多方法如getBytes在定义的时候使用throws ***表示该方法可能抛出的异常类型。调用方在调用的时候必须强制捕获(或者继续throws,在更高的调用层捕获),不处理的话编译不会通过的。
所有异常都可以调用printStackTrace()方法打印异常栈
···
static byte[] toGBK(String s) {
try {
return s.getBytes(“GBK”);
} catch (UnsupportedEncodingException e) {
// 先记下来再说:
e.printStackTrace();
}
return null;
···
小结
Java使用异常来表示错误,并通过try … catch捕获异常;
Java的异常是class,并且从Throwable继承;
Error是无需捕获的严重错误,Exception是应该捕获的可处理的错误;
RuntimeException无需强制捕获,非RuntimeException(Checked Exception)需强制捕获,或者用throws声明;
不推荐捕获了异常但不进行任何处理。

多catch语句

每个catch分别捕获对应的Exception及其子类,JVM捕获到异常后,从上到下匹配catch语句,匹配之后就执行catch块,多catch语句只有一个被执行,所以catch语句顺序很重要,应该按照子类在前来书写

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (UnsupportedEncodingException e) {
        System.out.println("Bad encoding");
    } catch (IOException e) {
        System.out.println("IO error");
    }
}

UnsupportedEncodingException是IOException的子类

finallly语句

finally语句块有无错误都会被执行(finally语句非必须,总是最后执行)
try-catch-finally
某些情况下,可以没有catch,只使用try … finally结构。例如:

void process(String file) throws IOException {
    try {
        ...
    } finally {
        System.out.println("END");
    }
}

方法throws了异常,等到上层捕获,所以这里可以不写catch块
可以一次捕获多个异常catch (IOException | NumberFormatException e)
如果处理的代码一样的话

抛出异常

异常的传播

某个方法抛出异常,当前方法没有捕获就会被抛到更高的调用层直到遇到某个try-catch被捕获为止:

public class Main {
    public static void main(String[] args) {
        try {
            process1();
        } catch (Exception e) {
            e.printStackTrace();//打印异常栈信息
        }
    }

    static void process1() {
        process2();
    }

    static void process2() {
        Integer.parseInt(null); // 会抛出NumberFormatException
    }
}

抛出异常

void process2(String s) {
    if (s==null) {
        throw new NullPointerException();
    }
}

catch某个异常后,还可以在catch子句中抛出新异常(留给上头烦恼啦),相当于异常类型“转换”了:

void process1(String s) {
    try {
        process2();
    } catch (NullPointerException e) {
        throw new IllegalArgumentException();
    }
}

void process2(String s) {
    if (s==null) {
        throw new NullPointerException();
    }
}

————————————我是一条分割线—————————————————
这部分内容不太会,等之后再来填坑吧
调用printStackTrace()可以打印异常的传播栈,对于调试非常有用;
捕获异常并再次抛出新的异常时,应该持有原始异常信息;
通常不要在finally中抛出异常。如果在finally中抛出异常,应该原始异常加入到原有异常中。调用方可通过Throwable.getSuppressed()获取所有添加的Suppressed Exception。

自定义异常

在这里插入图片描述
抛出异常时,尽量复用JDK已定义的异常类型;
自定义异常体系时,推荐从RuntimeException派生“根异常”,再派生出业务异常;
自定义异常时,应该提供多种构造方法。

NullPointerException

NullPointerException即空指针异常,null对象调用其方法或者字段就会产生NullPointerException。
好的编码习惯可以减少这个异常

private String name = "";//使用空字符串初始化而不是默认的null

Java 14开始,如果产生了NullPointerException,JVM会给出详细信息说明null对象是谁

使用断言

assert关键字实现断言,断言是一种调试方式
语句assert x >= 0;即为断言,断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError。
使用assert语句时,还可以添加一个可选的断言消息:
assert x >= 0 : “x must >= 0”;
这样,断言失败的时候,AssertionError会带上消息x must >= 0,更加便于调试。

实际开发中,很少使用断言。更好的方法是编写单元测试,后续我们会讲解JUnit的使用。

使用JDK Logging

日志是为了替代System.out.println(),可以定义格式,重定向到文件等;

日志可以存档,便于追踪问题;

日志记录可以按级别分类,便于打开或关闭某些级别;

可以根据配置文件调整日志,无需修改代码;

Java标准库提供了java.util.logging来实现日志功能。

Java标准库内置的Logging使用并不是非常广泛

使用Commons Logging

默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging
使用Commons Logging只需要和两个类打交道,并且只有两步:
第一步,通过LogFactory获取Log类的实例; 第二步,使用Log实例的方法打日志。
示例代码如下:

public class Main {
    public static void main(String[] args) {
        Log log = LogFactory.getLog(Main.class);
        log.info("start...");
        log.warn("end.");
    }
}

Commons Logging是第三方库,必须先下载解压
日后填坑
Commons Logging是使用最广泛的日志模块;
Commons Logging的API非常简单;
Commons Logging可以自动检测并使用其他日志模块。

使用Log4j

在这里插入图片描述
以XML配置为例,使用Log4j的时候,我们把一个log4j2.xml的文件放到classpath下就可以让Log4j读取配置文件并按照我们的配置来输出日志。

在开发阶段,始终使用Commons Logging接口来写入日志,并且开发阶段无需引入Log4j。如果需要把日志写入文件, 只需要把正确的配置文件和Log4j相关的jar包放入classpath,就可以自动把日志切换成使用Log4j写入,无需修改任何代码。

小结
通过Commons Logging实现日志,不需要修改代码即可使用Log4j;

使用Log4j只需要把log4j2.xml和相关jar放入classpath;

如果要更换Log4j,只需要移除log4j2.xml和相关jar;

只有扩展Log4j时,才需要引用Log4j的接口(例如,将日志加密写入数据库的功能,需要自己开发)。

使用SLF4J和Logback

Commons Logging和Log4j,一个负责充当日志API,一个负责实现日志底层,搭配使用非常便于开发。
如何使用SLF4J?它的接口实际上和Commons Logging几乎一模一样:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class Main {
    final Logger logger = LoggerFactory.getLogger(getClass());
}

从目前的趋势来看,越来越多的开源项目从Commons Logging加Log4j转向了SLF4J加Logback。


版权声明:本文为weixin_41005443原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。