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。