異常
Java筆記目錄可以點這裏:Java 強化筆記
開發中的錯誤(語法錯誤、邏輯錯誤、異常)
在開發 Java 程序的過程中,會遇到各種各樣的錯誤:
- 語法錯誤
會導致編譯失敗,程序無法正常運行 - 邏輯錯誤
比如需要執行加法操作時,不小心寫成了減法操作 - 運行時錯誤
在程序運行過程中產生的意外,會導致程序終止運行
在 Java 中也叫做異常
程序產生了異常,一般稱之爲:拋出了異常
- 如果沒有主動去處理它,會導致程序終止運行
例:如果【代碼2】拋出了異常,並且沒有主動去處理它,程序就會退出,【代碼3】將沒有機會執行。
pubilc staic void main(String[] args) {
【代碼1】
【代碼2】
【代碼3】
}
思考:下面代碼的打印結果是什麼?
public static void main(String[] args) {
System.out.println(1);
Integer i1 = new Integer("123");
System.out.println(2);
Integer i2 = new Integer("abc");
System.out.println(3);
}
由於 "abc"
無法轉換成整數,new Integer("abc")
會拋出一個異常:
- 異常類型:
java.lang.NumberFormatException
- 由於沒有主動去處理這個異常,所以導致程序終止運行
思考:下面代碼的打印結果是什麼?
public static void main(String[] args) {
System.out.println(1);
Integer i = new Integer("1234");
Object obj = "1/2.34";
Double d = (Double)obj;
System.out.println(2);
}
由於 "12.34"
無法強轉成 Double
類型,(Double)Object
會拋出一個異常
- 異常類型:
java.lang.ClassCastException
- 由於沒有主動去處理這個異常,所以導致程序終止運行
思考:下面代碼的打印結果是什麼?
public static void main(String[] args) {
Integer[] nums = { 11, null, 22 };
for (int num : nums) {
System.out.println(num);
}
}
Integer
在自動拆箱爲 int
時,會調用 Integer
對象的 inValue()
方法;
由於 nums[1]
爲 null
,使用 null
調用方法會拋出一個異常
- 異常類型:
java.lang.NullPointerException
- 由於沒有主動去處理這個異常,所以導致程序終止運行
上面拋出異常的代碼建議這麼寫:
Integer[] nums= { 11, null, 22 };
for (Integer num : nums) {
System.out.println(num);
}
打印的細節:
Java 中直接打印 null 會報錯;
但是有時候確實可以把 null 打印出來:
public class Dog {
@Override
public String toString() {
return "Dog - 666";
}
}
Dog dog = new Dog();
System.out.println(dog); // Doge - 666
Dog dog2 = null;
System.out.println(dog2); // null
爲什麼 null 能被打印出來呢?可以看一下 println
的底層實現:發現調用了 String.valueOf
public void println(Object x) {
String s = String.valueOf(x);
synchronized (this) {
print(s);
newLine();
}
}
再看看 String.valueOf
:可以發現打印出 null 其實是經過處理的。
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
異常(Exception)
Java 中有各種各樣的異常
- 所有的異常最終都繼承自
java.lang.Throwable
如何防止程序因爲拋出異常導致終止運行?
- 可以通過
try-catch
來捕捉處理異常
try-catch
思考:下面代碼的打印結果是什麼?
public static void main(String[] args) {
System.out.println(1);
try {
System.out.println(2);
Integer i = new Integer("abc");
System.out.println(3);
} catch (NumberFormatException e) {
System.out.println(4);
}
System.out.println(5);
}
1
2
4
5
一個 catch
捕獲多種類型的異常:
try {
} catch(異常A | 異常B | 異常 C) {
// 當拋出【異常A】或【異常B】或【異常C】類型的異常時,會進入這個代碼塊
}
- 從 Java 7 開始,單個 catch 可以捕獲多種類型的異常
- 如果並列的幾個異常類型之間存在父子關係,保留父類型即可
- 這裏的變量 e 是隱式 final 的
異常對象的常用方法(getMessage、printStackTrace)
try {
Integer integer = new Integer("abc");
} catch (NumberFormatException e) {
// 異常描述
System.out.println(e.getMessage());
// 異常名稱 + 異常
System.out.println(e);
// 打印堆棧信息
e.printStackTrace();
}
finally
try
或 catch
正常執行完畢後,一定會執行 finally
中的代碼:
finally
可以和try-catch
搭配使用,也可以只和try
搭配使用- 經常會在
finally
中編寫一些關閉、釋放資源 的代碼(比如關閉文件)
finally
細節:
如果在執行 try
或 catch
時,JVM 退出或者當前線程被中斷、殺死
finally
可能不會執行
如果 try
或 catch
中使用了 return
、break
、continue
等提前結束語句
finally
會在return
、break
、continue
之前執行
思考:下面代碼的打印結果是什麼?
for (int i = 1; i <= 3; i++) {
try{
System.out.println(i + "_try_1");
if (i == 2) continue;
System.out.println(i + "_try_2");
} finally {
System.out.println(i + "_finally");
}
}
1_try_1
1_try_2
1_finally
2_try_1
2_finally
3_try_1
3_try_2
3_finally
for (int i = 1; i <= 3; i++) {
try{
System.out.println(i + "_try_1");
if (i == 2) break;
System.out.println(i + "_try_2");
} finally {
System.out.println(i + "_finally");
}
}
1_try_1
1_try_2
1_finally
2_try_1
2_finally
public static void main(String[] args) {
System.out.println(get());
}
static int get() {
try {
new Integer("abc");
System.out.println(1);
return 2;
} catch (Exception e) {
System.out.println(3);
return 4;
} finally {
System.out.println(5);
}
}
3
5
4
異常的種類(檢查型異常、非檢查型異常)
檢查型異常(Checked Exception)
- 這類異常一般難以避免,編譯器會進行檢查
如果開發者沒有處理這類異常,編譯器將會報錯 - 哪些異常是檢查型異常?
除Error
、RuntimeException
以外的異常
非檢查型異常(Unchecked Exception)
- 這類異常一般可以避免,編譯器不會進行檢查
如果開發者沒有處理這類異常,編譯器將不會報錯 - 哪些異常是非檢查型異常?
Error
、RuntimeException
常見的檢查型異常:
常見的非檢查型異常 – Error
常見的非檢查型異常 – RuntimeException
throws(作用、流程、細節)
throws
的作用:將異常拋給上層方法
void test() throws FileNotFoundException, ClassNotFoundException {
PrintWriter out = new PrintWriter("F:/mj/520it.txt");
Class cls = Class.forName("Dog");
}
如果 throws
後面的異常類型存在父子關係,保留父類型即可
void test() throws Exception {
PrintWriter out = new PrintWriter("F:/mj/520it.txt");
Class cls = Class.forName("Dog");
}
void test() throws Throwable {
PrintWriter out = new PrintWriter("F:/mj/520it.txt");
Class cls = Class.forName("Dog");
}
可以一部分異常使用 try-catch
處理,另一部分異常使用 throws
處理:
void test() throws FileNotFoundException {
PrintWriter out = new PrintWriter("F:/mj/520it.txt");
try {
Class cls = Class.forName("Dog");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
thorws
的流程:
- 如果異常最終拋給了 JVM,那麼整個 Java 程序將終止運行。
下面這個例子中,異常從 method1
開始網上拋,到 method2
,再到 method3
,最後拋給 main
,main
又拋給了 JVM;
public static void main(String[] args) throws ClassNotFoundException {
method1(); // 正常來說, 這裏必須處理異常了, 否則出異常後Java程序會終止運行
}
static void method1() throws ClassNotFoundException {
method2(); // 這裏可以處理異常
}
static void method2() throws ClassNotFoundException {
method3(); // 這裏可以處理異常
}
static void method3() throws ClassNotFoundException{
Class clazz = Class.forName("Dog"); // 這裏可以處理異常
}
所以如果一直不處理異常,只往上拋最終會導致程序停止運行,一定要在某個地方處理異常。
throws
的細節
當父類的方法沒有 throws
異常:子類的重寫方法也不能 throws
異常
當父類的方法有 throws
異常,子類的重寫方法可以:
- 不
throws
異常 throws
跟父類一樣的異常throws
父類異常的子類型
public class Person {
public void test1() {}
public void test2() throws IOException {}
public void test3() throws IOException {}
public void test4() throws IOException {}
public void test5() throws IOException {}
}
public class Student extends Person {
@Override
public void test1() {}
@Override
public void test2() {}
@Override
public void test3() throws IOException {}
@Override
public void test4() throws FileNotFoundException {}
// 只能拋出父類異常的子類, 或者不拋
// public void test5() throws Exception {} // 拋出子類異常的父類異常, 會報錯
}
throw
使用 throw
可以拋出一個新建的異常
public class Person {
public Person(String name) throws Exception { // 拋出異常
if (name == null || name.length() == 0) {
// 檢查型異常必須處理或者往上拋
throw new Exception("name must not be empty.");
}
}
}
public class Person {
public Person(String name) {
if (name == null || name.length() == 0) {
// 非檢查型異常可以不處理
throw new IllegalArgumentException("name must not be empty.");
}
}
}
自定義異常(Exception、RuntimeException)
開發中自定義的異常類型,基本都是以下 2 種做法:
- 繼承自
Exception
使用起來代碼會稍微複雜
希望開發者重視這個異常、認真處理這個異常(該異常無法從代碼層面去避免,必須引起重視) - 繼承自
RuntimeException
使用起來代碼會更加簡潔
不嚴格要求開發者去處理這個異常(因爲規範的代碼可以避免該異常)
示例:
自定義兩種異常:EmptyNameException
、WrongAgeException
public class EmptyNameException extends RuntimeException {
public EmptyNameException() {
super("name must not be empty");
}
}
public class WrongAgeException extends RuntimeException {
private int age;
public WrongAgeException(int age) {
super("wrong age:" + age + ", age must be > 0");
}
}
在 Person
類中使用自定義的異常:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
if (name == null || name.length() == 0) {
throw new EmptyNameException();
}
if (age <= 0) {
throw new WrongAgeException(age);
}
this.name = name;
this.age = age;
}
}
使用 Person
類有機率拋出異常:
public static void main(String[] args) {
//WrongAgeException: wrong age:-10, age must be > 0
Person person = new Person("Jack", -10);
// 這句代碼不會執行
System.out.println(1);
}
使用異常的好處
- 將錯誤處理代碼與普通代碼區分開
- 能將錯誤信息傳播到調用堆棧中
- 能對錯誤類型進行區分和分組
try-with-resources 語句
具體見:File、字符集、字節流、字符流、緩衝流、數據流、對象流、序列化、try-with-resources語句
從Java7開始推出的try-with-resources
語句(可以沒有catch
、finally
)
try(資源1; 資源2; ...) {
} catch (Exception e) {
} finally {
}
可以在try
後面的小括號中聲明一個或多個資源(resource)
- 實現了
java.lang.AutoCloseable
接口的實例,都可以稱之爲是資源
不管try
中的語句是正常還是意外結束
- 最終都會自動按順序調用每一個資源的
close
方法(close
方法的調用順序與資源的聲明順序相反) - 調用完所有資源的
close
方法後,再執行finally
中的語句
練習 - 編寫一個斷言類
/*
* 斷言類
*/
public class Asserts {
public static void test(boolean v) {
if (v) return;
// throw new RuntimeException();
System.err.println(new RuntimeException().getStackTrace()[1]);
}
}
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
// int age = 10;
// Asserts.test(age > 0);
// String name = "";
// Asserts.test(name != null && name.length() != 0);
Asserts.test(add(10, 20) == 30);
Asserts.test(sub(20, 10) == 20);
}
static int add(int num1, int num2) {
return num1 + num2;
}
static int sub(int num1, int num2) {
return num1 - num2;
}
}