Core Java 常见笔试题总结(异常类问题)
zolj0676
8年前
<p>所有代码均在本地编译运行测试,环境为 Windows7 32位机器 + eclipse Mars.2 Release (4.5.2)</p> <h2><strong>下面的代码输出结果是多少?为什么?并由此总结几个编程规范。</strong></h2> <pre> <code class="language-java"> 1 class smallT { 2 public static void main(String args[]) { 3 smallT t = new smallT(); 4 int b = t.get(); 5 System.out.println(b); 6 } 7 8 public int get() { 9 try { 10 return 1; 11 } finally { 12 return 2; 13 } 14 } 15 } </code></pre> <pre> <code class="language-java">众所周知,在一个try-finally 语句中,finally 语句块总是在控制权离开try 语句块时执行的,但是finally中如果有return则try中的return结果不再返回给主调者,所以返回的结果是2。即,如果finally里也有返回语句,那么以finally的为主。也就是说,千万不要用一个return、break、continue 或throw 来退出一个finally 语句块,且千万不要允许将一个受检查的异常传播到一个finally 语句块之外去。容易出一些潜在bug。 解析</code></pre> <p>原因:查看字节码</p> <pre> <code class="language-java"> public int get(); Code: 0: goto 4 3: pop 4: iconst_2 5: ireturn Exception table: from to target type 0 3 3 any </code></pre> <p>get方法执行开始,直接产生了一个goto指令跳转到了4处,iconst_2指令将常量2压入操作数栈,跳过了pop指令。然后执行ireturn指令,从当前方法返回2。</p> <h2><strong>如下代码, finally里的代码会不会执行?如执行,是在return前还是后?</strong></h2> <pre> <code class="language-java"> 1 public class Test1 { 2 public static void main(String[] args) { 3 System.out.println(new Test1().test()); 4 } 5 6 static int test() { 7 int x = 1; 8 try { 9 return x; 10 } finally { 11 ++x; 12 } 13 } 14 } </code></pre> <pre> <code class="language-java">执行try时,遇到return语句,且还有finally语句块,那么程序此时一定要到finally执行,但是在转去finally语句块之前,try中先把要返回的结果(虽然准备好了返回值,但是还没返回呢,所以我说是执行的中间)存放到不同于X的局部变量中,执行完finally后,在从局部变量中取出原返回结果,因此,即使finally中对变量x进行了改变,但是不会影响返回结果。因为方法内使用栈保存返回值。结论:此时finally一定会执行,且在return前执行,并返回1, 解析</code></pre> <p>详细原因看字节码:</p> <pre> <code class="language-java"> static int test(); Code: 0: iconst_1 1: istore_0 2: iload_0 3: istore_2 4: iinc 0, 1 7: iload_2 8: ireturn 9: astore_1 10: iinc 0, 1 13: aload_1 14: athrow </code></pre> <p>iconst_1: 常数1进栈, istore_0: 栈顶元素1出栈,并把 1 保存在本地变量表的第1个位置里(下标为0的位置),iload_0 将本地变量表第一个位置的 1 推送至栈顶, istore_2: 栈顶元素 1 出栈,并把 1 保存在本地变量表的第3个位置里(下标为2的位置),iinc指令对本地变量表的 第一个位置元素+1 ,iload_2 将 本地变量表第3个位置的 1 推送至栈顶 ,准备执行ireturn指令返回……</p> <h2><strong>下面有关JAVA异常类的描述,说法错误的是?</strong></h2> <p><img src="https://simg.open-open.com/show/3c8f1a37476ad979446e7d6f9a990579.png"></p> <p>纯粹的概念问题,下面总结下:</p> <p>在Java中异常被当做对象来处理,根类是java.lang.Throwable类,在Java中定义了很多异常类(如OutOfMemoryError、NullPointerException、IndexOutOfBoundsException等),这些异常类分为两大类:Error和Exception。</p> <p>Error是无法处理的异常,比如OutOfMemoryError,一般发生这种异常,JVM会选择终止程序。因此我们编写程序时不需要关心这类异常。Exception,也就是我们经常见到的一些异常情况,比如NullPointerException、IndexOutOfBoundsException,这些异常是我们可以处理的异常。Exception类的异常包括checked exception和unchecked exception(unchecked exception也称运行时异常RuntimeException,当然这里的运行时异常并不是说的运行期间的异常,只是Java中用运行时异常这个术语来表示,Exception类的异常都是在运行期间发生的)。</p> <p>unchecked exception(非检查异常),也称运行时异常(RuntimeException),比如常见的NullPointerException、IndexOutOfBoundsException。对于运行时异常,java编译器不要求必须进行异常捕获处理或者抛出声明,由程序员自行决定。 checked exception(检查异常),也称非运行时异常(运行时异常以外的异常就是非运行时异常),java编译器强制程序员必须进行捕获处理,比如常见的IOExeption和SQLException。对于非运行时异常如果不进行捕获或者抛出声明处理,编译都不会通过</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/75e2eecbc341ce3a74e46eefcaca9b23.jpg"></p> <p>图中红色部分为受检查异常。它们必须被捕获,或者在函数中声明为抛出该异常。</p> <h2><strong>下面程序的输出是什么?</strong></h2> <p><img src="https://simg.open-open.com/show/548847fc40de824712861c4188cf5e88.png"></p> <pre> <code class="language-java">首先是foo(0),在try代码块中未抛出异常,finally是无论是否抛出异常是必定执行的语句,所以 output += “3”;然后是 output += “4”; 执行foo(1)的时候,try代码块抛出异常,进入catch代码块,output += “2”;finally是必执行的,即使return也会执行output += “3”,由于catch代码块中有return语句,最后一个output += “4”不会执行。所以结果是3423。 解析</code></pre> <h2><strong>下面程序执行结果?</strong></h2> <pre> <code class="language-java">import java.io.IOException; class Arcane1 { public static void main(String[] args) { try { System.out.println("Hello world"); } catch (IOException e) { System.out.println("I've never seenprintln fail!"); } } } </code></pre> <pre> <code class="language-java">好像看起来应该是可以编译的,但是本程序编译期间错误。try 子句执行打印的I/O操作,并且catch 子句捕获 IOException 异常。但是这不能编译,因为println 方法没有声明会抛出任何被检查异常,而 IOException 却正是一个被检查异常。Exception 类的异常包括 checked exception 和 unchecked exception(unchecked exception也称运行时异常RuntimeException)Java语言规范中描述道:如果一个catch 子句要捕获一个类型为 E 的被检查异常,而其相对应的try 子句如不能抛出E 的某种子类型的异常,那么这就是一个编译期错误。 解析</code></pre> <p>补充一个println方法的源码。</p> <pre> <code class="language-java"> public void println(String x) { synchronized (this) { print(x); newLine(); } } </code></pre> <h2><strong>下面程序执行结果?</strong></h2> <pre> <code class="language-java">class Arcane2 { public static void main(String[] args) { try { } catch (Exception e) { System.out.println("This can't happen"); } } } </code></pre> <pre> <code class="language-java">看起来应该是不可以编译的,但是它却可以编译通过。因为它唯一的catch 子句检查了Exception,捕获 Exception 或 Throwble 的 catch 子句是合法的,不管与其相对应的 try 子句的内容为何。尽管Arcane2 是一个合法的程序,但是catch 子句的内容永远的不会被执行,这个程序什么都不会打印。 解析</code></pre> <h2><strong>下面程序执行结果?</strong></h2> <pre> <code class="language-java"> 1 interface Type1 { 2 void f() throws CloneNotSupportedException; 3 } 4 5 interface Type2 { 6 void f() throws InterruptedException; 7 } 8 9 interface Type3 extends Type1, Type2 { 10 } 11 12 class Arcane3 implements Type3 { 13 public void f() { 14 System.out.println("Hello world"); 15 } 16 17 public static void main(String[] args) { 18 Type3 t3 = new Arcane3(); 19 t3.f(); 20 } 21 } </code></pre> <pre> <code class="language-java">方法f 在 Type1 接口中声明要抛出被检查异 CloneNotSupportedException,并且在Type2 接口中声明要抛出 被检查异常InterruptedException。Type3 接口继承了Type1 和Type2,因此,看起来在静态类型为Type3的对象上调用方法f时,有潜在可能会抛出这些异常。一个方法必须要么捕获其方法体可以抛出的所有被检查异常,要么声明它将抛出这些异常。 Arcane3 的main 方法在静态类型为Type3 的对象上调用了方法f,但 它对CloneNotSupportedException 和InterruptedExceptioin 并没有作这些处理。那么,为什么这个程序可以编译呢? 上述分析的缺陷在于对“Type3.f 可以抛出在Type1.f 上声明的异常和在 Type2.f 上声明的异常”所做的假设。这并不正确,因为每一个接口都限制了方 法f 可以抛出的被检查异常集合。一个方法可以抛出的被检查异常集合是它所适 用的所有类型声明要抛出的被检查异常集合的交集,而不是合集。因此,静态类 型为Type3 的对象上的f 方法根本就不能抛出任何被检查异常。因此,Arcane3可以毫无错误地通过编译,并且打印Hello world。 解析</code></pre> <p>这三个程序说明了一项基本要求,即对于捕获被检查异常的catch 子句,只有在相应的try 子句可以抛出这些异常时才被允许。第二个程序说明了这项要求不会应用到的冷僻案例。第三个程序说明了多个继承而来的throws 子句取的是交集,即异常将减少而不是增加。</p> <h2><strong>下面程序执行结果?</strong></h2> <pre> <code class="language-java">class UnwelcomeGuest { public static final long GUEST_USER_ID = -1; private static final long USER_ID; static { try { USER_ID = getUserIdFromEnvironment(); } catch (IdUnavailableException e) { USER_ID = GUEST_USER_ID; System.out.println("Logging in as guest"); } } private static long getUserIdFromEnvironment() throws IdUnavailableException { throw new IdUnavailableException(); } public static void main(String[] args) { System.out.println("User ID: " + USER_ID); } } class IdUnavailableException extends Exception { } </code></pre> <p>程序将尝试着从其环境中读取一个用户ID,如果这种尝试失败了,则缺省地认为它是一个来宾用户。</p> <pre> <code class="language-java">该程序看起来很直观。对getUserIdFromEnvironment 的调用将抛出一个异常,从而使程序将GUEST_USER_ID(-1L)赋值给USER_ID,并打印Loggin in as guest。然后main 方法执行,使程序打印User ID: -1,但是实际上该程序并不能编译。出现了编译错误:The final field USER_ID may already have been assigned。 USER_ID 域是一个空final(blank final),它是一个在声明中没有进行初始化操作的final 域。很明显,只有在对USER_ID赋值失败时,才会在try 语句块中抛出异常,因此,在catch 语句块中赋值是相当安全的。不管怎样执行静态初始化操作语句块,只会对USER_ID 赋值一次,这正是空final 所要求的。 为什么编译器不知道这些呢?要确定一个程序是否可以不止一次地对一个空final 进行赋值是一个很困难的问题。事实上,这是不可能的。为了能够编写出一个编译器,语言规范在这一点上采用了保守的方式。在程序中,一个空final 域只有在它是明确未赋过值的地方才可以被赋值。因为它是保守的。 解析</code></pre> <h2><strong>下面程序执行结果?</strong></h2> <pre> <code class="language-java">public class Test1 { public static void main(String[] args) { try { System.out.println("Hello world"); System.exit(0); } finally { System.out.println("Goodbye world"); } } } </code></pre> <pre> <code class="language-java">try 语句块执行它的 println 语句,并且通过调用System.exit 来提前结束执行。在此时,可能认为控制权会转交给finally 语句块。然而,它只打印了Hello world。 不论try语句块的执行是正常地还是意外地结束,finally语句块确实都会执行。 然而在这个程序中,try 语句块根本就没有结束其执行过程。System.exit 方法将停止当前线程和所有其他当场死亡的线程。finally 子句的出现并不能给予线程继续去执行的特殊权限。 当System.exit 被调用时,虚拟机在关闭前要执行两项清理工作。首先,它执行所有的关闭挂钩操作,这些挂钩已经注册到了Runtime.addShutdownHook 上。这对于释放 VM 之外的资源将很有帮助。务必要为那些必须在VM 退出之前发生的行为关闭挂钩。 总之,System.exit 将立即停止所有的线程,它并不会使finally 语句块得 到调用,但是它在停止VM 之前会执行关闭挂钩操作。当VM 被关闭时,请使用关闭挂钩来终止外部资源。通过调用System.halt 可以在不执行关闭挂钩的情况下停止VM,但是这个方法很少使用。 解析</code></pre> <h2><strong>下面程序执行结果?</strong></h2> <pre> <code class="language-java">class Reluctant { private Reluctant internalInstance = new Reluctant(); public Reluctant() throws Exception { throw new Exception("I'm not coming out"); } public static void main(String[] args) { try { Reluctant b = new Reluctant(); System.out.println("Surprise!"); } catch (Exception ex) { System.out.println("I told you so"); } } } </code></pre> <pre> <code class="language-java">main 方法调用了 Reluctant 构造器,它将抛出一个异常。你可能期望catch 子句能够捕获这个异常,并且打印 I told you so。凑近仔细看看这个程序就会发现,Reluctant 实例还包含第二个内部实例,它的构造器也会抛出一个异常。无论抛出哪一个异常,看起来main 中的catch 子句都应该捕获它,但是当尝试着去运行它时,就会发现它压根没有去做这类的事情:它抛出了 StackOverflowError 异常,为什么呢? 与大多数抛出 StackOverflowError 异常的程序一样,本程序也包含了一个无限递归。当你调用一个构造器时,实例变量的初始化操作将先于构造器的程序体而运行。在本题中, internalInstance 变量的初始化操作递归调用了构造器,而该构造器通过再次调用Reluctant 构造器而初始化该变量自己的 internalInstance 域,如此无限递归下去。这些递归调用在构造器程序体获得执行机会之前就会抛出StackOverflowError 异常,而它是Error 的子类型而不是Exception 的子类型,所以catch 子句无法捕获它。 对于一个对象包含与它自己类型相同的实例的情况,并不少见。例如,链接节点、树节点和图节点都属于这种情况。必须非常小心地初始化这样的包含实例,以避免StackOverflowError 异常。 解析</code></pre> <h2><strong>下面程序执行结果?如有错误如何修改?</strong></h2> <pre> <code class="language-java">class Reluctant { private static Class<Reluctant> engineClass = Reluctant.class; private Engine engine = (Engine) engineClass.newInstance(); public Reluctant() { } } class Engine extends Reluctant { } </code></pre> <pre> <code class="language-java">构造器必须声明其实例初始化操作会抛出的所有被检查异常!尽管其构造器没有任何程序体,但是它将抛出两个被检查异常,InstantiationException 和IllegalAccessException。它们是Class.Instance 抛出的,该方法是在初始化engine 域的时候被调用的。 总之,实例初始化操作是先于构造器的程序体而运行的。实例初始化操作抛出的任何异常都会传播给构造器。如果初始化操作抛出的是被检查异常,那么构造器必须声明也会抛出这些异常,但是应该避免这样做,因为它会造成混乱。最后,对于我们所设计的类,如果其实例包含同样属于这个类的其他实例,那么对这种无限递归要格外当心。 解析</code></pre> <p>我的修改:</p> <pre> <code class="language-java">class Reluctant { private static Class<Reluctant> engineClass = Reluctant.class; private Engine engine = newEngine(); private static Engine newEngine() { try { return (Engine) engineClass.newInstance(); } catch (IllegalAccessException e) { throw new AssertionError(e); } catch (InstantiationException e) { throw new AssertionError(e); } } public Reluctant() { } } class Engine extends Reluctant { } View Code</code></pre> <h2><strong>下面程序执行结果?</strong></h2> <pre> <code class="language-java">class Reluctant { static void copy(String src, String dest) throws IOException { InputStream in = null; OutputStream out = null; try { in = new FileInputStream(src); out = new FileOutputStream(dest); byte[] buf = new byte[1024]; int n; while ((n = in.read(buf)) > 0) out.write(buf, 0, n); } finally { if (in != null) in.close(); if (out != null) out.close(); } } } </code></pre> <p>方法将一个文件拷贝到另一个文件,并且被设计为要关闭它所创建的每一个流,即使它碰到I/O 错误也要如此。</p> <pre> <code class="language-java">这个程序看起来已经面面俱到了。其流(in 和out)被初始化为null,并且新的流一旦被创建,它们马上就被设置为这些流域的新值。对于这些域所引用的流,如果不为空,则finally 语句块会将其关闭。即便在拷贝操作引发了一个IOException 的情况下,finally 语句块也会在方法返回之前执行。 问题在finally 语句块自身中。close 方法也可能会抛出IOException 异常。如 果这正好发生在in.close 被调用之时,那么这个异常就会阻止out.close 被调 用,从而使输出流仍保持在开放状态。 之前说过,不要在finally语句里执行任何退出语句,包括continue等,对close 的调用可能会导致finally 语句块意外结束。编译器并不能发现此问题,因为close 方法抛出的异常与read 和write 抛出的异常类型相同,而其外围方法(copy)声明将传播该异常。 解决方式是将每一个close 都包装在一个嵌套的try 语句块中。 解析</code></pre> <pre> <code class="language-java">从5.0 版本开始,可以利用Closeable 接口: class Reluctant { static void copy(String src, String dest) throws IOException { InputStream in = null; OutputStream out = null; try { in = new FileInputStream(src); out = new FileOutputStream(dest); byte[] buf = new byte[1024]; int n; while ((n = in.read(buf)) > 0) out.write(buf, 0, n); } finally { closeIgnoringException(in); closeIgnoringException(out); } } private static void closeIgnoringException(Closeable c) { if (c != null) { try { c.close(); } catch (IOException ex) { // There is nothing we can do if close fails } } } } 改进</code></pre> <h2><strong>下面程序执行结果?</strong></h2> <pre> <code class="language-java">class Reluctant { public static void main(String[] args) { int[][] tests = { { 6, 5, 4, 3, 2, 1 }, { 1, 2 }, { 1, 2, 3 }, { 1, 2, 3, 4 }, { 1 } }; int successCount = 0; try { int i = 0; while (true) { if (thirdElementIsThree(tests[i++])) successCount++; } } catch (ArrayIndexOutOfBoundsException e) { // No more tests to process } System.out.println(successCount); } private static boolean thirdElementIsThree(int[] a) { return a.length >= 3 & a[2] == 3; } } </code></pre> <pre> <code class="language-java">该程序用thirdElementIsThree 方法测试了tests 数组中的每一个元素。如果传递给thirdElementIsThree 的参数具有3 个或更多的元素,并且其第三个元素等于3,那么该方法将返回true。对于tests中的5 个元素来说,有2 个将返回true,因此看起来该程序应该打印2。 如果运行它,就会发现它打印0。 事实上,这个程序犯了两个错误。 第一个错误是该程序使用了一种可怕的循环惯用法,该惯用法依赖的是对数组的访问会抛出异常。这种惯用法不仅难以阅读,而且运行速度还非常地慢。不要使用异常来进行循环控制;应该只为异常条件而使用异常。应该使用惯用的循环来遍历或者使用for-each语句。 修改之后还是有问题。抛出越界异常。 第二个错误,如果传递给thirdElementIsThree 的参数具有3 个或更多的元素,并且其第三个元素等于3,那么该方法将返回true。问题是在这些条件不满足时它会做些什么。如果观察返回的布尔表达式,就会发现它与大多数 AND 操作不一样。这个表达式是a.length >= 3 & a[2] == 3。通常,在这种情况下看到的是 && 操作符,而这个表达式使用的是 & 操作符。& 操作符有其他的含义。除了常见的被当作整型操作数的位AND 操作之外,当被用于布尔操作数时,它的功能被重载为逻辑AND 操作符。这个操作符与更经常被使用的条件AND 操作符有所不同,& 操作符总是要计算它的两个操作数,而 && 操作符在其左边的操作数被计算为false 时,就不再计算右边的操作数。因此,thirdElementIsThree 方法总是要试图访问其数组参数的第三个元素,即使该数组参数的元素不足3 个也是如此。 修改 & 操作符替换为 && 操作符即可。通过这样的修改,这个程序就可以打印出我们所期望的2 了。 解析</code></pre> <p>总之,尽量不要去用异常终止循环,因为这种用法非常不清晰,而且会掩盖bug。要意识到逻辑 AND 和 OR 操作符的存在,并且不要因无意识的误用而受害。</p> <h2><strong>观察下面三个类,按照要求说明执行结果?</strong></h2> <pre> <code class="language-java">class Strange1 { public static void main(String[] args) { try { Missing m = new Missing(); } catch (java.lang.NoClassDefFoundError ex) { System.out.println("Got it!"); } } } class Strange2 { public static void main(String[] args) { Missing m; try { m = new Missing(); } catch (java.lang.NoClassDefFoundError ex) { System.out.println("Got it!"); } } } class Missing { Missing() { } } </code></pre> <p>编译所有这三个类,然后在运行Strange1 和Strange2 之前删除Missing.class 文件,然后运行前面两个类,结果分别是什么?</p> <pre> <code class="language-java">其中一个抛出了一个未被捕获的NoClassDefFoundError 异常,而另一个却打印出了Got it! 程序Strange1 只在其try 语句块中提及Missing 类型,因此你可能会认为它捕获 NoClassDefFoundError 异常,并打印Got it!另一方面,程序Strange2 在 try 语句块之外声明了一个Missing 类型的变量,因此你可能会认为所产生的NoClassDefFoundError 异常不会被捕获。 运行这些程序,就会看到它们的行为正好相反:Strange1抛出了未被捕获的NoClassDefFoundError异常,Strange2 却打印出了Got it!要查明为什么会是这样,我们需要研究一下由编译器生成的这些程序的字节码。 解析</code></pre> <pre> <code class="language-java">0: new 3: dup 4: invokespecial #3; //Method Missing."<init>":()V 7: astore_1 8: goto 20 11: astore_1 12: getstatic #5; // Field System.out:Ljava/io/PrintStream; 15: ldc #6; // String "Got it!" 17: invokevirtual #7;//Method PrintStream.println: (String); V 20: return Exception table: from to target type 0 8 11 Class java/lang/NoClassDefFoundError </code></pre> <p>Strange2.main 相对应的字节码与其只有一条指令不同:11: astore_2,这是一条将 catch 语句块中的捕获异常存储到捕获参数 ex 中的指令。在 Strange1 中,这个参数是存储在 VM 变量 1 中的,而在Strange2 中,它是存储在VM 变量 2 中的。这就是两个类之间唯一的差异,但是它所造成的程序行为上的差异是很大的!</p> <p>为了运行一个程序,VM 要加载和初始化包含main 方法的类。在加载和初始化之间,VM 必须链接(link)类。链接的第一阶段是校验,校验要确保一个类是完整的,并且遵循语法要求。两个main都有一个连接点,连接点是汇聚控制流的,上面main的连接点就是指令20.try如果正常结束,就会执行指令8,goto到20,catch语句块结束也要从指令17走到指令20.问题就出在这里,现在有两条路径达到链接点,由于两条路径中各有一个astore(指令7和指令11),Strange1.main在路径1(try到return)中,用VM变量1存储了m,在路径2(catch到return)中又用VM变量1存储异常,因此要进行变量类型合并。所以要检测Missing和NoClassDefFoundError的超类,因为Missing.class已经被删除了,所以问题出现了,要抛出NoClassDefFoundError异常。因为此时还是在main执行之前发生的,所以当然无法捕获了。这个异常是在校验期间、在类被初始化之前,并且在main 方法开始执行之前很早就抛出的。这就解释了为什么没有打印出任何关于这个未被捕获异常的跟踪栈信息。 要想编写一个能够探测出某个类是否丢失的程序,请使用反射来引用类而不要使用通常的语言结构。</p> <pre> <code class="language-java">class Strange { public static void main(String[] args) throws Exception { try { Object m = Class.forName("Missing").newInstance(); } catch (ClassNotFoundException ex) { System.err.println("Got it!"); } } } View Code</code></pre> <p>总之,不要对捕获NoClassDefFoundError 形成依赖。语言规范非常仔细地描述了类初始化是在何时发生的,但是类被加载的时机却不可预测。更一般地讲,捕获 Error 及其子类型几乎是完全不恰当的。这些异常是为那些不能被恢复的错误而保留的。</p> <h2><strong>下面程序中的方法workHard() 执行后会出现什么结果?</strong></h2> <pre> <code class="language-java">class Workout { public static void main(String[] args) { workHard(); System.out.println("It's nap time."); } private static void workHard() { try { workHard(); } finally { workHard(); } } } </code></pre> <p>要不是有try-finally 语句,该程序的行为将非常明显:workHard 方法递归地调用它自身,直到程序抛出StackOverflowError,在此刻它以这个未捕获的异常而终止。但是,try-finally 语句把事情搞得复杂了。当它试图抛出StackOverflowError 时,程序将会在finally 语句块的workHard 方法中终止,这样,它就递归调用了自己。这看起来确实就像是一个无限循环的秘方,但是这个程序真的会无限循环下去吗?如果运行它,它似乎确实是这么做的,假设栈的深度为3,这比它实际的深度要小得多。现在让我们来跟踪其执行过程。main 方法调用workHard,而它又从其try 语句块中递归地调用了自己,然后它再一次从其try 语句块中调用了自己。在此时,栈的深度是3。当workHard 方法试图从其try 语句块中再次调用自己时,该调用立即就会以StackOverflowError 而失败。这个错误是在最内部的finally 语句块中被捕获的,在此处栈的深度已经达到了3。在那里,workHard 方法试图递归地调用它自己,但是该调用却以StackOverflowError 而失败。这个错误将在上一级的finally 语句块中被捕获,在此处站的深度是2。该finally 中的调用将与相对应的try 语句块具有相同的行为:最终都会产生一个StackOverflowError。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/493f90ce3c651e1fec14f2b08b462514.png"></p> <p>当栈深度为2的时候,WorkOut 的运行过程如图所示。在这张图中,对workHard 的调用用箭头表示,workHard 的执行用圆圈表示,try 语句块中的调用用向左边的向下箭头表示,finally 语句块中的调用用向右边的向下箭头表示。箭头上的数字描述了调用的顺序。这张图展示了一个深度为0 的调用(即main 中的调用),两个深度为1 的调用,四个深度为2 的调用,总共是7 个调用。那4个深度为2的调用每一个都会立即产生StackOverflowError。至少在把栈的深度限制为2 的VM 上,该程序不会是一个无限循环:它在7 个调用和4 个异常之后就会终止。但是对于真实的VM 又会怎样呢?它仍然不会是一个无限循环。其调用图与前面的图相似,只不过要大得多得多而已。那么,究竟大到什么程度呢?</p> <p>有一个快速的试验表明许多 VM 都将栈的深度限制为1024 ,因此,调用的数量就是1+2+4+8…+2^1024,而抛出的异常的数量是2^1024。假设我们的机器可以在每秒钟内执行1010 个调用,并产生1010个异常,按照当前的标准,这个假设的数量已经相当高了。在这样的假设条件下,程序将在大约1.7×10^291 年后终止,而太阳的生命周期大约是1010 年,可以很确定,没有人能够看到这个程序终止的时刻。尽管它不是一个无限循环,但是它也就算是一个无限循环吧。</p> <p>从技术角度讲,调用图是一棵完全二叉树,它的深度就是VM 的栈深度的上限。WorkOut 程序的执行过程等于是在先序遍历这棵树。在先序遍历中,程序先访问一个节点,然后递归地访问它的左子树和右子树。对于树中的每一条边,都会产生一个调用,而对于树中的每一个节点,都会抛出一个异常。它证明了指数算法对于除了最小输入之外的所有情况都是不可行的,它还表明了使用递归可以编写出一个指数算法,但是不推荐 。</p> <h2><strong>如下代码的输出是?</strong></h2> <p><img src="https://simg.open-open.com/show/641ad0d232064980064bfb171e9907ad.png"></p> <pre> <code class="language-java">由于arr[0] =0,所以在进入 test()方法里面会在第一个if 上抛出一个 NullPointerException,接着会执行 finally 的语句, (finally语句先于 throw 语句执行),输出一个 e,然后回到 main方法中,由于捕捉到异常,所以进入到主调方法的catch语句中,然后打印一个 E,抛出异常后,程序停止运行。所以最终结果为 eE 解析 </code></pre> <h2><strong>以下关于JAVA语言异常处理描述正确的有?</strong></h2> <p><img src="https://simg.open-open.com/show/1c6f1e9d1a87d2c65e4636b22d9c37c6.png"></p> <pre> <code class="language-java">Java语言中的异常处理包括声明异常、抛出异常、捕获异常和处理异常四个环节。 throw用于抛出异常。throws关键字可以在方法上声明该方法要抛出的异常,然后在方法内部通过throw抛出异常对象。 try是用于检测被包住的语句块是否出现异常,如果有异常,则抛出异常,并执行catch语句。 cacth用于捕获从try中抛出的异常并作出处理。 finally语句块是不管有没有出现异常都要执行的内容。 解析</code></pre> <h2><strong>运行时异常与一般异常有何异同?</strong></h2> <pre> <code class="language-java">异常表示程序运行过程中可能出现的非正常状态,运行时异常(也叫非检查异常)表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。javac要求方法必须声明,抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。 解析</code></pre> <h2><strong>error和exception有什么区别?</strong></h2> <pre> <code class="language-java">Error指程序本身不能恢复和克服的一种严重问题。比如说内存溢出,线程死锁。不可能指望程序能处理这样的情况。Exception指软件本身设计的问题(系统异常也叫运行时,使用者无法克服)或运行环境变化导致的问题(普通异常也叫检查异常,使用者可以克服),程序本身能恢复和克服。也就是说它表示如果程序运行正常,从不会发生的情况。 解析</code></pre> <h2><strong>简述Java中的异常处理机制的简单原理和应用。</strong></h2> <pre> <code class="language-java">异常是指java程序运行时(非编译)所发生的非正常情况或错误, Java使用面向对象的方式来处理异常,它把程序中发生的每个异常也都分别封装到一个对象来表示,该对象中包含有异常的信息。 虚拟机必须宕机的错误属于error,程序可以死掉也可以不死掉的错误属于系统异常,程序不应该死掉的错误属于普通异常; Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为java.lang.Throwable,下面又派生了两个子类:Error和Exception;Error表示应用程序本身无法克服和恢复的一种严重问题,程序只有死的份了,例如,说内存溢出和线程死锁等系统问题。Exception表示程序还能够克服和恢复的问题,其中又分为系统异常(运行时异常,非检查)和普通异常(检查); 系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件死掉,例如,数组脚本越界(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException)、类转换异常(ClassCastException); 普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间不够,IO异常,以及SQL异常发生这样的异常后,程序不应该死掉。 java为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须try..catch处理或用throws声明继续抛给上层调用方法处理,所以普通异常也称为checked异常,checked 异常也就是我们经常遇到的IO异常,以及SQL异常都是这种异常。对于这种异常,JAVA编译器强制要求我们必需对出现的这些异常进行catch。因为普通异常和运行环境有关系,具客观性,不可决定性和不可预测性!必须捕获或者抛给上层。而系统异常(也叫rutime exception)可以处理也可以不处理,所以编译器不强制用try..catch处理或用throws声明,所以系统异常也称为unchecked异常。 解析</code></pre> <h2><strong>请写出你最常见到的5个runtime exception。</strong></h2> <pre> <code class="language-java">这道题主要看代码量到底多大,如果你长期写代码的,应该经常都看到过一些系统方面的异常,不一定真要回答出5个具体的系统异常,但要能够说出什么是系统异常,以及几个系统异常就可以了,当然,这些异常完全用其英文名称来写是最好的!所谓系统异常(不强制处理),都是RuntimeException的子类,在jdk doc中查RuntimeException类,就可以看到其所有的子类列表,也就是看到了所有的系统异常。我比较有印象的系统异常有:NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException。 解析</code></pre> <p> </p> <p>来自:http://www.cnblogs.com/kubixuesheng/p/5968347.html</p> <p> </p>