2019-10-14
1.9k
Java基础学习之异常处理
Java的异常
Java标准库中常用的异常:
异常是一种class
,它的继承关系如下:
Throwable
有两个子类:Error
和Exception
。
Error
表示严重的错误,例如:
OutOfMemoryError
:内存耗尽
NoClassDefFountError
:无法加载某个Class
StackOverflowError
:栈溢出
Exception
表示运行时错误,它可以被捕获并处理。
程序逻辑处理相关:
NumberFormatException
:数值类型的格式错误
FileNotFoundException
:未找到文件
SocketException
:读取网络失败
以及程序逻辑编写错误:
NullPointerException
:对某个null
的对象调用方法或字段
IndexOutOfBoundsException
:数组索引越界
Java规定:
必须捕获的异常包括:Exception
及其子类,除了RuntimeExcetion
及其子类。
不需要捕获的异常包括:Error
及其子类,RuntimeExcetion
及其子类。
捕获异常
捕获异常使用try...catch
语句,把可能发生异常的代码放到try {...}
中,然后使用catch
捕获对应的Exception
及其子类 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.io.UnsupportedEncodingException;import java.util.Arrays;public class Main { public static void main (String[] args) { byte [] bs = toGBK("中文" ); System.out.println(Arrary.toString(bs)); } static byte [] toGBK(String s) { try { return s.getBytes("GBK" ); } catch (UnsupportedEncodingException e) { System.out.println(e); return s.getBytes(); } } }
如果我们不捕获UnsupportedEncodingException
,则会出现编译失败,这是因为String.getBytes()
方法的定义是:
1 2 3 public byte [] getBytes(String charsetName) throws UnsupportedEncodingException { ... }
所以调用方在调用的时候,必须强制捕获这些异常,否则编译器会报错。
多catch语句
可以使用多个catch
语句,每个catch
捕获相对应的异常,但是,多个catch
语句只有一个能被执行 。所以存在多个catch
时候,catch
的顺序非常重要:子类必须写在前面 。例如:
1 2 3 4 5 6 7 8 public static void main (String[] args) { try { ... } catch (IOExcetion c) { System.out.println("IO error" ); } catch (UnsupportedEncodingException e) { System.out.println("Bad encoding" ); }
因为UnsupportedEncodingException
是IOExcetion
的子类,所以当抛出UnsupportedEncodingException
异常时,会被catch (IOException e) { ... }
捕获并执行。
finally语句
无论是否有异常发生,如果我们都希望执行一些语句,可以使用finally
语句,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 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" ); } }
如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,我们可以使用|
来合并:
1 2 3 4 5 6 7 8 9 10 11 public static void main (String[] args) { try { process1(); process2(); process3(); } catch (IOException | NumberFormatException e) { System.out.println("Bad input" ); } catch (Exception e) { System.out.println("Unknown error" ); } }
抛出异常
抛出异常分为两步:
创建某个Excetion
实例;
用throw
语句抛出。
例如:
1 2 3 4 5 void process (String s) { if (s == null ) { throw new NullPointerExcetion(); } }
异常转换
1 2 3 4 5 6 7 8 9 10 11 12 13 void process1 (String s) { try { process2(); } catch (NullPointerExcetion e) { throw new IllegalArgumentExcement; } } void process2 (String s) { if (s==null ){ throw new NullPonterExcetion(); } }
当process2()
抛出NullPointerException
后,被process1()
捕获,然后抛出IllegalArgumentException()
。
如果在main()
中捕获IllegalArgumentException
,我们看看打印的异常栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Main { public static void main (String[] args) { try { process1(); } catch (Exception e) { e.printStackTrace(); } } static void process1 () { try { process2(); } catch (NullPointerException e) { throw new IllegalArgumentException(); } } static void process2 () { throw new NullPointerException(); } }
异常栈如下:
1 2 3 4 5 6 7 8 9 10 11 java.lang.IllegalArgumentException at Main.process1(Main.java:15) at Main.main(Main.java:5) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:567) at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:415) at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:192) at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:132)
我们已经看不到原始异常NullPointerException
的信息了。 为了能追踪到完整的异常栈,在构造异常的时候,把原始的Exception
实例传进去,新的Exception
就可以持有原始Exception
信息。 即:
1 2 3 4 5 6 7 8 static void process1 () { try { process2(); } catch (NullPointerException e) { throw new IllegalArgumentException(e); } }
异常栈如下:
1 2 3 4 5 6 7 java.lang.IllegalArgumentException: java.lang.NullPointerException at Main.process1(Main.java:15) at Main.main(Main.java:5) Caused by: java.lang.NullPointerException at Main.process2(Main.java:20) at Main.process1(Main.java:13)
自定义异常
一个常见的做法是自定义一个BaseException
作为“根异常”,然后,派生出各种业务类型的异常。
BaseException
需要从一个适合的Exception
派生,通常建议从RuntimeException
派生:
1 2 3 public class BaseException extends RuntimeExcetion {}
其他业务类型的异常就可以从BaseException
派生:
1 2 3 4 5 6 7 8 public class UserNotFoundException extends BaseException {} public class LoginFailedException extends BaseException {} ...
使用JDK Logging
直接上🌰
1 2 3 4 5 6 7 8 9 10 11 12 13 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("ignore" ); logger.severe("process will be terminated..." ); } }
输出:
1 2 3 4 5 6 7 10月 12, 2019 6:34:55 下午 Hello main 信息: start process... 10月 12, 2019 6:34:55 下午 Hello main 警告: memory is running out... 10月 12, 2019 6:34:55 下午 Hello main 严重: process will be terminated...
logger.fine("ignore");
没有打印是因为JDK默认的消息级别是Info
:
SEVERE
WARNING
INFO
CONFIG
FINE
FINER
FINEST
使用Common Logging
使用 Common Logging只需要两个步骤:
通过LogFactory
获取Log
类的实例
使用Log
实例的方法打印日志
1 2 3 4 5 6 7 8 9 10 11 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..." ); } }
Commons Logging定义了6个日志级别:
FATAL
ERROR
WARNING
INFO
DEBUG
TRACE
使用Common Logging时,如果在静态方法中引用Log
,通常直接定义一个静态类型变量:
1 2 3 4 5 6 7 8 public class Main { static final Log log = LogFactory.getLog(Main.class); static void foo () { log.info("foo" ); } }
在实例方法中引用Log
,通过定义一个实例变量:
1 2 3 4 5 6 7 8 public class Person { protected final Log = LogFactory.getLog(getClass()); void foo () { log.info("foo" ); } }
这两种定义的方法区别在于:实例变量log获取方式是LogFactory.getLog(getClass)
,这种做法的好处是,子类可以直接使用log
实例,例如:
1 2 3 4 5 6 public class Student extends Person { void bar () { log.info("bar" ); } }
此外,Commons Logging的日志方法,例如info()
,除了标准的info(String)
外,还提供了一个非常有用的重载方法:info(String, Throwable)
,这使得记录异常更加简单:
1 2 3 4 5 6 try { ... } catch (Exception e) { log.error("got Exception" , e); }
使用Log4j
Commons Logging,可以作为“日志接口”来使用,而实现这个接口可以使用Log4j。
我们在使用Log4j框架时,更多时候是通过配置文件来配置它。
以XML配置为例,使用Log4j的时候,我们把一个log4j2.xml
的文件放到classpath
下就可以让Log4j读取配置文件并按照我们的配置来输出日志。
另外,因为Log4j也是一个第三方库,需要把下面4个jar包放在classpath
中:
log4j-api-2.x.jar
log4j-core-2.x.jar
log4j-jcl-2.x.jar
commons-logging-1.2.jar
使用SLF4J和Logback
SLF4J类似于Commons Logging,也是一个日志接口,而Logback类似于Log4j,是一个日志的实现。
SLF4J使用方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 import org.slf4j.Logger;import org.slf4j.LoggerFactory;class Main { final Logger = LoggerFactory.getLogger(getClass()); void foo () { int bar = 100 ; String name = "foo" ; logger.info("bar = {} name = {}" , bar, name); } }
SLF4J使用下面三个jar包:
slf4j-api-1.7.x.jar
logback-classic-1.2.x.jar
logback-core-1.2.x.jar
和Log4j类似,我们仍然需要一个Logback的配置文件,把logback.xml
放到classpath下。