-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Expand file tree
/
Copy pathgailan.md
More file actions
494 lines (353 loc) · 21.7 KB
/
gailan.md
File metadata and controls
494 lines (353 loc) · 21.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
---
title: 一文彻底搞懂Java异常处理,YYDS
shortTitle: Java异常处理全面解析
category:
- Java核心
tag:
- 异常处理
description: 本文详细介绍了Java异常处理的基本概念、种类和原则,以及如何使用try-catch-finally和throw、throws关键字进行异常处理。通过实际案例和代码示例,帮助读者掌握Java异常处理的关键技巧,提高编程中的错误检测与处理能力。
head:
- - meta
- name: keywords
content: Java,Java异常处理, try-catch-finally, throw, throws,异常处理,java 异常处理
---
### 01、什么是异常
“二哥,今天就要学习异常了吗?”三妹问。
“是的。只有正确地处理好异常,才能保证程序的可靠性,所以异常的学习还是很有必要的。”我说。
“那到底什么是异常呢?”三妹问。
“异常是指中断程序正常执行的一个不确定的事件。当异常发生时,程序的正常执行流程就会被打断。一般情况下,程序都会有很多条语句,如果没有异常处理机制,前面的语句一旦出现了异常,后面的语句就没办法继续执行了。”
“有了异常处理机制后,程序在发生异常的时候就不会中断,我们可以对异常进行捕获,然后改变程序执行的流程。”
“除此之外,异常处理机制可以保证我们向用户提供友好的提示信息,而不是程序原生的异常信息——用户根本理解不了。”
“不过,站在开发者的角度,我们更希望看到原生的异常信息,因为这有助于我们更快地找到 bug 的根源,反而被过度包装的异常信息会干扰我们的视线。”
“Java 语言在一开始就提供了相对完善的异常处理机制,这种机制大大降低了编写可靠程序的门槛,这也是 Java 之所以能够流行的原因之一。”
“那导致程序抛出异常的原因有哪些呢?”三妹问。
比如说:
- 程序在试图打开一个不存在的文件;
- 程序遇到了网络连接问题;
- 用户输入了糟糕的数据;
- 程序在处理算术问题时没有考虑除数为 0 的情况;
等等等等。
挑个最简单的原因来说吧。
```java
public class Demo {
public static void main(String[] args) {
System.out.println(10/0);
}
}
```
这段代码在运行的时候抛出的异常信息如下所示:
```
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.itwanger.s41.Demo.main(Demo.java:8)
```
“你看,三妹,这个原生的异常信息对用户来说,显然是不太容易理解的,但对于我们开发者来说,简直不要太直白了——很容易就能定位到异常发生的根源。”
### 02、Exception和Error的区别
“哦,我知道了。下一个问题,我经常看到一些文章里提到 Exception 和 Error,二哥你能帮我解释一下它们之间的区别吗?”三妹问。
“这是一个好问题呀,三妹!”
从单词的释义上来看,error 为错误,exception 为异常,错误的等级明显比异常要高一些。
从程序的角度来看,也的确如此。
Error 的出现,意味着程序出现了严重的问题,而这些问题不应该再交给 Java 的异常处理机制来处理,程序应该直接崩溃掉,比如说 OutOfMemoryError,内存溢出了,这就意味着程序在运行时申请的内存大于系统能够提供的内存,导致出现的错误,这种错误的出现,对于程序来说是致命的。
Exception 的出现,意味着程序出现了一些在可控范围内的问题,我们应当采取措施进行挽救。
比如说之前提到的 ArithmeticException,很明显是因为除数出现了 0 的情况,我们可以选择捕获异常,然后提示用户不应该进行除 0 操作,当然了,更好的做法是直接对除数进行判断,如果是 0 就不进行除法运算,而是告诉用户换一个非 0 的数进行运算。
### 03、checked和unchecked异常
“三妹,还能想到其他的问题吗?”
“嗯,不用想,二哥,我已经提前做好预习工作了。”三妹自信地说,“异常又可以分为 checked 和 unchecked,它们之间又有什么区别呢?”
“哇,三妹,果然又是一个好问题呢。”
checked 异常(检查型异常)在源代码里必须显式地捕获或者抛出,否则编译器会提示你进行相应的操作;而 unchecked 异常(非检查型异常)就是所谓的运行时异常,通常是可以通过编码进行规避的,并不需要显式地捕获或者抛出。
“我先画一幅思维导图给你感受一下。”

