爲什麼需要異常處理
生活中的異常: 做地鐵過來上班,地鐵晚點; 平時的地鐵時間間隔3分鐘,疫情的影響10分鐘。吃早餐異常情況:沒飯了。遇到異常情況怎麼辦? 解決異常: 做公交車、共享單車。想辦法處理異常,生活繼續,不能因爲碰到一點點挫折就放棄人生。
什麼是異常處理
異常的英文單詞是exception,字面翻譯就是“意外、例外”的意思,也就是非正常情況。事實上,異常本質上是程序上的錯誤,包括程序邏輯錯誤和系統錯誤。比如使用空的引用、數組下標越界、內存溢出錯誤等,這些都是意外的情況,背離我們程序本身的意圖。錯誤在我們編寫程序的過程中會經常發生,包括編譯期間和運行期間的錯誤,在編譯期間出現的錯誤有編譯器幫助我們一起修正,然而運行期間的錯誤便不是編譯器力所能及了,並且運行期間的錯誤往往是難以預料的。假若程序在運行期間出現了錯誤,如果置之不理,程序便會終止或直接導致系統崩潰,顯然這不是我們希望看到的結果。因此,如何對運行期間出現的錯誤進行處理和補救呢?Java提供了異常機制來進行處理,通過異常機制來處理程序運行期間出現的錯誤。通過異常機制,我們可以更好地提升程序的健壯性。
簡單來說,異常就是我們碰到的非正常的現象,可以通過解決異常進一步執行剩下業務;但是如果解決不恰當會影響系統的執行,甚至中斷執行。程序中的異常:
package ch003;
import java.util.Scanner;
public class Demo1 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("請輸入第一個數:");
int num1 = input.nextInt();
System.out.print("請輸入第二個數 :");
int num2 = input.nextInt();
int num3 = num1 / num2;
System.out.println(num3);
System.out.println("其他業務代碼........");
input.close();
}
}
根據執行的結果可以看出: 出現異常之後,後續的業務代碼沒有執行,程序直接中斷執行。如何解決異常保持程序出現異常之後可以正常執行?這就是java中的異常處理機制。
如何處理異常
異常處理機制:
①java中把所有遇到的異常用面向對象的方式處理, 把異常封裝成一個對象,假如我們編寫一個類描述一個異常,問題是: 這個類如何編寫?
public class Error{
private String address;//那個位置出現異常
private Date date;//闖紅燈的時間
private String law;//違反的規定
public void showMessage(){
//顯示異常信息
}
}
在java中有一個Exception來描述所有的異常信息,異常的父類: NullPointerException是一個具體的異常。
②整個異常處理機制包括: try—catch—finally throw和throws共計五個關鍵字。
try: 表示的可能出現異常的代碼塊
catch: 捕獲異常, 路口的攝像頭
finally: 不管異常是否出現都要執行的代碼: 繼續生活
throw: 手動拋出一個異常,自己遇到問題,拋出給別人;寶馬車被盜,自己不能處理這個異常,拋給 警察叔叔。
throws: 聲明一個方法拋出的異常,別人用你的電腦: 這個電腦經常死機,別人用你的車: 車剎車不靈
public class Throwable implements Serializable {
}
包括兩個直接子類: Error是無法處理的異常,比如OutOfMemoryError,一般發生這種異常,JVM會選擇終止程序。因此我們編寫程序時不需要關心這類異常,也沒有辦法通過異常處理機制解決的。Exception是異常,所有異常的祖先類,可以通過異常處理機制解決的。
Exception又可以分爲兩大類: RuntimeException運行期異常,代碼在執行過程中出現的異常,譬如NullPointerException和ArraysIndexOutOfBoundException。IOException和SQLException這些在編寫代碼的時候就會出現異常,非運行期異常,編譯器異常,java編譯器強制程序員必須進行捕獲處理,比如常見的IOExeption和SQLException。對於非運行時異常如果不進行捕獲或者拋出聲明處理,編譯都不會通過。
try {
FileOutputStream fos = new FileOutputStream("xxx");
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
檢查型異常(Checked Exception)與非檢查型異常(Unchecked Exception)區別?
- 所有的檢查性異常都繼承自java.lang.Exception;所有的非檢查性異常都繼承自java.lang.RuntimeEx ception。
- 檢查性異常和非檢查性異常最主要的區別在於其處理異常的方式:檢查性異常必須使用try catch或者throws等關鍵字進行處理,否則編譯器會報錯;非檢查性異常一般是程序代碼寫的不夠嚴謹而導致的問題,可以通過修改代碼來規避。
- 常見的運行時異常:空指針異常(NullPointerException)、除零異常(ArithmeticException)、數組越界異常(ArrayIndexOutOfBoundsException)等;
- 常見的檢查性異常:輸入輸出異常(IOException)、文件不存在異常(FileNotFoundException)、SQL語句異常(SQLException)等。
try–catch
①正常執行
package ch003;
import java.util.Scanner;
public class Demo2 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
try {
System.out.print("請輸入第一個數:");
int num1 = input.nextInt();
System.out.print("請輸入第二個數 :");
int num2 = input.nextInt();
int num3 = num1 / num2;
System.out.println(num3);
} catch (Exception e) {
e.printStackTrace();//打印堆棧信息
System.err.println("出現異常信息:"+e.getMessage());
}
System.out.println("其他業務代碼........");
input.close();
}
}
執行結果:
請輸入第一個數:100
請輸入第二個數 :10
10
其他業務代碼........
代碼正常執行,沒有出現異常,catch代碼塊不執行的。
②出現異常、成功捕獲
package ch003;
import java.util.Scanner;
public class Demo2 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
try {
System.out.print("請輸入第一個數:");
int num1 = input.nextInt();
System.out.print("請輸入第二個數 :");
int num2 = input.nextInt();
int num3 = num1 / num2;
System.out.println(num3);
} catch (ArithmeticException e) {
e.printStackTrace();//打印堆棧信息
System.err.println("出現異常信息:"+e.getMessage());
}
System.out.println("其他業務代碼........");
input.close();
}
}
執行結果:
請輸入第一個數:100
請輸入第二個數 :0
java.lang.ArithmeticException: / by zero
其他業務代碼........
at ch003.Demo2.main(Demo2.java:14)
出現異常信息:/ by zero
出現算術異常,執行了catch代碼塊,異常處理成功;後續業務也得到執行。
③出現異常、捕獲失敗
package ch003;
import java.util.Scanner;
public class Demo2 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
try {
System.out.print("請輸入第一個數:");
int num1 = input.nextInt();
System.out.print("請輸入第二個數 :");
int num2 = input.nextInt();
int num3 = num1 / num2;
System.out.println(num3);
} catch (ArithmeticException e) {
e.printStackTrace();//打印堆棧信息
System.err.println("出現異常信息:"+e.getMessage());
}
System.out.println("其他業務代碼........");
input.close();
}
}
執行結果:
請輸入第一個數:xxxx
Exception in thread "main" java.util.InputMismatchException
at java.util.Scanner.throwFor(Unknown Source)
at java.util.Scanner.next(Unknown Source)
at java.util.Scanner.nextInt(Unknown Source)
at java.util.Scanner.nextInt(Unknown Source)
at ch003.Demo2.main(Demo2.java:11)
出現的異常和捕獲的異常類型不匹配,系統直接終止執行。闖紅燈: 判刑50年,這個處理就不匹配;你還去不去上班,後續業務就不會執行。
多個catch
package ch003;
import java.util.InputMismatchException;
import java.util.Scanner;
public class Demo2 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
try {
System.out.print("請輸入第一個數:");
int num1 = input.nextInt();
System.out.print("請輸入第二個數 :");
int num2 = input.nextInt();
int num3 = num1 / num2;
System.out.println(num3);
}catch (ArithmeticException e) {
e.printStackTrace();//打印堆棧信息
System.err.println("算術異常信息:"+e.getMessage());
}catch (InputMismatchException e) {
e.printStackTrace();//打印堆棧信息
System.err.println("輸入不匹配異常:"+e.getMessage());
}catch (Exception e) {
e.printStackTrace();//打印堆棧信息
System.err.println("未知異常信息:"+e.getMessage());
}
System.out.println("其他業務代碼........");
input.close();
}
}
try可以跟多個catch代碼塊; catch的順序: 從小到大,從具體類到一般,從子類到父類。另外一種簡化的寫法:
package ch003;
import java.util.InputMismatchException;
import java.util.Scanner;
public class Demo3 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
try {
System.out.print("請輸入第一個數:");
int num1 = input.nextInt();
System.out.print("請輸入第二個數 :");
int num2 = input.nextInt();
int num3 = num1 / num2;
System.out.println(num3);
}catch (ArithmeticException|InputMismatchException|NullPointerException e) {
e.printStackTrace();//打印堆棧信息
System.err.println("異常信息:"+e.getMessage());
}catch (Exception e) {
System.err.println("異常信息:"+e.getMessage());
}
System.out.println("其他業務代碼........");
input.close();
}
}
後面跟一個Exception是爲了保證能夠處理未知異常,catch捕獲了已知的異常,有個兜底的處理。
異常方法
下面的列表是 Throwable 類的主要方法:
序號 | 方法及說明 |
---|---|
1 | public String getMessage() 返回關於發生的異常的詳細信息。這個消息在Throwable 類的構造函數中初始化了。 |
2 | public Throwable getCause() 返回一個Throwable 對象代表異常原因。 |
3 | public String toString() 使用getMessage()的結果返回類的串級名字。 |
4 | public void printStackTrace() 打印toString()結果和棧層次到System.err,即錯誤輸出流。 |
5 | public StackTraceElement [] getStackTrace() 返回一個包含堆棧層次的數組。下標爲0的元素代表棧頂,最後一個元素代表方法調用堆棧的棧底。 |
6 | public Throwable fillInStackTrace() 用當前的調用棧層次填充Throwable 對象棧層次,添加到棧層次任何先前信息中。 |
try-catch-finally
finally表示最終的,不管是否出現異常都會執行的代碼, IO流的關閉,數據庫連接的關閉。
package ch003;
import java.util.Scanner;
public class Demo4 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
try {
System.out.print("請輸入第一個數:");
int num1 = input.nextInt();
System.out.print("請輸入第二個數 :");
int num2 = input.nextInt();
int num3 = num1 / num2;
System.out.println(num3);
}catch (Exception e) {
System.err.println("異常信息:"+e.getMessage());
}finally {
System.out.println("===finally===");
}
System.out.println("其他業務代碼........");
input.close();
}
}
沒有異常:
請輸入第一個數:100
請輸入第二個數 :50
2
===finally===
其他業務代碼........
出現異常:
請輸入第一個數:100
請輸入第二個數 :0
異常信息:/ by zero
===finally===
其他業務代碼........
finally塊和return
- 首先一個不容易理解的事實:在 try塊中即便有return,break,continue等改變執行流的語句,finally也會執行
- finally中的return 會覆蓋 try 或者catch中的返回值,如果只修改變量的值沒有return語句,不會改變變量的值。
- finally中的return或異常會抑制(消滅)前面try或者catch塊中的異常。
package com.hanker.oop7;
public class Demo1 {
public static void main(String[] args) {
System.out.println(add(1,2));
}
public static int add(int a,int b) {
int c = 0;
try {
c = a + b;
return c;
} catch (Exception e) {
e.printStackTrace();
return 0;
}finally {
c = 1000;//2.沒有返回語句不會改變變量c的值,還是返回 3
System.out.println("finally");
//return c;//1.有返回語句會改變返回值
}
}
}
總結: 整體異常處理的流程
throw和throws
throw是手動拋出一個異常信息,一般會用於程序出現某種邏輯時程序員主動拋出某種特定類型的異常。throw只會出現在方法體中,當方法在執行過程中遇到異常情況時,將異常信息封裝爲異常對象,然後throw出去。throw關鍵字的一個非常重要的作用就是異常類型的轉換。
package ch003;
public class Student {
private String sex;
public String getSex() {
return sex;
}
public void setSex(String sex) throws Exception {
if( sex.equals("男") || sex.equals("女") ) {
this.sex = sex;
}else {
System.out.println("輸入錯誤");
throw new Exception("性別只能是男或女");
}
}
}
我們通過throw拋出一個異常信息,表示該方法有可能出現異常,問題來了: 別人在調用你的方法時如何瞭解你是否拋出異常了? 所以throw會配合另一個關鍵字: throws。throws在方法聲明的地方指定拋出的異常信息,當別的同事看到你的方法聲明的時候就可以瞭解你的方法出現什麼異常,一般不會到你的代碼裏閱讀。再次強調:throws出現在方法的聲明中,表示該方法可能會拋出的異常,然後交給上層調用它的方法程序處理,允許throws後面跟着多個異常類型.
聲明方法拋出異常帶來的問題: 方法調用者如何處理你拋出的異常;兩個辦法:
①繼續拋出,拋給上一級
package ch003;
public class StudentTest {
//main方法拋給jvm
public static void main(String[] args) throws Exception {
Student s = new Student();
s.setSex("xxx");
}
}
②處理異常,try–catch
package ch003;
public class StudentTest {
public static void main(String[] args) {
Student s = new Student();
try {
s.setSex("x");
} catch (Exception e) {
System.out.println("=出現異常="+e.getMessage());
}
}
}
自定義異常
上面我們拋出的異常用的類型是什麼?Exception類型的,但是它是所有異常的跟類,可以理解爲抽象類(但不是抽象類)描述的是所有的異常類型,不是具體的異常類。所以我們可以自定義一個異常類型,來描述性別的異常信息。如何自定義異常類型?
按照國際慣例,自定義的異常應該總是包含如下的構造函數:
一個無參構造函數
一個帶有String參數的構造函數,並傳遞給父類的構造函數。
一個帶有String參數和Throwable參數,並都傳遞給父類構造函數
一個帶有Throwable 參數的構造函數,並傳遞給父類的構造函數。
下面是IOException類的完整源代碼,可以借鑑。
package java.io;
public class IOException extends Exception {
static final long serialVersionUID = 7818375828146090155L;
public IOException() {
super();
}
public IOException(String message) {
super(message);
}
public IOException(String message, Throwable cause) {
super(message, cause);
}
public IOException(Throwable cause) {
super(cause);
}
}
自定義的異常
package ch003;
public class SexException extends Exception {
public SexException() {
super();
}
public SexException(String message) {
super(message);
}
public SexException(String message, Throwable cause) {
super(message, cause);
}
public SexException(Throwable cause) {
super(cause);
}
}
使用自定義的異常類型:
package ch003;
public class Student {
private String sex;
public String getSex() {
return sex;
}
public void setSex(String sex) throws SexException {
if( sex.equals("男") || sex.equals("女") ) {
this.sex = sex;
}else {
System.out.println("輸入錯誤");
throw new SexException("性別只能是男或女");
}
}
}
測試類:
package ch003;
public class StudentTest {
//main方法拋給jvm
public static void main(String[] args) {
Student s = new Student();
try {
s.setSex("xxx");
} catch (SexException e) {
e.printStackTrace();
}
}
}
方法重寫如何拋出異常
子類重寫父類方法的時候,如何確定異常拋出聲明的類型。下面是三點原則:
(1)父類的方法沒有聲明異常,子類在重寫該方法的時候不能聲明異常;
(2)如果父類的方法聲明一個異常exception1,則子類在重寫該方法的時候聲明的異常不能是exception1的父類;
(3)如果父類的方法聲明的異常類型只有非運行時異常(運行時異常),則子類在重寫該方法的時候聲明的異常也只能有非運行時異常(運行時異常),不能含有運行時異常(非運行時異常)。
異常處理和設計的幾個建議
1.只在必要使用異常的地方纔使用異常,不要用異常去控制程序的流程
2.切忌使用空catch塊
3.檢查異常和非檢查異常的選擇,建議儘量避免檢查異常的使用,如果確實該異常情況的出現很普遍,需要提醒調用者注意處理的話,就使用檢查異常;否則使用非檢查異常。
4.注意catch塊的順序
5.不要將提供給用戶看的信息放在異常信息裏
比如下面這段代碼:
public class Main {
public static void main(String[] args) {
try {
String user = null;
String pwd = null;
login(user,pwd);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
public static void login(String user,String pwd) {
if(user==null||pwd==null)
throw new NullPointerException("用戶名或者密碼爲空");
//...
}
}
6.避免多次在日誌信息中記錄同一個異常
7.異常處理儘量放在高層進行
8.在finally中釋放資源
如果有使用文件讀取、網絡操作以及數據庫操作等,記得在finally中釋放資源。這樣不僅會使得程序佔用更少的資源,也會避免不必要的由於資源未釋放而發生的異常情況。
日誌處理
爲什麼需要日誌系統,核心是可以跟蹤業務代碼的執行,特別是系統出現問題,我們可以跟據日誌找到出現問題的 類甚至第幾行;有了日誌我們就可以快速定位系統問題,進而可以幫助我們解決問題。視頻監控也是一種日誌形式,但是系統中都是日誌文件: ELK分析日誌: ElasticSearch+LogBack+Kibana; 日誌處理有很多框架: jdk自帶有日誌處理的類庫,常見的Log4j,
①添加log4j的jar包
②編寫屬性文件
就是日誌的輸出級別,輸出的目的地,輸出的格式:
### 把日誌信息輸出到控制檯 ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n
### 把日誌信息輸出到文件 ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l %m%n
### 設置優先級別、以及輸出源 ###
log4j.rootLogger=debug, stdout,file
注意文件位置:直接放到src下面,不是包裏面。
屬性文件的解釋:
stdout: standard out 標準輸出流, 就是輸出到控制檯
log4j.appender.file.File=mylog.log 指定輸出的日誌的文件名稱
%d{yyyy-MM-dd HH:mm:ss} %l %m%n
%d 表示日期佔位符, 大括號裏是格式
%l 表示location第幾行輸出的日誌
%m表示輸出日誌信息 message
%n表示輸出換行
日誌的級別:
debug: 最低級別,調試日誌
info: 一般信息日誌
warn: 警告日誌信息
error: 錯誤日誌信息
fatal: 致命錯誤日誌信息
debug < info < warn < error < fatal 如果指定warn級別則輸出的包括 debug,info,warn
log4j.rootLogger=error, stdout,file 輸出的日誌是 >= error級別的。
③在程序中編寫日誌代碼
package ch003;
import org.apache.log4j.Logger;
public class Student {
private Logger log = Logger.getLogger(Student.class);
private String sex;
public Student() {
log.debug("創建Student對象");
}
public String getSex() {
return sex;
}
//聲明拋出
public void setSex(String sex) throws SexException {
log.info("設置性別: "+ sex);
if( sex.equals("男") || sex.equals("女") ) {
this.sex = sex;
log.info("成功設置性別");
}else {
log.error("輸入有誤,性別只能是男或女.....");
//手動拋出
throw new SexException("性別只能是男或女");
}
}
}
測試類
package ch003;
import org.apache.log4j.Logger;
public class StudentTest {
private static Logger log = Logger.getLogger(StudentTest.class);
//main方法拋給jvm
public static void main(String[] args) {
log.debug("測試類開始執行....");
Student s = new Student();
try {
s.setSex("xxx");
} catch (SexException e) {
e.printStackTrace();
}
}
}
注意查看文件之前選擇項目,右鍵刷新即可看到日誌文件。