Skip to content

Instantly share code, notes, and snippets.

@pygman
Created August 30, 2016 08:10
Show Gist options
  • Save pygman/a31419b1dfc2bc38d00098bcff9a8982 to your computer and use it in GitHub Desktop.
Save pygman/a31419b1dfc2bc38d00098bcff9a8982 to your computer and use it in GitHub Desktop.
无聊的难题
public class Mocker<T extends Exception>{
private void pleaseThrow(final Exception t)throws T{
throw (T)t;
}
public static void main(final String[] args){
try{
new Mocker<RuntimeException>().pleaseThrow(new SQLException());
}catch(final SQLException ex){
ex.printStackTrace();
}
}
}
/*
RuntimeException和SQLException都继承自Exception,但是在这个代码中RuntimeException是未检查的异常,而SQLException是受检异常。
Java的泛型并不是具体化的。这意味着在编译时,泛型的类型信息会“丢失”,并且泛型参数像是被它的限定类型替换了一样,或者当限定类型不存在时,泛型参数被替换成了Object。这就是大家所说的类型“擦除”。
我们天真地希望第七行能产生一个编译错误,因为我们不能将SQLException转换成RuntimeException,但是这并不会发生。发生的是将T替换成了Exception,所以我们有:
throw (Exception) t; // t is also an Exception
pleaseThrow方法期望一个Exception,并且T被替换成了Exception,因此类型转换被擦除了,就像没写这个代码一样。这一点我们可从下面的字节码中得到佐证:
我们再看一下,如果代码中没有涉及泛型,那么编译产生的字节码是什么样的,我们看到,在ATHROW前会有如下的代码:
CHECKCAST java/lang/RuntimeException
现在,我们可以确信,代码中并没有涉及到类型转换,因此我们可以排除下面这两个选项:
“编译错误,因为我们不能将SQLException类型转换为RuntimeException”
“抛出ClassCastException,因为SQLException不是RuntimeException的一个实例”
因此毕竟我们抛出了SQLException,然后你希望它能被catch代码块捕获,然后打印它的堆栈跟踪信息。然而,事与愿违。
这个代码具有欺骗性,它使得编译器和我们一样变得困惑。这段代码让编译器认为catch代码块是不能到达的。对于不知情的旁观者来说,代码中并没有SQLException。所以,正确答案是:编译失败,因为编译器认为SQLException不会从try代码块中抛出-但是实际上它确实能抛出!
再次感谢Alexandru与我们分享这个问题!我们可以用另一个很酷的方式来查看代码中的错误以及SQLException实际上是怎样抛出的,这个方法是:修改catch代码块,把它修改为接收一个RuntimeException。这样你就可以看到SQLException的堆栈信息了。(实际上SQLException也并没有被catch代码段捕获,而是被虚拟机捕获并打印出异常栈的信息。)
*/
public class MyClass0830{
private String name;
public static void main(String args[]){
MyClass0830 m1 = new MyClass0830();
MyClass0830 m2 = new MyClass0830();
m1.name = m2.name = "m1";
callMe(m1, m2);
System.out.println(m1 + " & " + m2);
}
private static void callMe(MyClass... m){
m[0] = m[1];
m[1].name = "new name";
}
}
/*
这道题实际上简单得多,我们只要看到第十二行,它直接打印了m1和m2,而不是m1.name和m2.name。这段代码狡猾的地方在于,当我们要打印一个对象时,Java使用的是toString方法。“name”属性是我们自己加入的,如果你忘记这点,其他地方都判断正确的话,你可能会错误地选择m1&new name这个答案。
这行代码将两个对象的name属性都赋值为”m1”。
1
m1.name = m2.name = “m1";
然后callMe方法将m2对象的name属性设置成”new name”,然后代码就结束了。
但是,这个代码片段实际上将会打印出如下信息,包括类名称以及它们的哈希码:
1
MyClass@3d0bc85 & MyClass@7d08c1b7
所以正确的答案是“None of the above”
*/
private static final List<String> NAMES = new ArrayList<String>(){{
add("John");
System.out.println(NAMES);
}};
/*
很少有开发者知道这个初始化常量集合的简便语法,虽然这个语法会带来一些副作用。但事实上,这个语法鲜为人知未免不是一件好事。在感叹之后,你看到,我们往list里添加了一个元素,然后打印这个list。正常情况下,你期望看到打印的结果是[John],但是利用两个花括号进行初始化是有另一套初始化过程的。这里,我们用了一个匿名类来初始化一个List,当要打印NAMES时,实际上打印出来的是null,这是因为初始化程序尚未完成,此时的list是空的。
*/
Map<String, Object> collection = new TreeMap<>();
System.out.println(collection.compute("foo",(k,v)->(v == null)? new ArrayList<Object>() : ((List)v).add("bar")));
System.out.println(collection.compute("foo",(k,v)->(v == null)? new ArrayList<Object>() : ((List)v).add("ber")));
/*
好吧,来看看代码。compute方法通过key在map中查找一个value。如果这个value是null,则插入(key, value),并返回value。因为开始时,这个list是空的,“foo”值并不存在,v是null。然后,我们向map中插入一个“foo”并且“foo”指向new ArrayList<Object>(),此时的ArrayList对象是空的,所以它打印出[]。
下一行,“foo”键值存在于map容器中,所以我们计算右边的表达式。ArrayList对象成功转换为List类型,然后“ber”字符串被插入到List中。add方法返回true,因此true就是第二行打印的内容。
所以正确的答案是”[]true”。
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment