Java的异常分为两种,一种是运行时异常(RuntimeException),一种是非运行异常也叫检查式异常(CheckedException)。
1、运行时异常不需要程序员去处理,当异常出现时,JVM会帮助处理。常见的运行时异常有:
ClassCastException(类转换异常)
ClassNotFoundException
IndexOutOfBoundsException(数组越界异常)
NullPointerException(空指针异常)
ArrayStoreException(数组存储异常,即数组存储类型不一致)
还有IO操作的BufferOverflowException异常
2、非运行异常需要程序员手动去捕获或者抛出异常进行显示的处理,因为Java认为Checked异常都是可以被修复的异常。常见的异常有:
IOException
SqlException
三、异常的捕获和处理
Java异常处理的五个关键字:try、catch、finally、throw、throws
不能直接在main方法里把Exception抛出去交给JAVA运行时系统出力就完事了,这是一种不负责任的表现。如果想把程序写得特别健壮,使用try……catch去捕获异常并处理掉捕获后的异常是必不可少的做法。
三、try...cath...finally语句
3.1. try语句
3.2. catch语句
3.3. finally语句
四、声明并抛出异常
五.新特性
例如try-with-resources (编译时自动生成处理逻辑去关闭那些拓展了AutoCloseable和Closeable的对象)和 multiple catch等方便了我们 对异常的处理
import java.io.*;
public class Main2 {
public static void main(String[] args) {
File file = new File("d:\\test.txt");
try (BufferedReader br = new BufferedReader(new FileReader(file))) {// Try-with-resources
String s =br.readLine();
if(s==null) {
throw new BlankTextException("空白!");
}
} catch(IOException | BlankTextException e ){// Multiple catch
System.out.print(e.getMessage());
}
}
}
六、使用自定义异常
package com.cxy.test;
/**
* 自定义的一个异常类MyException,继承Exception类
*/
public class MyException extends Exception {
private int id;
public MyException(String message,int id) {
super(message);//调用父类Exception的构造方法
this.id = id;
}
//获取异常代码
public int getId() {
return id;
}
}
package com.cxy.test;
import java.text.MessageFormat;
public class TestMyException {
//throws MyException,抛出我们自定义的MyException类的异常。
public void regist(int num) throws MyException{
if(num<0) {
//使用throw手动抛出一个MyException类的异常对象。
throw new MyException("人数为负,不合理",1);
}
/**
* 注意:假如我们抛出了异常之后,
* System.out.println(MessageFormat.format("登记人数:{0}",num));是不会被执行的。
* 抛出异常之后整个方法的调用就结束了。
*/
System.out.println(MessageFormat.format("登记人数:{0}",num));
}
public void manage() {
try {
regist(-100);
} catch (MyException e) {
// TODO Auto-generated catch block
System.out.println("登记失败,错误码:"+e.getId());
e.printStackTrace();
}
System.out.println("操作结束");
}
public static void main(String[] args) {
TestMyException t = new TestMyException();
t.manage();
}
}
六、异常处理总结
养成良好的编程习惯,不要把错误给吞噬掉(即捕获到异常以后又不做出相应处理的做法,这种做法相当于是把错误隐藏起来了,可实际上错误依然还是存在的), 也不要轻易地往外抛错误,能处理的一定要处理,不能处理的一定要往外抛。往外抛的方法有两种,一种是在知道异常的类型以后,方法声明时使用throws把 异常往外抛,另一种是手动往外抛,使用“throw+异常对象”你相当于是把这个异常对象抛出去了,然后在方法的声明写上要抛的那种异常。
NoClassDefoundError 和 ClassNotFoundException
NoClassDeFoundError
ClassNotFoundException
一个继承自Exception,另一个继承自Error。
NoClassDeFoundError
根据上面的继承结构图我们可以发现NoClassDeFoundError继承自LinkageError,根据名字我们就可以猜到这是一个类加载过程中连接时错误。
在Linking的解析阶段:解析阶段是JVM将常量池内的符号引用替换为直接引用的过程
符号引用:就是用一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。
直接引用: 就是直接指向目标的指针
public static void main(String[] args) {
new ArrayList();
}
比如里面的new ArrayList(),在类生命周期的解析阶段之前,还只是符号这就是符号引用。 在解析过程中,JVM会去找ArrayList是否被加载,如果未加载会先加载ArrayList,然后返回ArrayList的引用。这就是直接引用
所以,如果在连接阶段的解析时,找不到相应的类,就会报NoClassDeFoundError
package errorTest;
public class Main {
public static void main(String[] args) {
ABC abc=new ABC();
}
}
class ABC{
}
class abc extends ABC{
}
由于Windows中,文件名不区分大小写,所以下面这个例子中,在编译的时候,先编译父类ABC.class,再编译子类abc,class,并直接覆盖ABC.class的内容
解析
阶段的时候,就找不到ABC这个类啦~。所以就报 NoClassDeFoundError
ClassNotFoundException
这个这个错误其实是经常遇到的,Class.forName()操作,程序运行的时候通过类的全限定名去加载时找不到这个类,就会报异常了,所以这类操作需要手动抛出异常或者try/catch(虽然编辑器一般会进行提示)
public class Main2 {
public static void main(String[] args) {
try {
Class.forName("com.cxy.Test");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
- NoClassDeFoundError 继承自Error属于用户程序无法捕获处理的异常
- ClassNotFoundException 继承自Exception属于用户程序能捕获处理的异常
- NoClassDeFoundError 发生在类生命周期中解析阶段找不到相应的类
- ClassNotFoundException 发生在类生命周期的加载阶段,找不到相应的类。
第一,尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常,或者是把我们自定义的 RuntimeException 被扩散出来,而不是被捕获。
进一步讲,除非深思熟虑了,否则不要捕获 Throwable 或者 Error,这样很难保证我们能够正确程序处理 OutOfMemoryError。
第二,不要生吞(swallow)异常。这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。
生吞异常,往往是基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的,但是千万不要在产品代码做这种假设!
如果我们不把异常抛出来,或者也没有输出到日志(Logger)之类,程序可能在后续代码以不可控的方式结束。没人能够轻易判断究竟是哪里抛出了异常,以及是什么原因产生了异常。
第三,生产环境拒绝e.printStackTrace();
官方文档的开头就是“Prints this throwable and its backtrace to the standard error stream”。
问题就在这里,在稍微复杂一点的生产系统中,标准出错(STERR)不是个合适的输出选项,因为你很难判断出到底输出到哪里去了。
尤其是对于分布式系统,如果发生异常,但是无法找到堆栈轨迹(stacktrace),这纯属是为诊断设置障碍。所以,最好使用产品日志,详细地输出到日志系统里。
第四:Throw early, catch late
public void readPreferences(String fileName){
//...perform operations...
InputStream in = new FileInputStream(fileName);
//...read the preferences file...
}
比如这个方法,可能第一步filaName 就是 null,那么程序就会抛出 NullPointerException,但是由于没有第一时间暴露出问题,堆栈信息可能非常令人费解,往往需要相对复杂的定位。在发现问题的时候,第一时间抛出,能够更加清晰地反映问题。
public void readPreferences(String filename) {
Objects. requireNonNull(filename);
//...perform other operations...
InputStream in = new FileInputStream(filename);
//...read the preferences file...
}
像上面这样,或者仿照写一个Asser方法,将异常情况throw early
至于“catch late”,捕获异常后,需要怎么处理呢?最差的处理方式,就是“生吞异常”,本质上其实是掩盖问题。如果实在不知道如何处理,可以选择保留原有异常的 cause 信息,直接再抛出或者构建新的异常抛出去。在更高层面,因为有了清晰的(业务)逻辑,往往会更清楚合适的处理方式是什么。
有的时候,我们会根据需要自定义异常,这个时候除了保证提供足够的信息,还有两点需要考虑:
- 是否需要定义成 Checked Exception,因为这种类型设计的初衷更是为了从异常情况恢复,作为异常设计者,我们往往有充足信息进行分类。
- 在保证诊断信息足够的同时,也要考虑避免包含敏感信息,因为那样可能导致潜在的安全问题。比如登陆的校验,避免输出用户数据等信息,避免坏人的攻击。参考 java.net.ConnectException,出错信息是类似“ Connection refused (Connection refused)”,而不包含具体的机器名、IP、端口等,一个重要考量就是信息安全。
业界有一种争论(甚至可以算是某种程度的共识),Java 语言的 Checked Exception 也许是个设计错误:
-
Checked Exception 的假设是我们捕获了异常,然后恢复程序。但是,其实我们大多数情况下,根本就不可能恢复。Checked Exception 的使用,已经大大偏离了最初的设计目的。
-
Checked Exception 不兼容 functional 编程,如果你写过 Lambda/Stream 代码,相信深有体会。
性能角度
Java 的异常处理机制,这里有两个可能会相对昂贵的地方:
-
try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
-
Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。
当我们的服务出现反应变慢、吞吐量下降的时候,检查发生最频繁的 Exception 也是一种思路。