Java 异常面试题 (13)
问:java 异常有哪几种,特点是什么?
答:异常是发生在程序执行过程中阻碍程序正常执行的错误操作,只要在 Java 语句执行中产生异常则一个异常对象就会被创建。Throwable 是所有异常的父类,它有两个直接子类 Error 和 Exception,其中 Exception 又被继续划分为被检查的异常(checked exception)和运行时的异常(runtime exception,即不受检查的异常);Error 表示系统错误,通常不能预期和恢复(譬如 JVM 崩溃、内存不足等);被检查的异常(Checked exception)在程序中能预期且要尝试修复(如我们必须捕获 FileNotFoundException 异常并为用户提供有用信息和合适日志来进行调试,Exception 是所有被检查的异常的父类);运行时异常(Runtime Exception)又称为不受检查异常,譬如我们检索数组元素之前必须确认数组的长度,否则就可能会抛出 ArrayIndexOutOfBoundException 运行时异常,RuntimeException 是所有运行时异常的父类。
问:java 中 throw 与 throws 的区别是什么?
答:a、使用位置; b、
throw 使用的位置在方法中,后面跟的异常对象实例,表示抛出异常,由方法体内语句处理,如果方法中有 throw 抛出 RuntimeException 及其子类则声明上可以没有 throws,如果方法中有 throw 抛出 Exception 及其子类则声明上必须有 throws。throws 使用的位置在方法参数小括号后面,后面跟的是一个或者多个异常类名且用逗号隔开,表示抛出异常并交给调用者去处理,如果后面根据的是 RuntimeException 及其子类则该方法可以不用处理,如果后面根据的是 Exception 及其子类则必须要编写代码进行处理或者调用的时候抛出。
问:java 中被检查的异常和不受检查的异常有什么区别?
答:被检查的异常应该用 try-catch 块代码处理或用 throws 关键字抛出,不受检查的异常在程序中不要求被处理或用 throws 抛出;Exception 是所有被检查异常的基类,而 RuntimeException(是 Exception 的子类) 是所有不受检查异常的基类;被检查的异常适用于那些不是因程序引起的错误情况(如 FileNotFoundException),而不被检查的异常通常都是由于糟糕的编程引起(如 NullPointerException)。
问:java 中 Error 和 Exception 有什么区别?
答:Error 表示系统级的错误,是 java 运行环境内部错误或者硬件问题,不能指望程序来处理这样的问题,除了退出运行外别无选择,它是 java 虚拟机抛出的。Exception 表示程序需要捕捉、需要处理的异常,是由与程序设计的不完善而出现的问题,程序可以处理的问题。
问:java 中什么是异常链?
答:异常链是指在进行一个异常处理时抛出了另外一个异常,由此产生了一个异常链条,大多用于将受检查异常(checked exception)封装成为非受检查异常(unchecked exception)或者 RuntimeException。特别注意如果你因为一个异常而决定抛出另一个新的异常时一定要包含原有的异常,这样处理程序才可以通过 getCause() 和 initCause() 方法来访问异常最终的根源。
有的时候我们会用printStackTrace来打印异常栈,有可能我们会在处理异常的时候同时又抛出一个异常。 自定义两个异常 class MyException1 extends Exception{ } class MyException2 extends Exception{ MyException2(Throwable throwable){ super(throwable); } MyException2(){ super(); } } MyException2 调用了父类的构造方法,目的是为了能够传递一个cause进来。 接下来定义一个A 类,并在他的f()方法里调用g()方法,然后在f处理g里抛出的异常的时候再次抛出一个异常 class A{ public void f() throws MyException2{ try { g(); } catch (MyException1 e) { // TODO Auto-generated catch block e.printStackTrace(); throw new MyException2(); } } public void g() throws MyException1{ throw new MyException1(); } } 在catch里我们又抛出了一个MyException2的异常,这里我们先调用它的无参构造方法 接下来我们在main里跑一下 public class Main { public static void main(String[] args) { A a = new A(); try { a.f(); } catch (MyException2 e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 看一下控制台 我们可以明显的发现异常栈的内容变少了。这是因为MyException2对MyException1的环境一无所知。 那么如果MyException2 能够持有MyException1的环境信息,这样做起来就十分方便了,并且也形成了一条链,我们也成为异常链。 唯一要修改的就是调用MyException2的有参构造函数,把MyException1当作cause传递进去,这样的话我们就能获取到MyException1的信息了。 class A{ public void f() throws MyException2{ try { g(); } catch (MyException1 e) { // TODO Auto-generated catch block e.printStackTrace(); //这里做了修改 throw new MyException2(e); } } public void g() throws MyException1{ throw new MyException1(); } } 我们再次运行 我们会发现我们已经可以持有MyException1的环境信息了
问:java 中如何编写自定义异常?
答:可以通过继承 Exception 类或其任何子类来实现自己的自定义异常类,自定义异常类可以有自己的变量和方法来传递错误代码或其它异常相关信息来处理异常。下面是一个自定义异常的常见模板:
public class DemoException extends IOException { private static final long serialVersionUID = 123456789L; private String errorCode="DemoException"; public DemoException(String msg, String errorCode){ super(msg); this.errorCode = errorCode; } public String getErrorCode(){ return this.errorCode; } }
常见试题:
问:请简单描述下面方法的执行流程和最终返回值是多少? public static int test1(){ int ret = 0; try{ return ret; }finally{ ret = 2; } } public static int test2(){ int ret = 0; try{ int a = 5/0; return ret; }finally{ return 2; } } public static void test3(){ try{ int a = 5/0; }finally{ throw new RuntimeException("hello"); }
/**输出
Exception in thread "main" java.lang.RuntimeException: hello
at javabasics.exception.ExcepyionDemo.test3(ExcepyionDemo.java:12)
at javabasics.exception.ExcepyionDemo.main(ExcepyionDemo.java:18)
Process finished with exit code 1
*/ }
答:本题旨在考察 try-catch-finally 块的用法踩坑经验,具体解析如下。
test1 方法运行返回 0,因为执行到 try 的 return ret; 语句前会先将返回值 ret 保存在一个临时变量中,然后才执行 finally 语句,最后 try 再返回那个临时变量,finally 中对 ret 的修改不会被返回。
test2 方法运行返回 2,因为 5/0 会触发 ArithmeticException 异常,但是 finally 中有 return 语句,finally 中 return 不仅会覆盖 try 和 catch 内的返回值且还会掩盖 try 和 catch 内的异常,就像异常没有发生一样(特别注意,当 finally 中没有 return 时该方法运行会抛出 ArithmeticException 异常),所以这个方法就会返回 2,而且不再向上传递异常了。
test3 方法运行抛出 hello 异常,因为如果 finally 中抛出了异常,则原异常就会被掩盖。
因此为避免代码逻辑混淆,我们应该避免在 finally 中使用 return 语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。
问:如果执行 finally 代码块之前方法返回了结果或者 JVM 退出了,这时 finally 块中的代码还会执行吗?
答:只有在 try 里面通过 System.exit(0) 来退出 JVM 的情况下 finally 块中的代码才不会执行,其他 return 等情况都会调用,所以在不终止 JVM 的情况下 finally 中的代码一定会执行。
问:分别说说下面代码片段都有什么问题?
public static void func() throws RuntimeException, NullPointerException { throw new RuntimeException("func exception"); } public static void main(String args[]) { try { func(); } catch (Exception ex) { ex.printStackTrace(); } catch (RuntimeException re) {//Exception 'java.lang.RuntimeException' has already been caught re.printStackTrace(); } }
上面代码段对于 func 方法后面 throws 列出的异常类型是不分先后顺序的,所以 func 方法是没问题的;对于 main 方法中在捕获 RuntimeException 类型变量 re 的地方会编译错误,因为 Exception 是 RuntimeException 的超类,func 方法执行的异常都会被第一个 catch 块捕获,所以会报编译时错误。
修改措施:两者调换一下位置即可
public class Base { public void func() throws IOException { throw new IOException("Base IOException"); } } public class Sub extends Base {
//'func()' in 'javabasics.exception.ExcepyionDemo.Sub' clashes with 'func()' in 'javabasics.exception.ExcepyionDemo.Base'; overridden method does not throw 'java.lang.Exception'
public void func() throws Exception { throw new Exception("Sub Exception"); } }
如上代码片段在编译时子类 func 方法会出现编译异常,因为在 java 中重写方法抛出的异常不能是原方法抛出异常的父类,这里 func 方法在父类中抛出了 IOException,所有在子类中的 func 方法只能抛出 IOExcepition 或是其子类,但不能是其父类。
public static void func() {} public static void main(String args[]) { try{ func(); }catch(IOException e) { e.printStackTrace(); } }
上面代码段编译时在 IOException 时会出现编译错误,因为 IOException 是受检查异常,而 func 方法并没有抛出 IOException,所以编译报错,但是如果将 IOException 改为 Exception(或者 NullPointerException 等)则编译报错将消失,因为 Exception 可以用来捕捉所有运行时异常,这样就不需要声明抛出语句。
答:通过上面几个代码片段可以看出我们在书写多 catch 块时要保证异常类型的优先级书写顺序,要保证子类靠前父类靠后的原则;此外在 java 中重写方法抛出的异常不能是原方法抛出异常的父类;如果方法没有抛出受检查类型异常则在调用方法的地方就不能主动添加受检查类型异常捕获,但是可以添加运行时异常或者 Exception 捕获。
问:关于 java 中的异常处理你有啥心得或者经验?
答:这其实是一个经验题,答案不局限的,可以自由发挥,下面给出几个示例点。
-
方法返回值尽量不要使用 null(特殊场景除外),这样可以避免很多 NullPointerException 异常。
-
catch 住了如果真的没必要处理则至少加行打印,这样可在将来方便排查问题。
-
接口方法抛出的异常尽量保证是运行时异常类型,除非迫不得已才抛出检查类型异常。
-
避免在 finally 中使用 return 语句或者抛出异常,如果调用的其他代码可能抛出异常则应该捕获异常并进行处理,因为 finally 中 return 不仅会覆盖 try 和 catch 内的返回值且还会掩盖 try 和 catch 内的异常,就像异常没有发生一样(特别注意,当 try-finally 中没有 return 时该方法运行会继续抛出异常)。
-
尽量不要在 catch 块中压制异常(即什么也不处理直接 return),因为这样以后无论抛出什么异常都会被忽略,以至没有留下任何问题线索,如果在这一层不知道如何处理异常最好将异常重新抛出由上层决定如何处理异常。
-
方法定义中 throws 后面尽量定义具体的异常列表,不要直接 throws Exception。
-
捕获异常时尽量捕获具体的异常类型而不要直接捕获其父类,这样容易造成混乱。
-
避免在 finally 块中抛出异常,不然第一个异常的调用栈会丢失。
-
不要使用异常控制程序的流程,譬如本应该使用 if 语句进行条件判断的情况下却使用异常处理是非常不好的习惯,会严重影响性能。
-
不要直接捕获 Throwable 类,因为 Error 是 Throwable 类的子类,当应用抛出 Errors 的时候一般都是不可恢复的情况。
当然还有其他的经验,上面只是给出一些常见的心得经验,具体回答时可自行拓展。
问:java 中 finally 块一定会执行吗?
答:不一定,分情况。因为首先想要执行 finally 块的前提是必须执行到了 try 块,当在 try 块或者 catch 块中有 System.exit(0); 这样的语句存在时 finally 块就不会被执行到了,因为程序被结束了。此外当在 try 块或者 catch 块里 return 时 finally 会被执行;而且 finally 块里 return 语句会把 try 块或者 catch 块里的 return 语句效果给覆盖掉且吞掉了异常。
问:java 中什么时候使用断言(assert)?
答:断言在开发中是一种常用的调试方式,很多开发语言中都支持这种机制。一般来说,断言用于保证程序最基本、关键的正确性,断言检查通常在开发和测试时开启,为了保证程序的执行效率,在软件发布后断言检查通常是关闭的,断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为 true,如果表达式的值为 false 则系统会报告一个 AssertionError。