Error和Exception区别
Java异常的基类为java.lang.Throwable.Throwable对象可以分为两种类型
一种Exception,一种是Error
Error是Throwable的子类,用于指示合理的应用程序不应该试图捕获的严重问题,比如JVM内部的严重问题,无法恢复,程序人员不用处理
Exception异常通过合理的处理,程序还可以回到正常执行流程。要求编程人员要进行处理。
受检异常和非受检异常
RuntimeException是运行时异常,它属于java的标准运行时检测的一部分,运行时异常都是继承RuntimeException的,他们会被Java虚拟机自动检测抛出这种异常属于错误,将被自动捕获,所以不用程序员手动处理
常见的非受检异常
1. NullPointerException
2. ClassCastException
3. ArrayIndexsOutOfBoundsException
4. ArithmeticException
5. IndexOutOfBoundsException
常见的受检异常
1. FileNotFoundException
2. IOException
3. SQLException
4. InterruptException
受检异常一个重要的目的在于提醒调用者这个服务可能会出现这个异常,告知调用者在出现这个异常的时候进行弥补。在对安全性要求高的场景最好使用受检异常以通知调用者,比如说银行取钱失败抛出的异常,调用者要对这个异常打日志,以应对可能存在的风险。而对于RuntimeException调用者往往不知道这个异常的存在,异常就会一层层往上抛以达到终止程序的目的。
受检异常和非受检异常的相互转化
受检异常转化为非受检异常案例
public static void test() {
try {
FileReader reader=new FileReader("...path");
} catch (FileNotFoundException e) {
throw new RuntimeException("error");
}
}
受检异常在安全性要求高的场景需要使用受检异常让调用者在出现异常的情况下进行弥补,但是为什么我们又要使用非受检异常来转化受检异常呢
我们可以利用受检异常可以转化为非受检异常
总的来说主要归结如下几个原因
- 受检异常使接口声明脆弱
比如说如下这样一个接口
public interface IUserService{
//修改用户名,抛出安全异常
publicvoid changePassword() throws MySecurityExcepiton;
}
这个接口可能会有多个实现者比如 普通用户UserImpl,模拟用户MockUserImpl,机器用户MachineUser,如果此时发现changePassword()方法可能还需要抛出RejectChangeException,那就要修改User接口了,如果修改了User接口,那么所有的IUserService接口的实现类都要追加对RejectChangeException的处理
这样会产生两个问题1.异常是主逻辑的补充,修改一个补充逻辑结果导致主逻辑的修改,这就会出现实现者”逆影响”接口的情景
受检异常使代码可读性降低
try catch会增加代码量,尤其是出现嵌套的try catch的时候代码可读性就下降了受检异常增加开发工作量
异常需要封装,经过封装的异常才能让异常更容易理解,上层模块能更好的处理问题,这会导致底层异常数次的封装,比如FileNotFoundException就可能要封装成UserXXXFileNotFoundException向上层模块抛出
所以在非受检异常不威胁系统的安全性,稳定性,可靠性,正确性的时候可以将受检异常转化为非受检异常
重新抛出异常
有时候希望将刚捕获的异常重新抛出,那么新抛出的异常是原来的异常抛出点的信息,而非是重新抛出点的信息.如果有时候要更新这个信息,可以使用fillInStackTrace()方法。演示如下
package com.java.exception;
public class LoggingException {
private static Logger logger = Logger.getLogger("LoggingException");
private static void logException(Exception e) {
StringWriter trace = new StringWriter();
e.printStackTrace(new PrintWriter(trace));
logger.severe(trace.toString());
}
public static void compute() throws Exception {
try {
//point1:
int i = 1 / 0;
} catch (Exception e) {
throw (Exception) e.fillInStackTrace();
}
}
public static void main(String[] args) throws Throwable {
try {
//point2:
compute();
} catch (Exception e) {
throw e.fillInStackTrace();
}
System.out.println("end");
}
}
运行结果
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.java.exception.LoggingException.main(LoggingException.java:28)
可见异常栈信息更新到point2行,而不是异常发生点第point1行
异常链
有时候捕获了异常然后抛出新异常,为了把原始异常的信息保存下来,这被称为异常链.在Throwable的子类在构造器中可以接受一个cause对象作为参数,这个cause用来表示原始异常,这样可以把原始异常传递给新异常,这样新异常就能最终到异常最初发生的位置
public class LoggingException {
private static Logger logger = Logger.getLogger("LoggingException");
private static void logException(Exception e) {
StringWriter trace = new StringWriter();
e.printStackTrace(new PrintWriter(trace));
logger.severe(trace.toString());
}
public static void compute() throws Exception {
try {
//point1
int i = 1 / 0;
} catch (Exception e) {
throw (Exception) e.fillInStackTrace();
}
}
public static void main(String[] args) throws Throwable {
try {
//point2
compute();
} catch (Exception e) {
throw new Exception(e);
}
System.out.println("end");
}
}
运行结果
Exception in thread "main" java.lang.Exception: java.lang.ArithmeticException: / by zero
at com.java.exception.LoggingException.main(LoggingException.java:28)
Caused by: java.lang.ArithmeticException: / by zero
at com.java.exception.LoggingException.compute(LoggingException.java:20)
at com.java.exception.LoggingException.main(LoggingException.java:26)
构造器异常抛出处理
public class InputFile {
private BufferedReader in;
public InputFile(String filename) throws Exception {
try {
in = new BufferedReader(new FileReader(filename));
} catch (FileNotFoundException e) {
try {
in.close();
} catch (IOException e1) {
System.out.println(" in.close() unsuccessfully ");
}
//重新抛出异常告诉调用者InputFile对象创建失败
throw e;
} finally {
}
}
public String getLine() {
String s;
try {
s = in.readLine();
} catch (IOException e) {
throw new RuntimeException("read line failed");
}
return s;
}
public void dispose() {
try {
in.close();
} catch (IOException e) {
throw new RuntimeException("in.close failed");
}
}
}
而在构造器finally中不进行in.close()主要考虑两个因素 1.文件创建成功用户还没有使用,所以资源不能关闭。2.文件创建失败,资源in并没有被初始化,所以不能关闭,如果关闭会抛出NullPointException
而在getLine方法中将异常转换为了RuntimeException表示这是一个编程错误。
对于这种对象的声明我们通常会采用如下的声明方法
try {
InputFile in = new InputFile("C:\\Users\\1361959119@qq.com\\Desktop\\4564.txt");
String s;
int i = 1;
while ((s = in.getLine()) != null) {
System.out.println("reading line -> "+s);
}
} catch (Exception e) {
System.out.println("InputFile Construction Failed");
} finally {
in.dispose();
}
这样的声明方法是存在缺陷的:
上述这样的异常捕获在InputFile对象构造失败抛出异常然后执行in.close()就会出现问题,因为此时in对象并没有被构造成功所以in.close()就会出错
为了避免上述的问题,应该采用如下的异常方式
try {
InputFile in = new InputFile("C:\\Users\\1361959119@qq.com\\Desktop\\4564.txt");
try {
String s;
while ((s = in.getLine()) != null) {
System.out.println("reading line -> "+s);
}
} catch (Exception e) {
System.out.println("cause exception in main");
} finally {
in.dispose();
}
} catch (Exception e) {
System.out.println("exception happenes in construction");
}
如果in构造失败那么直接跳转到第一个try-catch中,如果in构造成功而在in.getLine()时候抛出异常,那么在嵌套的try-catch中执行然后关闭in就可以
如果父类的构造器会抛出异常,而子类继承父类时如果按下面这样声明构造器会报错
class NeedsCleanUP1 {
public NeedsCleanUP1() throws ConstructionException {
}
}
class NeedsCleanUp2 extends NeedsCleanUP1{
public NeedsCleanUp2(){
try {
super();
} catch (ConstructionException e) {
e.printStackTrace();
}
}
}
编译器会报如下错误
Error:(25, 13) java: Constructor call must be the first statement in a constructor
所以子类只能抛出父类构造时的错误而不能捕获
try-finally
对于构造器不会抛出异常,但在使用之后需要关闭的资源该如何创建和关闭资源呢
辅助类如下
class NeedsCleanUp {
private static long counter = 1;
private final long id = counter++;
public void dispose() {
System.out.println("NeedsCleanup -> " + id + " clean up");
}
}
class ConstructionException extends Exception {
}
class NeedsCleanUP1 extends NeedsCleanUp {
public NeedsCleanUP1() throws ConstructionException {
}
}
核心代码
NeedsCleanUp nc1 = new NeedsCleanUp();
try {
//业务逻辑
//dosomething()
} finally {
nc1.dispose();
}
而在创建构造器抛出异常和需要进行资源清理的对象时又该如何处理
try {
NeedsCleanUP1 nc4=new NeedsCleanUP1();
try {
NeedsCleanUP1 nc5=new NeedsCleanUP1();
try {
//业务逻辑
//doSomething
}finally {
nc5.dispose();
}
} catch (ConstructionException e) {
e.printStackTrace();
} finally {
nc4.dispose();
}
} catch (ConstructionException e) {
e.printStackTrace();
}
这个时候代码变得比较复杂同时嵌套了三层的try-catch
对于构造器抛出异常同时资源需要清理的对象时记住一个原则:构造对象的时候一定要try-catch可能会抛出的构造异常,而在使用该对象的时候一定try-finally关闭使用结束后的资源
java异常机制的缺陷
在上面的例子中,修改【重新抛出异常】main函数如下
public static void main(String[] args) throws Throwable {
try {
//point2
compute();
} catch (Exception e) {
throw new Exception(e);
}finally {
throw new NullPointerException("非空异常");
}
//System.out.println("end");
}
运行结果
Exception in thread "main" java.lang.NullPointerException: 非空异常
at com.java.exception.LoggingException.main(LoggingException.java:32)
try语句中的compute抛出ArithmeticException,于是程序跳转到finally语句块继续执行,但是finally语句块又抛出了新的异常,于是新异常NullPointerException覆盖了旧异常ArithmeticException,这可以算是java异常的一个缺陷