首先,Exception 和 Error 都继承了 Throwable 类。换句话说,只有 Throwable 类(或者子类)的对象才能使用 throw 关键字抛出,或者作为 catch 的参数类型。
面试中经常问到的一个问题是,NoClassDefFoundError 和 ClassNotFoundException 有什么区别?
“三妹你知道吗?”
“不知道,二哥,你解释下呗。”
它们都是由于系统运行时找不到要加载的类导致的,但是触发的原因不一样。
- NoClassDefFoundError:程序在编译时可以找到所依赖的类,但是在运行时找不到指定的类文件,导致抛出该错误;原因可能是 jar 包缺失或者调用了初始化失败的类。
- ClassNotFoundException:当动态加载 Class 对象的时候找不到对应的类时抛出该异常;原因可能是要加载的类不存在或者类名写错了。
其次,像 IOException、ClassNotFoundException、SQLException 都属于 checked 异常;像 RuntimeException 以及子类 ArithmeticException、ClassCastException、ArrayIndexOutOfBoundsException、NullPointerException,都属于 unchecked 异常。
unchecked 异常可以不在程序中显示处理,就像之前提到的 ArithmeticException 就是的;但 checked 异常必须显式处理。
比如说下面这行代码:
```java
Class clz = Class.forName("com.itwanger.s41.Demo1");
```
如果没做处理,比如说在 Intellij IDEA 环境下,就会提示你这行代码可能会抛出 `java.lang.ClassNotFoundException`。

