(點選上方公眾號,可快速關註)
來源:ImportNew – Jerry Lee
呃,你是不是寫Java已經有些年頭了?還依稀記得這些吧: 那些年,它還叫做Oak;那些年,OO還是個熱門話題;那些年,C++同學們覺得Java是沒有出路的;那些年,Applet還風頭正勁……
但我打賭下麵的這些事中至少有一半你還不知道。這周我們來聊聊這些會讓你有些驚訝的Java內部的那些事兒吧。
1. 其實沒有受檢異常(checked exception)
是的!JVM才不知道這類事情,只有Java語言才會知道。
今天,大家都贊同受檢異常是個設計失誤,一個Java語言中的設計失誤。正如 Bruce Eckel 在布拉格的GeeCON會議上演示的總結中說的, Java之後的其它語言都沒有再涉及受檢異常了,甚至Java 8的新式流API(Streams API)都不再擁抱受檢異常 (以lambda的方式使用IO和JDBC,這個API用起來還是有些痛苦的。)
http://www.geecon.cz/speakers/?id=2
想證明JVM不理會受檢異常?試試下麵的這段程式碼:
public class Test {
// 方法沒有宣告throws
public static void main(String[] args) {
doThrow(new SQLException());
}
static void doThrow(Exception e) {
Test.
doThrow0(e); }
@SuppressWarnings(“unchecked”)
static
void doThrow0(Exception e) throws E {
throw (E) e;
}
}
不僅可以編譯透過,並且也丟擲了SQLException,你甚至都不需要用上Lombok的@SneakyThrows。
更多細節,可以再看看這篇文章,或Stack Overflow上的這個問題。
這篇文章
http://blog.jooq.org/2012/09/14/throw-checked-exceptions-like-runtime-exceptions-in-java/
問題
http://stackoverflow.com/q/12580598/521799
2. 可以有隻是傳回型別不同的多載方法
下麵的程式碼不能編譯,是吧?
class Test {
Object x() { return “abc”; }
String x() { return “123”; }
}
是的!Java語言不允許一個類裡有2個方法是『多載一致』的,而不會關心這2個方法的throws子句或傳回型別實際是不同的。
但是等一下!來看看Class.getMethod(String, Class…)方法的Javadoc:
註意,可能在一個類中會有多個匹配的方法,因為儘管Java語言禁止在一個類中多個方法簽名相同只是傳回型別不同,但是JVM並不禁止。 這讓JVM可以更靈活地去實現各種語言特性。比如,可以用橋方法(bridge method)來實現方法的協變傳回型別;橋方法和被多載的方法可以有相同的方法簽名,但傳回型別不同。
嗯,這個說的通。實際上,當寫了下麵的程式碼時,就發生了這樣的情況:
abstract class Parent
{ abstract T x();
}
class Child extends Parent
{ @Override
String x() { return “abc”; }
}
檢視一下Child類所生成的位元組碼:
// Method descriptor #15 ()Ljava/lang/String;
// Stack: 1, Locals: 1
java.lang.String x();
0 ldc
[16] 2 areturn
Line numbers:
[pc: 0, line: 7]
Local variable table:
[pc: 0, pc: 3] local: this index: 0 type: Child
// Method descriptor #18 ()Ljava/lang/Object;
// Stack: 1, Locals: 1
bridge synthetic java.lang.Object x();
0 aload_0 [this]
1 invokevirtual Child.x() : java.lang.String [19]
4 areturn
Line numbers:
[pc: 0, line: 1]
在位元組碼中,T實際上就是Object型別。這很好理解。
合成的橋方法實際上是由編譯器生成的,因為在一些呼叫場景下,Parent.x()方法簽名的傳回型別期望是Object。 新增泛型而不生成這個橋方法,不可能做到二進位制相容。 所以,讓JVM允許這個特性,可以愉快解決這個問題(實際上可以允許協變多載的方法包含有副作用的邏輯)。 聰明不?呵呵~
你是不是想要扎入語言規範和核心看看?可以在這裡找到更多有意思的細節。
http://stackoverflow.com/q/442026/521799
3. 所有這些寫法都是二維陣列!
class Test {
int[][] a() { return new int[0][]; }
int[] b() [] { return new int[0][]; }
int c() [][] { return new int[0][]; }
}
是的,這是真的。儘管你的人肉解析器不能馬上理解上面這些方法的傳回型別,但都是一樣的!下麵的程式碼也類似:
class Test {
int[][] a = {{}};
int[] b[] = {{}};
int c[][] = {{}};
}
是不是覺得這個很2B?想象一下在上面的程式碼中使用JSR-308/Java 8的型別註解。 語法糖的數目要爆炸了吧!
@Target(ElementType.TYPE_USE)
@interface Crazy {}
class Test {
@Crazy int[][] a1 = {{}};
int @Crazy [][] a2 = {{}};
int[] @Crazy [] a3 = {{}};
@Crazy int[] b1[] = {{}};
int @Crazy [] b2[] = {{}};
int[] b3 @Crazy [] = {{}};
@Crazy int c1[][] = {{}};
int c2 @Crazy [][] = {{}};
int c3[] @Crazy [] = {{}};
}
型別註解。這個設計引入的詭異在程度上僅僅被它解決問題的能力超過。
或換句話說:
在我4週休假前的最後一個提交裡,我寫了這樣的程式碼,然後。。。
【譯註:然後,親愛的同事你,就有得火救啦,哼,哼哼,哦哈哈哈哈~】
請找出上面用法合適的使用場景,還是留給你作為一個練習吧。
4. 你沒有掌握條件運算式
呃,你認為自己知道什麼時候該使用條件運算式?面對現實吧,你還不知道。大部分人會下麵的2個程式碼段是等價的:
Object o1 = true ? new Integer(1) : new Double(2.0);
等同於:
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
讓你失望了。來做個簡單的測試吧:
System.out.println(o1);
System.out.println(o2);
列印結果是:
1.0
1
哦!如果『需要』,條件運運算元會做數值型別的型別提升,這個『需要』有非常非常非常強的引號。因為,你覺得下麵的程式會丟擲NullPointerException嗎?
Integer i = new Integer(1);
if (i.equals(1))
i = null;
Double d = new Double(2.0);
Object o = true ? i : d; // NullPointerException!
System.out.println(o);
關於這一條的更多的資訊可以在這裡找到。
http://blog.jooq.org/2013/10/08/java-auto-unboxing-gotcha-beware/
5. 你沒有掌握複合賦值運運算元
是不是覺得不服?來看看下麵的2行程式碼:
i += j;
i = i + j;
直覺上認為,2行程式碼是等價的,對吧?但結果即不是!JLS(Java語言規範)指出:
複合賦值運運算元運算式 E1 op= E2 等價於 E1 = (T)((E1) op (E2)) 其中T是E1的型別,但E1只會被求值一次。
這個做法太漂亮了,請允許我取用Peter Lawrey在Stack Overflow上的回答。
http://stackoverflow.com/a/8710747/521799
使用*=或/=作為例子可以方便說明其中的轉型問題:
byte b = 10;
b *= 5.7;
System.out.println(b); // prints 57
byte b = 100;
b /= 2.5;
System.out.println(b); // prints 40
char ch = ‘0’;
ch *= 1.1;
System.out.println(ch); // prints ‘4’
char ch = ‘A’;
ch *= 1.5;
System.out.println(ch); // prints ‘a’
為什麼這個真是太有用了?如果我要在程式碼中,就地對字元做轉型和乘法。然後,你懂的……
6. 隨機Integer
這條其實是一個迷題,先不要看解答。看看你能不能自己找出解法。執行下麵的程式碼:
for (int i = 0; i < 10; i++) {
System.out.println((Integer) i);
}
…… 然後要得到類似下麵的輸出(每次輸出是隨機結果):
92
221
45
48
236
183
39
193
33
84
這怎麼可能?!
我要劇透了…… 解答走起……
好吧,解答在這裡(http://blog.jooq.org/2013/10/17/add-some-entropy-to-your-jvm/), 和用反射改寫JDK的Integer快取,然後使用自動打包解包(auto-boxing/auto-unboxing)有關。 同學們請勿模仿!或換句話說,想想會有這樣的狀況,再說一次:
在我4週休假前的最後一個提交裡,我寫了這樣的程式碼,然後。。。
【譯註:然後,親愛的同事你,就有得火救啦,哼,哼哼,哦哈哈哈哈~】
7. GOTO
這條是我的最愛。Java是有GOTO的!打上這行程式碼:
int goto = 1;
結果是:
Test.java:44: error:
expected int goto = 1;
^
這是因為goto是個還未使用的關鍵字,保留了為以後可以用……
但這不是我要說的讓你興奮的內容。讓你興奮的是,你是可以用break、continue和有標簽的程式碼塊來實現goto的:
向前跳:
label: {
// do stuff
if (check) break label;
// do more stuff
}
對應的位元組碼是:
2 iload_1 [check]
3 ifeq 6 // 向前跳
6 ..
向後跳:
label: do {
// do stuff
if (check) continue label;
// do more stuff
break label;
} while(true);
對應的位元組碼是:
2 iload_1 [check]
3 ifeq 9
6 goto 2 // 向後跳
9 ..
8. Java是有型別別名的
在別的語言中(比如,Ceylon), 可以方便地定義型別別名:
interface People => Set
;
這樣定義的People可以和Set
People? p1 = null;
Set
? p2 = p1; People? p3 = p2;
在Java中不能在頂級(top level)定義型別別名。但可以在類級別、或方法級別定義。 如果對Integer、Long這樣名字不滿意,想更短的名字:I和L。很簡單:
class Test {
void x(I i, L l) { System.out.println(
i.intValue() + “, ” +
l.longValue()
);
}
}
上面的程式碼中,在Test類級別中I是Integer的『別名』,在x方法級別,L是Long的『別名』。可以這樣來呼叫這個方法:
new Test().x(1, 2L);
當然這個用法不嚴謹。在例子中,Integer、Long都是final型別,結果I和L 效果上是個別名 (大部分情況下是。賦值相容性只是單向的)。如果用非final型別(比如,Object),還是要使用原來的泛型引數型別。
玩夠了這些噁心的小把戲。現在要上乾貨了!
9. 有些型別的關係是不確定的
好,這條會很稀奇古怪,你先來杯咖啡,再集中精神來看。看看下麵的2個型別:
// 一個輔助類。也可以直接使用List
interface Type
{}
class C implements Type
> {} class D
implements Type
>>> {}
型別C和D是啥意思呢?
這2個型別宣告中包含了遞迴,和java.lang.Enum的宣告類似 (但有微妙的不同):
public abstract class Enum
> { … }
有了上面的型別宣告,一個實際的enum實現只是語法糖:
// 這樣的宣告
enum MyEnum {}
// 實際只是下麵寫法的語法糖:
class MyEnum extends Enum
{ … }
記住上面的這點後,回到我們的2個型別宣告上。下麵的程式碼可以編譯透過嗎?
class Test {
Type super C> c = new C();
Type super D
> d = new D (); }
很難的問題,Ross Tate回答過這個問題。答案實際上是不確定的:
C是Type super C>的子類嗎?
步驟 0) C
步驟 1) Type
> 步驟 2) C (檢查萬用字元 ? super C)
步驟 . . . (進入死迴圈)
然後:
D是Type super D
>的子類嗎?
步驟 0) D
> 步驟 1) Type
>>> > 步驟 2) D
>> 步驟 3) List
>> >
步驟 4) D
> >> 步驟 . . . (進入永遠的展開中)
試著在你的Eclipse中編譯上面的程式碼,會Crash!(別擔心,我已經提交了一個Bug。)
我們繼續深挖下去……
在Java中有些型別的關係是不確定的!
如果你有興趣知道更多古怪Java行為的細節,可以讀一下Ross Tate的論文『馴服Java型別系統的萬用字元』 (由Ross Tate、Alan Leung和Sorin Lerner合著),或者也可以看看我們在子型別多型和泛型多型的關聯方面的思索。
『馴服Java型別系統的萬用字元』
http://www.cs.cornell.edu/~ross/publications/tamewild/tamewild-tate-pldi11.pdf
子型別多型和泛型多型的關聯方面的思索
http://blog.jooq.org/2013/06/28/the-dangers-of-correlating-subtype-polymorphism-with-generic-polymorphism/
10. 型別交集(Type intersections)
Java有個很古怪的特性叫型別交集。你可以宣告一個(泛型)型別,這個型別是2個型別的交集。比如:
class Test
{ }
系結到類Test的實體上的泛型型別引數T必須同時實現Serializable和Cloneable。比如,String不能做系結,但Date可以:
// 編譯不透過!
Test
s = null;
// 編譯透過
Test
d = null;
Java 8保留了這個特性,你可以轉型成臨時的型別交集。這有什麼用? 幾乎沒有一點用,但如果你想強轉一個lambda運算式成這樣的一個型別,就沒有其它的方法了。 假定你在方法上有了這個蛋疼的型別限制:
void execute(T t) {}
你想一個Runnable同時也是個Serializable,這樣你可能在另外的地方執行它並透過網路傳送它。lambda和序列化都有點古怪。
lambda是可以序列化的:
如果lambda運算式的標的型別和它捕獲的引數(captured arguments)是可以序列化的,則這個lambda運算式是可序列化的。
但即使滿足這個條件,lambda運算式並沒有自動實現Serializable這個標記介面(marker interface)。 為了強製成為這個型別,就必須使用轉型。但如果只轉型成Serializable …
execute((Serializable) (() -> {}));
… 則這個lambda運算式不再是一個Runnable。
呃……
So……
同時轉型成2個型別:
execute((Runnable & Serializable) (() -> {}));
結論
一般我只對SQL會說這樣的話,但是時候用下麵的話來結束這篇文章了:
Java中包含的詭異在程度上僅僅被它解決問題的能力超過。
看完本文有收穫?請轉發分享給更多人
關註「ImportNew」,提升Java技能