- Java的异常
-
计算机程序运行的过程中,总是会出现各种各样的错误。有一些错误是用户造成的,比如,希望用户输入一个
int
类型的年龄,但是用户的输入是abc。程序想要读写某个文件的内容,但是用户已经把它删除了。
还有一些错误是随机出现,并且永远不可能避免的。比如:- 网络突然断了,连接不到远程服务器;
- 内存耗尽,程序崩溃了;
- 用户点“打印”,但根本没有打印机;
- ……
-
Java内置了一套异常处理机制,总是使用异常来表示错误。异常是一种
class
,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了。1 try { 2 String s = processFile(“C:\\test.txt”); 3 // ok: 4 } catch (FileNotFoundException e) { 5 // file not found: 6 } catch (SecurityException e) { 7 // no read permission: 8 } catch (IOException e) { 9 // io error: 10 } catch (Exception e) { 11 // other error: 12 }
- Java的异常是
class
,它的继承关系如下: -
- 从继承关系可知:
Throwable
是异常体系的根,它继承自Object
。Throwable
有两个体系:Error
和Exception
,Error
表示严重的错误,程序对此一般无能为力。 Exception
则是运行时的错误,它可以被捕获并处理。-
Exception
又分为两大类:RuntimeException
以及它的子类;- 非
RuntimeException
(包括IOException
、ReflectiveOperationException
等等)
- Java规定:
-
必须捕获的异常,包括
Exception
及其子类,但不包括RuntimeException
及其子类,这种类型的异常称为Checked Exception。 -
不需要捕获的异常,包括
Error
及其子类,RuntimeException
及其子类。
-
- 编译器对RuntimeException及其子类不做强制捕获要求,不是指应用程序本身不应该捕获并处理RuntimeException。是否需要捕获,具体问题具体分析。
-
捕获异常
-
捕获异常使用
try...catch
语句,把可能发生异常的代码放到try {...}
中,然后使用catch
捕获对应的Exception
及其子类。1 import java.io.UnsupportedEncodingException; 2 import java.util.Arrays; 3 4 public class Main { 5 public static void main(String[] args) { 6 byte[] bs = toGBK("中文"); 7 System.out.println(Arrays.toString(bs)); 8 } 9 10 static byte[] toGBK(String s) { 11 try { 12 // 用指定编码转换String为byte[]: 13 return s.getBytes("GBK"); 14 } catch (UnsupportedEncodingException e) { 15 // 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException: 16 System.out.println(e); // 打印异常信息 17 return s.getBytes(); // 尝试使用用默认编码 18 } 19 } 20 }
- 如果我们不捕获
UnsupportedEncodingException(去掉try catch)
,会出现编译失败的问题。编译器会报错,错误信息类似:unreported exception UnsupportedEncodingException; must be caught or declared to be thrown,并且准确地指出需要捕获的语句是return s.getBytes("GBK");
。意思是说,像UnsupportedEncodingException
这样的Checked Exception,必须被捕获。 - 这是因为
String.getBytes(String)
方法定义是(如下),在方法定义的时候,使用throws Xxx
表示该方法可能抛出的异常类型(Runtime及其子类除外)。调用方在调用的时候,必须强制捕获这些异常,否则编译器会报错。public byte[] getBytes(String charsetName) throws UnsupportedEncodingException { ... }
- 在
toGBK()
方法中,因为调用了String.getBytes(String)
方法,就必须捕获UnsupportedEncodingException
。我们也可以不捕获它,而是在方法定义处用throws表示toGBK()
方法可能会抛出UnsupportedEncodingException
,就可以让toGBK()
方法通过编译器检查。1 import java.io.UnsupportedEncodingException; 2 import java.util.Arrays; 3 4 public class Main { 5 public static void main(String[] args) { 6 byte[] bs = toGBK("中文"); 7 System.out.println(Arrays.toString(bs)); 8 } 9 10 static byte[] toGBK(String s) throws UnsupportedEncodingException { 11 return s.getBytes("GBK"); 12 } 13 }
- 只要是方法声明的Checked Exception,不在调用层捕获,也必须在更高的调用层捕获。所有未捕获的异常,最终也必须在
main()
方法中捕获,不会出现漏写try
的情况。这是由编译器保证的。main()
方法也是最后捕获Exception
的机会。 - 如果是测试代码,上面的写法就略显麻烦。如果不想写任何
try
代码,可以直接把main()
方法定义为throws Exception。
因为main()
方法声明了可能抛出Exception
,也就声明了可能抛出所有的Exception
,因此在内部就无需捕获了。代价就是一旦发生异常,程序会立刻退出。 - 在
toGBK()
内部“消化”异常static byte[] toGBK(String s) { try { return s.getBytes("GBK"); } catch (UnsupportedEncodingException e) { // 什么也不干 } return null;
} - 这种捕获后不处理的方式是非常不好的,即使真的什么也做不了,也要先把异常记录下来:
static byte[] toGBK(String s) { try { return s.getBytes("GBK"); } catch (UnsupportedEncodingException e) { // 先记下来再说: e.printStackTrace(); } return null;
} - 所有异常都可以调用
printStackTrace()
方法打印异常栈,这是一个简单有用的快速打印异常的方法。
-
- 捕获异常
- 凡是可能抛出异常的语句,都可以用
try ... catch
捕获。把可能发生异常的语句放在try { ... }
中,然后使用catch
捕获对应的Exception
及其子类。 -
可以使用多个
catch
语句,每个catch
分别捕获对应的Exception
及其子类。JVM在捕获到异常后,会从上到下匹配catch
语句,匹配到某个catch
后,执行catch
代码块,然后不再继续匹配。简单地说就是:多个
catch
语句只有一个能被执行。 - 存在多个
catch
的时候,catch
的顺序非常重要:子类必须写在前面。 -
无论是否有异常发生,都希望执行一些语句,例如清理工作,可以把执行语句写若干遍:正常执行的放到
try
中,每个catch
再写一遍。1 public static void main(String[] args) { 2 try { 3 process1(); 4 process2(); 5 process3(); 6 System.out.println("END"); 7 } catch (UnsupportedEncodingException e) { 8 System.out.println("Bad encoding"); 9 System.out.println("END"); 10 } catch (IOException e) { 11 System.out.println("IO error"); 12 System.out.println("END"); 13 } 14 }
- Java的
try ... catch
机制还提供了finally
语句,finally
语句块保证有无错误都会执行。上述代码可以改写如下。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"); } finally { System.out.println("END"); } }
-
finally
有几个特点:finally
语句不是必须的,可写可不写;finally
总是最后执行。
- 某些情况下,可以没有
catch
,只使用try ... finally
结构。void process(String file) throws IOException { try { ... } finally { System.out.println("END"); } }
因为方法声明了可能抛出的异常,所以可以不写
catch
。
- 凡是可能抛出异常的语句,都可以用
- 抛出异常
- 查看
Integer.java
源码可知,抛出异常的方法代码如下。public static int parseInt(String s, int radix) throws NumberFormatException { if (s == null) { throw new NumberFormatException("null"); } ... }
-
查看
Integer.java
源码可知,抛出异常的方法代码如下当发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。如何抛出异常?参考Integer.parseInt()
方法,抛出异常分两步:- 创建某个
Exception
的实例; - 用
throw
语句抛出。void process2(String s) { if (s==null) { NullPointerException e = new NullPointerException(); throw e; } } 或: void process2(String s) { if (s==null) { throw new NullPointerException(); } }
- 在代码中获取原始异常可以使用
Throwable.getCause()
方法。如果返回null
,说明已经是“根异常”了。 - 捕获到异常并再次抛出时,一定要留住原始异常,否则很难定位第一案发现场!
- 创建某个
- 在
try
或者catch
语句块中抛出异常,不会影响finally
的执行。JVM会先执行finally
,然后抛出异常。 - 如果在执行
finally
语句时抛出异常,那么,finally
抛出异常后,原来在catch
中准备抛出的异常就“消失”了,因为只能抛出一个异常。没有被抛出的异常称为“被屏蔽”的异常(Suppressed Exception)。public class Main { public static void main(String[] args) { try { Integer.parseInt("abc"); } catch (Exception e) { System.out.println("catched"); throw new RuntimeException(e); } finally { System.out.println("finally"); throw new IllegalArgumentException(); } } } 输出: catched finally Exception in thread "main" java.lang.IllegalArgumentException at Main.main(Main.java:11)
- 在极少数的情况下,我们需要获知所有的异常。如何保存所有的异常信息?方法是先用
origin
变量保存原始异常,然后调用Throwable.addSuppressed()
,把原始异常添加进来,最后在finally
抛出。(通过Throwable.getSuppressed()
可以获取所有的Suppressed Exception
。绝大多数情况下,在finally
中不要抛出异常。因此,通常不需要关心Suppressed Exception
。)
1 public class Main { 2 public static void main(String[] args) throws Exception { 3 Exception origin = null; 4 try { 5 System.out.println(Integer.parseInt("abc")); 6 } catch (Exception e) { 7 origin = e; 8 throw e; 9 } finally { 10 Exception e = new IllegalArgumentException(); 11 if (origin != null) { 12 e.addSuppressed(origin); 13 } 14 throw e; 15 } 16 } 17 }
- 查看
- 自定义异常
-
在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系是非常重要的。一个常见的做法是自定义一个
BaseException
作为“根异常”,然后,派生出各种业务类型的异常。BaseException
需要从一个适合的Exception
派生,通常建议从RuntimeException
派生。public class BaseException extends RuntimeException { }
//其他业务类型的异常就可以从BaseException
派生:public class UserNotFoundException extends BaseException { } public class LoginFailedException extends BaseException { }
...//自定义的
BaseException
应该提供多个构造方法public class BaseException extends RuntimeException { public BaseException() { super(); } public BaseException(String message, Throwable cause) { super(message, cause); } public BaseException(String message) { super(message); } public BaseException(Throwable cause) { super(cause); } }
-
上述构造方法实际上都是原样照抄
RuntimeException
。这样,抛出异常的时候,就可以选择合适的构造方法。通过IDE可以根据父类快速生成子类的构造方法。
-
-
NullPointerException
NullPointerException
即空指针异常,俗称NPE。如果一个对象为null
,调用其方法或访问其字段就会产生NullPointerException
,这个异常通常是由JVM抛出的。- 指针这个概念实际上源自C语言,Java语言中并无指针。我们定义的变量实际上是引用,Null Pointer更确切地说是Null Reference。
NullPointerException
是一种代码逻辑错误,遇到NullPointerException
,遵循原则是早暴露,早修复,严禁使用catch
来隐藏这种编码错误。- 成员变量在定义时初始化使用空字符串
""
而不是默认的null
可避免很多NullPointerException
,编写业务逻辑时,用空字符串""
表示未填写比null
安全得多。 - 如果调用方一定要根据
null
判断,比如返回null
表示文件不存在,那么考虑返回Optional<T>
public Optional<String> readFromFile(String file) { if (!fileExist(file)) { return Optional.empty(); } ... }
这样调用方必须通过
Optional.isPresent()
判断是否有结果。 -
定位NullPointerException
- 从Java 14开始,如果产生了
NullPointerException
,JVM可以给出详细的信息告诉我们null
对象到底是谁。 -
这种增强的
NullPointerException
详细信息是Java 14新增的功能,但默认是关闭的,我们可以给JVM添加一个-XX:+ShowCodeDetailsInExceptionMessages
参数启用它:java -XX:+ShowCodeDetailsInExceptionMessages Main.java
- 从Java 14开始,如果产生了
-
使用断言
- 断言(Assertion)是一种调试程序的方式。在Java中,使用
assert
关键字来实现断言。public static void main(String[] args) { double x = Math.abs(-123.45); assert x >= 0; System.out.println(x); }
语句
assert x >= 0;
即为断言,断言条件x >= 0
预期为true
。如果计算结果为false
,则断言失败,抛出AssertionError
。 - 使用
assert
语句时,还可以添加一个可选的断言消息。assert x >= 0 : "x must >= 0";
断言失败的时候,
AssertionError
会带上消息x must >= 0
,更加便于调试。
- 断言(Assertion)是一种调试程序的方式。在Java中,使用
-
使用JDK Logging
- Java标准库内置了日志包
java.util.logging
,可以直接用。import java.util.logging.Level; import java.util.logging.Logger; public class Hello { public static void main(String[] args) { Logger logger = Logger.getGlobal(); logger.info("start process..."); logger.warning("memory is running out..."); logger.fine("ignored."); logger.severe("process will be terminated..."); } }
-
日志的输出可以设定级别。JDK的Logging定义了7个日志级别,从严重到普通:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
- Java标准库内置了日志包
-
因为默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
-
Commons Logging(理解)
-
Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。Commons Logging的特色是,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。
-
使用Commons Logging只需要和两个类打交道,并且只有两步:第一步,通过
LogFactory
获取Log
类的实例; 第二步,使用Log
实例的方法打日志。import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class Main { public static void main(String[] args) { Log log = LogFactory.getLog(Main.class); log.info("start..."); log.warn("end."); } }
-
-
使用Log4j
-
使用SLF4J和Logback