建议你要么使用 try-catch 进行捕获:
```java
try {
Class clz = Class.forName("com.itwanger.s41.Demo1");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
```
注意打印异常堆栈信息的 `printStackTrace()` 方法,该方法会将异常的堆栈信息打印到标准的控制台下,如果是测试环境,这样的写法还 OK,如果是生产环境,这样的写法是不可取的,必须使用日志框架把异常的堆栈信息输出到日志系统中,否则可能没办法跟踪。
要么在方法签名上使用 throws 关键字抛出:
```java
public class Demo1 {
public static void main(String[] args) throws ClassNotFoundException {
Class clz = Class.forName("com.itwanger.s41.Demo1");
}
}
```
这样做的好处是不需要对异常进行捕获处理,只需要交给 Java 虚拟机来处理即可;坏处就是没法针对这种情况做相应的处理。
“二哥,针对 checked 异常,我在知乎上看到一个帖子,说 Java 中的 checked 很没有必要,这种异常在编译期要么 try-catch,要么 throws,但又不一定会出现异常,你觉得这样的设计有意义吗?”三妹提出了一个很尖锐的问题。
“哇,这种问题问的好。”我不由得对三妹心生敬佩。
“的确,checked 异常在业界是有争论的,它假设我们捕获了异常,并且针对这种情况作了相应的处理,但有些时候,根本就没法处理。”我说,“就拿上面提到的 ClassNotFoundException 异常来说,我们假设对其进行了 try-catch,可真的出现了 ClassNotFoundException 异常后,我们也没多少的可操作性,再 `Class.forName()` 一次?”
另外,checked 异常也不兼容函数式编程,后面如果你写 Lambda/Stream 代码的时候,就会体验到这种苦涩。
当然了,checked 异常并不是一无是处,尤其是在遇到 IO 或者网络异常的时候,比如说进行 Socket 链接,我大致写了一段:
```java
public class Demo2 {
private String mHost;
private int mPort;
private Socket mSocket;
private final Object mLock = new Object();
public void run() {
}
private void initSocket() {
while (true) {
try {
Socket socket = new Socket(mHost, mPort);
synchronized (mLock) {
mSocket = socket;
}
break;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
```
当发生 IOException 的时候,socket 就重新尝试连接,否则就 break 跳出循环。意味着如果 IOException 不是 checked 异常,这种写法就略显突兀,因为 IOException 没办法像 ArithmeticException 那样用一个 if 语句判断除数是否为 0 去规避。
或者说,强制性的 checked 异常可以让我们在编程的时候去思考,遇到这种异常的时候该怎么更优雅的去处理。显然,Socket 编程中,肯定是会遇到 IOException 的,假如 IOException 是非检查型异常,就意味着开发者也可以不考虑,直接跳过,交给 Java 虚拟机来处理,但我觉得这样做肯定更不合适。
### 04、关于 throw 和 throws
“二哥,你能告诉我 throw 和 throws 两个关键字的区别吗?”三妹问。
“throw 关键字,用于主动地抛出异常;正常情况下,当除数为 0 的时候,程序会主动抛出 ArithmeticException;但如果我们想要除数为 1 的时候也抛出 ArithmeticException,就可以使用 throw 关键字主动地抛出异常。”我说。
```java
throw new exception_class("error message");
```
语法也非常简单,throw 关键字后跟上 new 关键字,以及异常的类型还有参数即可。
举个例子。
```java
public class ThrowDemo {
static void checkEligibilty(int stuage){
if(stuage<18) {
throw new ArithmeticException("年纪未满 18 岁,禁止观影");
} else {
System.out.println("请认真观影!!");
}
}
public static void main(String args[]){
checkEligibilty(10);
System.out.println("愉快地周末..");
}
}
```
这段代码在运行的时候就会抛出以下错误:
```
Exception in thread "main" java.lang.ArithmeticException: 年纪未满 18 岁,禁止观影
at com.itwanger.s43.ThrowDemo.checkEligibilty(ThrowDemo.java:9)
at com.itwanger.s43.ThrowDemo.main(ThrowDemo.java:16)
```
“throws 关键字的作用就和 throw 完全不同。”我说,“前面的小节里已经讲了 checked exception 和 unchecked exception,也就是检查型异常和非检查型异常;对于检查型异常来说,如果你没有做处理,编译器就会提示你。”
`Class.forName()` 方法在执行的时候可能会遇到 `java.lang.ClassNotFoundException` 异常,一个检查型异常,如果没有做处理,IDEA 就会提示你,要么在方法签名上声明,要么放在 try-catch 中。

