方法一:约定返回错误码。
例如,处理一个文件,如果返回0,表示成功,返回其他整数,表示约定的错误码:
int code = processFile("C:\\test.txt"); if (code == 0) { // ok: } else { // error: switch (code) { case 1: // file not found: case 2: // no read permission: default: // unknown error: } }
方法二:在语言层面上提供一个异常处理机制。
Java内置了一套异常处理机制,总是使用异常来表示错误。
异常是一种class,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了:
try { String s = processFile(“C:\\test.txt”); // ok: } catch (FileNotFoundException e) { // file not found: } catch (SecurityException e) { // no read permission: } catch (IOException e) { // io error: } catch (Exception e) { // other error: }
┌───────────┐ │ Object │ └───────────┘ ▲ │ ┌───────────┐ │ Throwable │ └───────────┘ ▲ ┌─────────┴─────────┐ │ │ ┌───────────┐ ┌───────────┐ │ Error │ │ Exception │ └───────────┘ └───────────┘ ▲ ▲ ┌───────┘ ┌────┴──────────┐ │ │ │ ┌─────────────────┐ ┌─────────────────┐┌───────────┐ │OutOfMemoryError │... │RuntimeException ││IOException│... └─────────────────┘ └─────────────────┘└───────────┘ ▲ ┌───────────┴─────────────┐ │ │ ┌─────────────────────┐ ┌─────────────────────────┐ │NullPointerException │ │IllegalArgumentException │... └─────────────────────┘ └─────────────────────────┘
Error表示严重的错误,程序对此一般无能为力,例如:
OutOfMemoryError:内存耗尽NoClassDefFoundError:无法加载某个ClassStackOverflowError:栈溢出Exception则是运行时的错误,它可以被捕获并处理。
某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:
NumberFormatException:数值类型的格式错误FileNotFoundException:未找到文件SocketException:读取网络失败还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:
NullPointerException:对某个null的对象调用方法或字段IndexOutOfBoundsException:数组索引越界Exception又分为两大类:
RuntimeException以及它的子类;RuntimeException(包括IOException、ReflectiveOperationException等等)Java规定:
Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。Error及其子类,RuntimeException及其子类捕获异常使用try...catch语句,把可能发生异常的代码放到try {...}中,然后使用catch捕获对应的Exception及其子类
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(); // 尝试使用用默认编码 } } } >>> [-42, -48, -50, -60]
可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。
简单地说就是:多个catch语句只有一个能被执行。例如:
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException e) { System.out.println(e); } catch (NumberFormatException e) { System.out.println(e); } }
存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。例如:
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException e) { System.out.println("IO error"); } catch (UnsupportedEncodingException e) { // 永远捕获不到 System.out.println("Bad encoding"); } }
UnsupportedEncodingException异常是永远捕获不到的,因为它是IOException的子类。当抛出UnsupportedEncodingException异常时,会被catch (IOException e) { ... }捕获并执行
无论是否有异常发生,如果我们都希望执行一些语句
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"); } }
某些情况下,可以没有catch,只使用try ... finally结构。例如:
void process(String file) throws IOException { try { ... } finally { System.out.println("END"); } }
如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么就得编写多条catch子句:
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException e) { System.out.println("Bad input"); } catch (NumberFormatException e) { System.out.println("Bad input"); } catch (Exception e) { System.out.println("Unknown error"); } }
因为处理IOException和NumberFormatException的代码是相同的,所以我们可以把它两用|合并到一起:
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException | NumberFormatException e) { // IOException或NumberFormatException System.out.println("Bad input"); } catch (Exception e) { System.out.println("Unknown error"); } }
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 } } >>> java.lang.NumberFormatException: null at java.base/java.lang.Integer.parseInt(Integer.java:620) at java.base/java.lang.Integer.parseInt(Integer.java:776) at Main.process2(Main.java:15) at Main.process1(Main.java:11) at Main.main(Main.java:4)
main()调用process1();process1()调用process2();process2()调用Integer.parseInt(String);Integer.parseInt(String)调用Integer.parseInt(String, int)。
在代码中获取原始异常可以使用Throwable.getCause()方法。如果返回null,说明已经是“根异常”了。
用throw语句抛出
void process2(String s) { if (s==null) { NullPointerException e = new NullPointerException(); throw e; } }
实际上,绝大部分抛出异常的代码都会合并写成一行:
void process2(String s) { if (s==null) { throw new NullPointerException(); } }
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 Exception in thread "main" finally java.lang.IllegalArgumentException at Main.main(Main.java:10)
绝大多数情况下,在finally中不要抛出异常。因此,我们通常不需要关心Suppressed Exception。
在极少数的情况下,我们需要获知所有的异常
先用origin变量保存原始异常,然后调用Throwable.addSuppressed(),把原始异常添加进来,最后在finally抛出
public class Main { public static void main(String[] args) throws Exception { Exception origin = null; try { System.out.println(Integer.parseInt("abc")); } catch (Exception e) { origin = e; throw e; } finally { Exception e = new IllegalArgumentException(); if (origin != null) { e.addSuppressed(origin); } throw e; } } } >>> Exception in thread "main" java.lang.IllegalArgumentException at Main.main(Main.java:10) Suppressed: java.lang.NumberFormatException: For input string: "abc" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:68) at java.base/java.lang.Integer.parseInt(Integer.java:658) at java.base/java.lang.Integer.parseInt(Integer.java:776) at Main.main(Main.java:5)
Java标准库定义的常用异常包括:
Exception │ ├─ RuntimeException │ │ │ ├─ NullPointerException │ │ │ ├─ IndexOutOfBoundsException │ │ │ ├─ SecurityException │ │ │ └─ IllegalArgumentException │ │ │ └─ NumberFormatException │ ├─ IOException │ │ │ ├─ UnsupportedCharsetException │ │ │ ├─ FileNotFoundException │ │ │ └─ SocketException │ ├─ ParseException │ ├─ GeneralSecurityException │ ├─ SQLException │ └─ TimeoutException
在代码中需要抛出异常时,尽量使用JDK已定义的异常类型
static void process1(int age) { if (age <= 0) { throw new IllegalArgumentException(); } }
自定义新的异常类型
一个常见的做法是自定义一个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); } }
断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言
实际开发中,很少使用断言。更好的方法是编写单元测试
JVM默认关闭断言指令,即遇到assert语句就自动忽略了,不执行。
要执行assert语句,必须给Java虚拟机传递-enableassertions(可简写为-ea)参数启用断言
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
断言失败时会抛出AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段
对于可恢复的程序错误,不应该使用断言。例如:
void sort(int[] arr) { assert arr != null; }
应该抛出异常并在上层捕获:
void sort(int[] arr) { if (x == null) { throw new IllegalArgumentException("array cannot be null"); } }