1. Java中的错误
- Java中的错误,分为两种:语法错误,语义错误。
- 语法错误也叫做编译时错误,无法正常编译,只要语法有错,Eclipse,IDEA等IDE会自动检查语法,飘红提示导致java文件无法编译为class文件。
- 语义错误也叫做运行时错误,也称之为异常。语义错误可以正常编译,但是无法正常运行。
- 语义错误即异常并不是一种可预见性错误,而是必须通过运行才能发现的错误。比如int a = 10 / 0,这是符合语法的,但只有代码运行后才能知道加载进内存的被除数的值是不是0,是0的话才导致异常。
2. Java中的异常
Java中的异常是语义错误,叫做Throwable,Throwable本质是jdk封装好的一个内置类,其中又可以细分为以下三种:
- 错误类,即Error类。错误类一般是由JVM引起的,一般不由开发人员处理,可忽略。
- 运行时异常类,也叫做非检查性异常。该异常可以不进行捕获,不用处理,由JVM自动进行处理。如果不对该异常处理的话,类似于直接使用throws将出现的异常向上抛出去了,最终整个代码执行链上都没处理的话,则层层向上抛出,最终抛给栈底,一般都是程序入口方法,比如main方法。再由栈顶直接抛给JVM。而JVM会直接导致程序流程异常终止的。因此,运行时异常一般也需要手动进行try catch处理。
- 非运行时异常类,也叫做检查性异常。该异常必须由java开发人员手动进行处理。
JAVA中异常的含义:
异常是一个程序执行期间发生的事件,当当前线程执行程序时一旦发生了这种异常事件,java会自动将该事件封装为一个Exception对象。
默认情况下,异常事件一旦发生将会直接丢给JVM,中断当前线程的正常指令流,即直接结束当前线程的运行。
而java提供的异常处理机制,让我们可以捕获异常事件,从而决定对该异常如何处理。
3. 异常处理的两种方式
- try catch finally语句块,手动进行异常的捕获和处理。
- 使用throws关键字将可能出现的异常继续向流程调用方抛出,最终会层层抛出,直到在代码执行链上遇到try catch或者最终抛给了JVM进行处理。
tips: 对于异常本质的处理方式只有try catch finally语句块,throws并不具备处理异常的能力。
如果一直throws向上游程序抛出异常的话,最终只是抛给了JVM,而JVM的处理就是让程序流程异常终止。
注意:区分throws和throw,throws用来向上抛出异常,而throw用来手动抛出一个异常对象。
异常处理案例:
package zeh.myjavase.code14exception;
import org.junit.Test;
//Java中的异常处理机制:try catch finally
//异常处理的作用:避免程序异常终止
//Java中只能使用try catch finally处理异常,尽管使用throws一直向上抛,但是最终抛给JVM后仍然会导致程序中断。
//记住,不使用try catch处理的异常都是耍流氓,因为一旦发生异常,程序必将异常终止。
public class Demo01TryCatch {
@Test
public void testTryCatch1() {
int a = 10;
int b = 0;
int c = 0;
try {
// 此处抛异常,直接走catch,此异常节点之后的代码不会执行,当前线程会自动跳转到catch处。
c = a / b;
System.out.println("try异常之后的代码逻辑...");
} catch (Exception e) {
System.out.println("成功处理了异常,执行catch后续的操作..." + e);
}
System.out.println("c = " + c);
System.out.println("testTryCatch1 后续操作!");
}
@Test
public void testTryCatch2() throws Exception {
//直接调用 testException() ,没有 try catch 处理,直接向上throws 了所以导致代码在此处终止。
testException();
// 因为testException()方法throws出来的异常,当前方法并没有进行捕获,所以继续向上throws了,线程遇到throws后则立即退出当前方法栈。
// 因此下面的输出语句不会执行。
System.out.println("testTryCatch2 后续操作!");
}
@Test
public void testTryCatch3() {
try {
// 调用 testException()
System.out.println("c:" + testException());
System.out.println("try异常之后的代码逻辑...");
} catch (Exception e) {
// testException()方法抛出的异常被当前方法catch住了,则当前线程会跳转到catch处继续向下执行。
e.printStackTrace();
System.out.println("成功处理了test()方法抛出的异常....");
}
// 调用 testException() 方法抛出的异常被当前方法catch处理了,因此,线程会继续向下执行。
System.out.println("testTryCatch3 后续操作!");
}
//1.如果一个方法使用了 throws 抛出了异常,则该异常抛给调用方处理,被调用方抛出异常后线程自动退出,立即终止对被调用方的执行。
//2.对于同一个异常,如果一个方法既使用throws抛出了该异常,同时方法内部也使用了try catch对该异常进行了捕获,则该异常会被方法内部优先捕获处理,而不会抛给调用方。
//3.针对检查性异常,不论 throws 抛出的检查性异常有没有被自己方法内部使用 try catch 处理,调用方从语法上都得使用try catch处理或者使用throws继续向上抛出()。
private int testException() throws Exception {
int a = 10;
int b = 0;
int c = 0;
// 使用try catch处理,但是在catch中仍然手动向上抛异常,而没有进行catch捕获,则后续代码不会执行,因为一旦throws一个异常对象出去后,当前线程将立即退出当前作用域。
try {
c = a / b;// 此处出现异常
} catch (Exception e) {
// 上面出现异常后此处catch住了,处理一下,当前线程会自动跳转到catch处继续向下执行
// 很可惜处理的方式同时又是手动throw了一个异常。
// 手动抛出的异常是自定义异常,实际上就是产生一个异常对象,相当于发生了异常,因此也需要try catch处理,如果不处理而是throws向上抛,一直没有遇到try
// catch处理,则最终会导致程序异常终止
// 后面的操作不会执行,因为此处没有使用try catch去处理自定义抛出的异常,该异常被直接throws给调用方法了,当前线程也将直接退出,返回到调用方。
throw new Exception("手动抛出一个异常");
}
// 下面的操作是不会执行的,因为上面的catch块中手动抛出的异常并没有在当前作用域中进行捕获处理,因此上面的异常对象随着throws抛出后,线程就退出了当前作用域了。
System.out.println("c is " + c);
System.out.println("testException 后续操作!!!");
return c;
}
//调用方使用throws 将被调用方 throws 出来的异常继续抛出。
@Test
public void testTryCatch4() throws Exception {
checkException();
// 下面的语句不会执行
System.out.println("testTryCatch4 后续操作!");
}
//调用方使用 try catch 将被调用方 throws 的异常进行再度处理。
@Test
public void testTryCatch5() {
try {
checkException();
} catch (Exception e) {
System.out.println("使用try catch处理 被调用方 throws 出来的 Exception。");
}
// 下面的语句会继续执行
System.out.println("testTryCatch5 后续操作!");
}
//被调用方使用throws 抛出一个Exception(注意没有明确指定异常是RuntimeException的,则一律视为检查性异常),则被调用方必须使用try catch 或者 throws 再度处理,否则语法飘红。
private void checkException() throws Exception {
System.out.println("throws 检查性异常...");
throw new Exception("手动抛出 checkException。");
}
//(1)、由于被调用方自己内部使用 try catch处理了异常,所以调用方此时不会捕获到异常。
//(2)、虽然被调用方自己处理了异常,但同时 throws 了异常,所以调用方都必须对异常进行再度处理。
@Test
public void testTryCatch6() {
try {
checkExceptionHandlerBySelf();
} catch (Exception e) {
System.out.println("调用方使用 try catch 处理 throws 的 checkException.");
}
System.out.println("testTryCatch6 后续操作!");
}
//(1)、被调用方内部使用 try catch 处理了异常。
//(2)、被调用方使用 throws 将异常抛给调用方处理。
//(3)、此情况,异常真正会被 try catch 处理,而不会让调用方捕获。
//(4)、尽管如此,只要被调用方使用了 throws 抛出了一个checkException,从语法上调用方都需要使用 try catch或者throws进行再度处理,否则语法飘红。
private void checkExceptionHandlerBySelf() throws Exception {
System.out.println("throws 检查性异常,自己内部使用 try catch 处理...");
try {
throw new Exception("手动抛出 checkException。");
} catch (Exception e) {
System.out.println("checkExceptionHandlerBySelf 自己内部处理");
}
System.out.println("checkExceptionHandlerBySelf 后续操作!");
}
}
4. finally块的说明
当代码执行到try语句时,对finally块中,不论是try和catch块中使用return或者break,还是出现了异常,finally块总会执行。
即:finally块的逻辑总会执行的,除非在执行finally之前使用System.exit(),使JVM进行强制结束,这样finally就不会执行。
分析如下:
- try语句没有被执行,比如在try语句前就return了,这样finally语句并不会执行。因此说明了finally语句块被执行的必要而非充分条件就是:相应的try语句一定被执行到。
- 如果在try代码之前或者try代码、catch代码中执行了System.exit(0);语句使得JVM进程强制退出,那么将终止Java虚拟机,因此,finally语句也不会被执行到。
- finally块的语句在try块或者catch块中的return语句执行之后,返回之前执行。而且finally语句块中的逻辑有可能影响try块或者catch块中return的值,也有可能不影响。
如果返回值类型是基本类型,则不影响;如果是引用类型,则可能影响。
如果finally语句块中也有return语句,则最终会覆盖掉try块和catch块中的return语句,直接返回finally语句块中的return逻辑。
最终结论:在代码执行到try语句且不终止JVM的情况下,finally语句块一定会执行。
案例:
package zeh.myjavase.code14exception;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
// finally块的作用
public class Demo02TryCatchFinally {
// 默认情况下,finally逻辑总会执行:当前线程进入try catch finally作用域后,不管try块有没有异常产生,当前线程最后都会执行finally块。
@Test
public void testFinally1() {
int result;
try {
System.out.println("进入try");
System.out.println("try逻辑");
result = 10 / 0;
} finally {
System.out.println("finally逻辑");
}
// 异常没有被catch处理,因此下面语句不会执行
System.out.println("result is " + result);
}
@Test
public void testFinally2() {
int a = 10;
if (a == 10) {
// try代码块之前就执行了return,则后面的任何语句都不会执行,包括finally块
return;
}
int result;
try {
System.out.println("进入try");
System.out.println("try逻辑");
result = 10 / 0;
} finally {
System.out.println("finally逻辑");
}
}
@Test
public void testFinally3() {
int a = 10;
int result = 0;
try {
System.out.println("进入try");
System.out.println("try逻辑");
// try代码中产生异常之前执行了return,则只会执行finally块
if (a == 10) {
return;
}
result = 10 / 0;
} catch (Exception e) {
System.out.println("捕获异常");
} finally {
System.out.println("finally逻辑");
}
System.out.println("result is " + result);
}
@Test
public void testFinally4() {
int a = 10;
int result = 0;
try {
System.out.println("进入try");
System.out.println("try逻辑");
result = 10 / 0;
// try代码中产生异常之后执行了return,则此处的return实际上没有意义,因为一旦产生异常,当前线程就会跳转到catch块去向下执行逻辑。
// 因此,finally块和后续的代码都会执行
if (a == 10) {
return;
}
} catch (Exception e) {
System.out.println("捕获异常");
} finally {
System.out.println("finally逻辑");
}
System.out.println("result is " + result);
}
@Test
public void testFinally5() {
int a = 10;
int result = 0;
try {
System.out.println("进入try");
System.out.println("try逻辑");
result = 10 / 1;
// try代码中产生异常之后执行了return,此处的return有意义,因为try代码中并没有产生异常,因此,线程不会跳转到catch块,而是继续向下执行,发现满足条件,因此直接return了。
// 当前线程在return之前总会执行finally块。
if (a == 10) {
return;
}
} catch (Exception e) {
System.out.println("捕获异常");
} finally {
System.out.println("finally逻辑");
}
System.out.println("result is " + result);
}
@Test
public void testFinally6() {
int result = 0;
try {
System.out.println("进入try");
System.out.println("try逻辑");
// 在进入try块后结束了JVM进程,则finally将不会执行,整个后续的逻辑都不会执行。
System.exit(0);
result = 10 / 0;
} catch (Exception e) {
System.out.println("捕获异常");
} finally {
System.out.println("finally逻辑");
}
System.out.println("result is " + result);
}
@Test
public void testFinally7() {
int result = 0;
try {
System.out.println("进入try");
System.out.println("try逻辑");
// 先产生了异常
result = 10 / 0;
// 然后再终止JVM进程,此时当前线程已经跳转到了catch块了,因此,该语句不会被执行到
System.exit(0);
} catch (Exception e) {
System.out.println("捕获异常");
} finally {
System.out.println("finally逻辑");
}
System.out.println("result is " + result);
}
@Test
public void testFinally8() {
int result = 0;
try {
System.out.println("进入try");
System.out.println("try逻辑");
// 先产生了异常
result = 10 / 0;
} catch (Exception e) {
System.out.println("捕获异常");
// 当前线程跳转到此处,然后终止了JVM,则后续逻辑都不会执行
System.exit(0);
} finally {
System.out.println("finally逻辑");
}
System.out.println("result is " + result);
}
@Test
public void testFinally9() {
int result = 0;
try {
System.out.println("进入try");
System.out.println("try逻辑");
// 先产生了异常
result = 10 / 0;
} catch (Exception e) {
System.out.println("捕获异常");
} finally {
System.out.println("finally逻辑");
System.out.println("不论是try和catch块中使用return还是break,还是出现异常,finally块总会执行,除非在执行finally之前" +
"执行了System.exit(),使jvm进程强制结束,或者在try 之前就return了,那么这种情况下finally块就不会执行了。");
Map<String, Object> map = tetsMap();
System.out.println("name:" + map.get("name"));
}
System.out.println("result is " + result);
}
/**
* 验证 finally 语句在try或者catch中的return语句执行之后,返回之前执行。而且finally语句中可能会修改return返回的结果。
*
* @return
*/
public Map<String, Object> tetsMap() {
Map<String, Object> map = new HashMap<>();
try {
map.put("name", "赵二虎");
return map;
} catch (Exception e) {
e.printStackTrace();
} finally {
map.put("name", "于瑞");
// return new HashMap<>();//finally语句中的return将覆盖掉try或者catch中的return语句。
}
return null;
}
}
5. 运行时异常
- 运行时异常如果不显式处理,则直接一层一层向调用方抛出,类似于throws,直到遇见try catch语句,如果抛向异常栈的最底部(即最初的调用方,注意栈是先进后出的)一直没有catch处理则程序会异常终止。
- 常见的运行时异常:
ArrayIndexOutOfBoundsException 数组下标越界异常; NullPointerException 空指针异常; ArithmeticException 算数异常; MissingResourceException 资源丢失异常; ClassNotFoundException 找不到类异常。
- 运行时异常属于非检查性异常,程序中可以选择try catch处理,也可以不处理。这些异常一般是由于程序逻辑错误引起的,完全是可以避免的异常,也就是说这些异常完全是程序员可以自己规避的异常,jdk团队已经整理好了,所以才将这些异常定义为运行时异常,目的就是不让程序员频繁的使用try catch进行处理(即抛出这些运行时异常不处理完全可以),然而不处理一旦发生这种运行时异常就会直接导致程序异常终止。
- 从逻辑角度讲,运行时异常应该由程序员通过代码实现进行规避。
6. 异常的作用
- 异常就是用来检查程序运行期间是否出现意外,异常一旦发生建议必须使用try catch进行处理,或者通过逻辑代码进行规避。
- 不使用try catch处理的异常,一定会导致程序异常终止,即一旦程序某处抛出异常而没有处理,当前线程将立即退出当前作用域,层层向上返回直到遇见处理该异常的流程,如果一直没有逻辑处理该异常,最终抛给JVM,导致JVM异常终止。
tips: 所谓不使用try catch处理的异常,包括任何的异常。包括运行时异常,非运行时异常,手动抛出的异常等。只要是异常,都必须使用try catch进行处理,才能保证代码不异常终止。
记住,不使用try catch处理的异常都是耍流氓,因为一旦发生异常,程序必将终止。
7. throws和throw
- throws表示抛出当前方法内部出现的异常给调用方,当前方法不进行异常的处理,交给调用方进行处理。如果调用方不处理,则继续向上抛或者异常终止程序。如果当前方法使用try catch捕获了异常进行了处理,则当前方法的throws不生效,即try catch语句块的优先级比throws高。
- throw是在代码当中手动产生一个异常,相当于认为制造了一个异常,异常产生后的处理方式和其他异常完全一样。即如果产生的是检查性异常则需要使用try catch或者throws进行处理;如果产生的是运行时异常则可以不做任何处理,直接交给JVM(但建议使用try catch进行处理)。
throw案例:
package zeh.myjavase.code14exception;
//Java中异常处理的throw:用于手动抛出一个自定义异常
//使用try catch finally 真正处理 或者 使用throws继续向上抛
//运行时异常自动向上throws,一直遇到try catch处理,如果一直没遇到,则最终抛给JVM,导致程序异常终止。
//非运行异常必须手动try catch finally或者一直向上throws。
public class Demo03ThrowTest {
public static void main(String args[]) {
try {
//检查异常必须手动处理
throwException();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("检查性异常执行之后的异常!");
//运行时异常不用处理,最终交给JVM处理,相当于不显式使用 try catch处理的话,则默认一直向上抛,直到JVM异常终止。
throwRuntimeException();
System.out.println("运行时异常执行之后的异常!");
}
public static void throwException() throws Exception {
//检查异常必须进行处理,使用try catch或者throws
throw new Exception("自定义异常!");
}
// 手动抛出一个运行时异常,运行时异常不要求手动处理,默认一直自动向上throws
public static void throwRuntimeException() {
throw new RuntimeException("自定义运行时异常");
}
}
throws案例:
package zeh.myjavase.code14exception;
import org.junit.Test;
// 异常处理学习
// Java中异常的throws:当前方法不处理异常,交给调用处进行处理
// 调用处既可以采用try catch finally进行处理,也可以采用throws继续向上抛(并未真正处理);
// 如果一直向栈底抛最终都没有处理,则JVM就异常终止。
public class Demo04ThrowsTest {
@Test
public void testThrowsOne() {
int c = 0;
try {
//对于被调用方throws的检查性异常,调用方必须使用try catch显式处理或者继续向上throws。
c = divideOne(10, 0);
} catch (Exception e) {
e.printStackTrace();
}
// 一旦当前作用域中使用try catch处理了异常,则线程会继续向下执行,而不会退出当前作用域。
System.out.println(c);
System.out.println("***后续操作***");
}
// throws表示此方法不处理异常,而是交给调用方进行处理;调用方可以try catch进行处理,也可以继续向上层throws。
private int divideOne(int a, int b) throws Exception {
int result = 0;
result = a / b;
// 上述语句发生异常后直接throws给调用方了,则被调用方后续操作还会执行吗,很明显不会,因为此处对异常就没有处理,直接throws到调用方了。
// 当前方法没有处理异常而是直接throws到调用方,一旦发生throws操作,则线程会立即退出当前作用域。
System.out.println("异常抛出后的后续逻辑...");
return result;
}
@Test
public void testThrowsTwo() {
int c = 0;
try {
c = divideTwo(10,0);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(c);
System.out.println("***后续操作***");
}
// 该方法既将异常throws到调用方了,同时自己又使用try catch处理了该异常,这样处理后:该异常将首先被try catch进行处理,而不再throws给调用方。
// 尽管如此,只要被调用方明确使用了throws抛出了异常,则被调用方从语法上将都必须处理(try catch或者继续向上throws)
private int divideTwo(int a, int b) throws Exception {
int result = 0;
//对于发生的同一个异常,如果被调用方自己处理了,则优先自己处理,不会再 throws 给调用方。
try {
result = a / b;
} catch (Exception e) {
System.out.println("====" + e);
}
// 上述语句发生异常后,优先在当前作用域中自己处理了,而没有向上throws出去,因此,当前线程不会退出该作用域,而会继续向下执行。
System.out.println("异常抛出后的后续逻辑...");
return result;
}
}
8. 代码执行链和异常栈
- 一个简单的测试类
public class Test{ public static void main(String args[]){ fun1();// main方法调用fun1方法。调用方:main 被调用方:fun1 } public static void fun1(){ fun2();// fun1方法调用fun2方法。调用方:fun1 被调用方:fun2 } public static void fun2(){ fun3();// fun2方法调用fun3方法。调用方:fun2 被调用方:fun3 } public static void fun3(){ fun4();// fun3方法调用fun4方法。调用方:fun3 被调用方:fun4 } }
扩展tips:
了解栈内存结构和栈帧。
栈内存的数据结构是线性的,可以采用数组实现,也可以采用链表等实现。
栈内存数据结构的特点是先进后出。
栈内存只有一个口,既是入口也是出口。
先进入栈内存的数据首先被压栈,也叫做入栈,后续进来的数据依次序入栈。
最终栈内存的数据状态是,先进入的被压到栈底,后进入的在栈顶。
这样一来,后进入的数据则优先出栈。
这边是栈结构的特性,即先进后出。
栈帧是JVM为每个被压入虚拟机栈内存的方法等创建的一个对象,里面保存着局部变量表、操作数栈、方法链接符等信息。 - JVM进程启动后,启动主线程从main方法开始执行,即从上游业务开始向下游业务执行,开始执行整个方法的调用链。这种程序的执行链被称为代码执行链。
在代码执行链中,当主线程执行到fun4方法时,虚拟机栈中的状态是这样的:
栈顶
fun4
fun3
fun2
fun1
main
栈底
(1)、程序入口方法是main,所以虚拟机首先会创建main方法对应的栈帧,栈帧中保存着局部变量表、操作数栈、方法链接符等信息。
(2)、将main方法对应的栈帧压栈。
(3)、main方法调用了fun1,则创建fun1的栈帧,压栈。
(4)、以此类推,当执行到fun4方法时,则创建fun4方法的栈帧,压栈。
(5)、最终形态即为:代码执行链最上游的方法沉到栈底;代码执行链最下游的方法漂到栈顶。
(6)、代码执行链之所以采用栈结构进行存储,是因为完全符合软件执行流程,只有采用栈结构,下游的执行结果才能够方便的被线程获取到(因为方法越是在业务下游,其数据内容越是在栈顶,方便出栈)。
tips:
如果在主线程中的某个流程中,启动了异步子线程,那么异步子线程会拥有完全独立的虚拟机栈空间,保存为子线程的整个方法调用链上的每个方法创建的栈帧。
扩展:JVM内存划分中,程序计数器、栈内存是线程私有的;而堆内存、方法区是线程共享的内存区域。 - 如果当代码执行链执行到fun4方法时,fun4方法抛出了异常,那么虚拟机生成Exception实例就会保存整个代码执行链的虚拟机栈信息。
异常对象生成后,fun4方法会提前结束。
exception对象会一直沿着代码执行链的反方向,一直向上游移动(即throws的原理或者运行时异常默认向上抛出的原理)。
直到某个上游方法将exception进行了try catch捕获或者最终交给了JVM处理。
如果try catch捕获了异常并进行了打印(JVM捕获后会默认打印exception对象栈信息),则此时就会打印出异常栈信息。 - exception对象被打印,实际上就是其中的虚拟机栈信息出栈的过程。
打印的顺序就是按照出栈的顺序打印的。
具体就是:真正抛出异常的方法栈帧始终在栈定方向;而整个调用链的上游方法始终在栈底方向。 - 总结:
异常栈信息的第一行就是抛出这个异常的最原始的地方。
异常栈信息的最后一行就是最开始调用的地方。
如果异常栈信息后面跟着Cause by,就证明抛出当前异常的原因是捕获到了下面的异常。
9. 断言
9.1 使用assert
JDK1.4之后增加了断言的功能:断言就是肯定某个结果的返回值是正确的,如果最终该结果的返回值是错误的,则通过断言肯定会提示错误信息。
断言的格式如下(可用()将boolean表达式扩起来):
(1) assert boolean表达式;
(2) assert boolean表达式:详细的信息。
如果boolean表达式的结果为true,则什么错误信息都不提示;如果为false,则会提示错误信息;如果没有声明详细的错误描述,则系统会按照默认的错误信息方式显式。
9.2 开启断言
在Java中,assert关键字是从JAVA SE 1.4 引入的,为了避免和老版本的Java代码中使用了assert关键字导致错误((即JDK1.4之前有的项目中使用assert作为变量,这样如果直接开启有的老项目就会报错喽) ),Java在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!),如果要开启断言检查,则需要用开关-enableassertions或-ea来开启。
在java运行时加入JVM启动参数:
-ea或者-enableassertions
9.3 myeclipse开启断言
eclipse、myeclipse开启assert(断言),默认是关闭,如下: 说白了就是设置一下jvm的参数,参数是-ea或者-enableassertions (1)全局开启
(2)只对当前代码开启,右键运行代码设置jvm参数
9.4 assert陷阱
assert关键字用法简单,但是使用assert往往会让你陷入越来越深的陷阱中。应避免使用。笔者经过研究,总结了以下原因:
- assert关键字需要在运行时候显式开启才能生效,否则你的断言就没有任何意义。而现在主流的Java IDE工具默认都没有开启-ea断言检查功能。这就意味着你如果使用IDE工具编码,调试运行时候会有一定的麻烦。并且,对于Java Web应用,程序代码都是部署在容器里面,你没法直接去控制程序的运行,如果一定要开启-ea的开关,则需要更改Web容器的运行配置参数。这对程序的移植和部署都带来很大的不便。
- 用assert代替if是陷阱之二。assert的判断和if语句差不多,但两者的作用有着本质的区别:assert关键字本意上是为测试调试程序时使用的,但如果不小心用assert来控制了程序的业务流程,那在测试调试结束后去掉assert关键字就意味着修改了程序的正常的逻辑。
- assert断言失败将面临程序的退出。这在一个生产环境下的应用是绝不能容忍的。一般都是通过异常处理来解决程序中潜在的错误。但是使用断言就很危险,一旦失败系统就挂了。
9.5 对assert的思考
assert既然是为了调试测试程序用,不在正式生产环境下用,那应该考虑更好的测试JUint来代替其作用,JUint相对assert关键的所提供的功能是有过之而无不及。当然完全可以通过IDE debug来进行调试测试。在此看来,assert的前途一片昏暗。
因此,应当避免在Java中使用assert关键字,除非哪一天Java默认支持开启-ea的开关,这时候可以考虑。对比一下,assert能给你带来多少好处,多少麻烦,这是我们选择是否使用的的原则。
9.6 断言案例
package zeh.myjavase.code14exception;
// JDK1.4之后增加了断言的功能:断言就是肯定某个结果的返回值是正确的,如果最终该结果的返回值是错误的,则通过断言肯定会提示错误信息。
// 断言的格式如下(可用()将boolean表达式扩起来):
// assert boolean表达式;
// assert boolean表达式:详细的信息。
// 如果boolean表达式的结果为true,则什么错误信息都不提示;如果为false,则会提示错误信息;如果没有声明详细的错误描述,则系统会按照默认的错误信息方式显式。
public class Demo05Assert {
public static void main(String[] args) {
int i[] = {1, 2, 3};
// assert (i.length == 0);// 断言i数组的长度是0,肯定是错误的,直接运行发现没有错误信息提示。
// 注意:直接运行断言没有任何错误信息提示,因为java在设置assert关键字时,考虑到了系统的应用
// 为了防止某些用户使用assert作为关键字 ,所以在程序运行时断言并不会起作用。如果想让断言起作用,则在java运行时加入JVM启动参数:
// -ea或者-enableassertions
assert (i.length == 0) : "数组的长度不为0";
}
}
10 对异常的思考
- 不要使用异常来管理业务逻辑,应该使用条件语句。如果一个控制逻辑可以通过if-else语句来简单完成的,那就不用使用异常,因为异常会降低代码的可读性和性能。比如一些null值的判断,除0的情况。
// 使用如下的if判断来控制逻辑。 String str = null; if(str == null){ ... } // 如果不使用上面的if判断,而是直接对异常进行catch捕获来控制逻辑的话,将会很丑陋: String str = null; try{ // 任由str抛出异常,然后通过对异常的catch来判断逻辑的走向,这种方式不建议 str.toString(); }catch(Exception e){ ... }
- 异常对象的名字必须清晰而且有具体的含义,表示异常具体发生的问题。比如FileNotFoundException就很清晰直观。
- 当方法判断应该抛出异常时,必须直接throws异常出去,而不要return一些错误的码值,因为错误值不够直观。比如抛出 FileNotFoundException异常,而不是直接返回-1或者-2之类的错误码。
- 应该catch具体的异常,而不是直接catch Exception完事,异常的catch应该越具体越好,这对代码的性能、可读性和诸多方面都有好处。
- Null的判断逻辑并不是一成不变的,如果特殊情况下允许一个方法返回null值,则在调用方应该使用if-else去控制逻辑,否则将会抛出NullPointException。
- 不要仅仅捕获了异常,而不做任何处理,至少得打印出异常的堆栈信息等,否则异常会被吞掉,不便于后期维护。
- 请使用finally来释放一些打开的资源,比如打开的文件,数据库连接等。
- 大部分情况下不建议在循环内部处理异常,应在循环外对异常进行捕获处理。
- 异常的粒度很重要,应该为一个基本操作定义一个try-catch块,而不是为了简单,直接将几百行代码放到一个try-catch块中。
- 为每一个异常定义一个明确唯一的code,这一点很重要。
文档信息
- 本文作者:Marshall