“那什么情况下使用 throws 而不是 try-catch 呢?”三妹问。
“假设现在有这么一个方法 `myMethod()`,可能会出现 ArithmeticException 异常,也可能会出现 NullPointerException。这种情况下,可以使用 try-catch 来处理。”我回答。
```java
public void myMethod() {
try {
// 可能抛出异常
} catch (ArithmeticException e) {
// 算术异常
} catch (NullPointerException e) {
// 空指针异常
}
}
```
“但假设有好几个类似 `myMethod()` 的方法,如果为每个方法都加上 try-catch,就会显得非常繁琐。代码就会变得又臭又长,可读性就差了。”我继续说。
“一个解决办法就是,使用 throws 关键字,在方法签名上声明可能会抛出的异常,然后在调用该方法的地方使用 try-catch 进行处理。”
```java
public static void main(String args[]){
try {
myMethod1();
} catch (ArithmeticException e) {
// 算术异常
} catch (NullPointerException e) {
// 空指针异常
}
}
public static void myMethod1() throws ArithmeticException, NullPointerException{
// 方法签名上声明异常
}
```
“好了,我来总结下 throw 和 throws 的区别,三妹,你记一下。”
1)throws 关键字用于声明异常,它的作用和 try-catch 相似;而 throw 关键字用于显式的抛出异常。
2)throws 关键字后面跟的是异常的名字;而 throw 关键字后面跟的是异常的对象。
示例。
```java
throws ArithmeticException;
```
```java
throw new ArithmeticException("算术异常");
```
3)throws 关键字出现在方法签名上,而 throw 关键字出现在方法体里。
4)throws 关键字在声明异常的时候可以跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常。
### 05、关于 try-catch-finally
“二哥,之前你讲了异常处理机制,这一节讲什么呢?”三妹问。
“该讲 try-catch-finally 了。”我说,“try 关键字后面会跟一个大括号 `{}`,我们把一些可能发生异常的代码放到大括号里;`try` 块后面一般会跟 `catch` 块,用来处理发生异常的情况;当然了,异常不一定会发生,为了保证发不发生异常都能执行一些代码,就会跟一个 `finally` 块。”
“具体该怎么用呀,二哥?”三妹问。
“别担心,三妹,我一一来说明下。”我说。
`try` 块的语法很简单:
```java
try{
// 可能发生异常的代码
}
```
“注意啊,三妹,如果一些代码确定不会抛出异常,就尽量不要把它包裹在 `try` 块里,因为加了异常处理的代码执行起来要比没有加的花费更多的时间。”
`catch` 块的语法也很简单:
```java
try{
// 可能发生异常的代码
}catch (exception(type) e(object)){
// 异常处理代码
}
```
一个 `try` 块后面可以跟多个 `catch` 块,用来捕获不同类型的异常并做相应的处理,当 try 块中的某一行代码发生异常时,之后的代码就不再执行,而是会跳转到异常对应的 catch 块中执行。
如果一个 try 块后面跟了多个与之关联的 catch 块,那么应该把特定的异常放在前面,通用型的异常放在后面,不然编译器会提示错误。举例来说。
```java
static void test() {
int num1, num2;
try {
num1 = 0;
num2 = 62 / num1;
System.out.println(num2);
System.out.println("try 块的最后一句");
} catch (ArithmeticException e) {
// 算术运算发生时跳转到这里
System.out.println("除数不能为零");
} catch (Exception e) {
// 通用型的异常意味着可以捕获所有的异常,它应该放在最后面,
System.out.println("异常发生了");
}
System.out.println("try-catch 之外的代码.");
}
```
“为什么 Exception 不能放到 ArithmeticException 前面呢?”三妹问。
“因为 ArithmeticException 是 Exception 的子类,它更具体,我们看到就这个异常就知道是发生了算术错误,而 Exception 比较泛,它隐藏了具体的异常信息,我们看到后并不确定到底是发生了哪一种类型的异常,对错误的排查很不利。”我说,“再者,如果把通用型的异常放在前面,就意味着其他的 catch 块永远也不会执行,所以编译器就直接提示错误了。”
“再给你举个例子,注意看,三妹。”
```java
static void test1 () {
try{
int arr[]=new int[7];
arr[4]=30/0;
System.out.println("try 块的最后");
} catch(ArithmeticException e){
System.out.println("除数必须是 0");
} catch(ArrayIndexOutOfBoundsException e){
System.out.println("数组越界了");
} catch(Exception e){
System.out.println("一些其他的异常");
}
System.out.println("try-catch 之外");
}
```
这段代码在执行的时候,第一个 catch 块会执行,因为除数为零;我再来稍微改动下代码。
```java
static void test1 () {
try{
int arr[]=new int[7];
arr[9]=30/1;
System.out.println("try 块的最后");
} catch(ArithmeticException e){
System.out.println("除数必须是 0");
} catch(ArrayIndexOutOfBoundsException e){
System.out.println("数组越界了");
} catch(Exception e){
System.out.println("一些其他的异常");
}
System.out.println("try-catch 之外");
}
```
“我知道,二哥,第二个 catch 块会执行,因为没有发生算术异常,但数组越界了。”三妹没等我把代码运行起来就说出了答案。
“三妹,你说得很对,我再来改一下代码。”
```java
static void test1 () {
try{
int arr[]=new int[7];
arr[9]=30/1;
System.out.println("try 块的最后");
} catch(ArithmeticException | ArrayIndexOutOfBoundsException e){
System.out.println("除数必须是 0");
}
System.out.println("try-catch 之外");
}
```
“当有多个 catch 的时候,也可以放在一起,用竖划线 `|` 隔开,就像上面这样。”我说。
“这样不错呀,看起来更简洁了。”三妹说。
`finally` 块的语法也不复杂。
```java
try {
// 可能发生异常的代码
}catch {
// 异常处理
}finally {
// 必须执行的代码
}
```
在没有 [`try-with-resources`](https://javabetter.cn/exception/try-with-resources.html) 之前,finally 块常用来关闭一些连接资源,比如说 socket、数据库链接、IO 输入输出流等。
```java
OutputStream osf = new FileOutputStream( "filename" );
OutputStream osb = new BufferedOutputStream(opf);
ObjectOutput op = new ObjectOutputStream(osb);
try{
output.writeObject(writableObject);
} finally{
op.close();
}
```
“三妹,注意,使用 finally 块的时候需要遵守这些规则。”
- finally 块前面必须有 try 块,不要把 finally 块单独拉出来使用。编译器也不允许这样做。
- finally 块不是必选项,有 try 块的时候不一定要有 finally 块。
- 如果 finally 块中的代码可能会发生异常,也应该使用 try-catch 进行包裹。
- 即便是 try 块中执行了 return、break、continue 这些跳转语句,finally 块也会被执行。
“真的吗,二哥?”三妹对最后一个规则充满了疑惑。
“来试一下就知道了。”我说。
```java
static int test2 () {
try {
return 112;
}
finally {
System.out.println("即使 try 块有 return,finally 块也会执行");
}
}
```
来看一下输出结果:
```
即使 try 块有 return,finally 块也会执行
```
“那,会不会有不执行 finally 的情况呀?”三妹很好奇。
“有的。”我斩钉截铁地回答。
- 遇到了死循环。
- 执行了 `System. exit()` 这行代码。
`System.exit()` 和 `return` 语句不同,前者是用来退出程序的,后者只是回到了上一级方法调用。
“三妹,来看一下源码的文档注释就全明白了!”

至于参数 status 的值也很好理解,如果是异常退出,设置为非 0 即可,通常用 1 来表示;如果是想正常退出程序,用 0 表示即可。
### 06、小结
Java 的异常处理是一种重要的机制,可以帮助我们处理程序执行期间发生的错误❎或异常。
异常分为两类:Checked Exception 和 Unchecked Exception,其中 Checked Exception 需要在代码中显式地处理或声明抛出,而 Unchecked Exception 不需要在代码中显式地处理或声明抛出。异常处理通常使用 try-catch-finally 块来处理,也可以使用 throws 关键字将异常抛出给调用者处理。
下面是 Java 异常处理的一些总结:
- 使用 try-catch 块捕获并处理异常,可以避免程序因异常而崩溃。
- 可以使用多个 catch 块来捕获不同类型的异常,并进行不同的处理。
- 可以使用 finally 块来执行一些必要的清理工作,无论是否发生异常都会执行。
- 可以使用 throw 关键字手动抛出异常,用于在程序中明确指定某些异常情况。
- 可以使用 throws 关键字将异常抛出给调用者处理,用于在方法签名中声明可能会出现的异常。
- Checked Exception 通常是由于外部因素导致的问题,需要在代码中显式地处理或声明抛出。
- Unchecked Exception 通常是由于程序内部逻辑或数据异常导致的,可以不处理或者在需要时进行处理。
- 在处理异常时,应该根据具体的异常类型进行处理,例如可以尝试重新打开文件、重新建立网络连接等操作。
- 异常处理应该根据具体的业务需求和设计原则进行,避免过度捕获和处理异常,从而降低程序的性能和可维护性。
----
GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/)
微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。
