關于Java你可能不知道的10件事
呃,你是不是寫
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
用起來還是有些痛苦的。)
想證明JVM
不理會受檢異常?試試下面的這段代碼:
public class Test { // 方法沒有聲明throws public static void main(String[] args) { doThrow(new SQLException()); } static void doThrow(Exception e) { Test.<RuntimeException> doThrow0(e); } @SuppressWarnings("unchecked") static <E extends Exception> void doThrow0(Exception e) throws E { throw (E) e; } }
不僅可以編譯通過,并且也拋出了SQLException
,你甚至都不需要用上Lombok
的@SneakyThrows
。
更多細節,可以再看看這篇文章,或Stack Overflow
上的這個問題。
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<T> { abstract T x(); } class Child extends Parent<String> { @Override String x() { return "abc"; } }
查看一下Child
類所生成的字節碼:
// Method descriptor #15 ()Ljava/lang/String; // Stack: 1, Locals: 1 java.lang.String x(); 0 ldc <String "abc"> [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
允許這個特性,可以愉快解決這個問題(實際上可以允許協變重載的方法包含有副作用的邏輯)。 聰明不?呵呵~
你是不是想要扎入語言規范和內核看看?可以在這里找到更多有意思的細節。
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);
關于這一條的更多的信息可以在這里找到。
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
上的回答:
使用*=
或/=
作為例子可以方便說明其中的轉型問題:
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: <identifier> 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<Person>;
這樣定義的People
可以和Set<Person>
互換地使用:
People? p1 = null; Set<Person>? p2 = p1; People? p3 = p2;
在Java
中不能在頂級(top level
)定義類型別名。但可以在類級別、或方法級別定義。 如果對Integer
、Long
這樣名字不滿意,想更短的名字:I
和L
。很簡單:
class Test<I extends Integer> { <L extends Long> 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<T> {} class C implements Type<Type<? super C>> {} class D<P> implements Type<Type<? super D<D<P>>>> {}
類型C
和D
是啥意思呢?
這2個類型聲明中包含了遞歸,和java.lang.Enum
的聲明類似 (但有微妙的不同):
public abstract class Enum<E extends Enum<E>> { ... }
有了上面的類型聲明,一個實際的enum
實現只是語法糖:
// 這樣的聲明 enum MyEnum {} // 實際只是下面寫法的語法糖: class MyEnum extends Enum<MyEnum> { ... }
記住上面的這點后,回到我們的2個類型聲明上。下面的代碼可以編譯通過嗎?
class Test { Type<? super C> c = new C(); Type<? super D<Byte>> d = new D<Byte>(); }
很難的問題,Ross Tate
回答過這個問題。答案實際上是不確定的:
C是Type<? super C>的子類嗎?步驟 0) C <?: Type<? super C> 步驟 1) Type<Type<? super C>> <?: Type (繼承) 步驟 2) C (檢查通配符 ? super C) 步驟 . . . (進入死循環)</pre>
然后:
D是Type<? super D<Byte>>的子類嗎?步驟 0) D<Byte> <?: Type<? super C<Byte>> 步驟 1) Type<Type<? super D<D<Byte>>>> <?: Type<? super D<Byte>> 步驟 2) D<Byte> <?: Type<? super D<D<Byte>>> 步驟 3) List<List<? super C<C>>> <?: List<? super C<C>> 步驟 4) D<D<Byte>> <?: Type<? super D<D<Byte>>> 步驟 . . . (進入永遠的展開中)</pre>
試著在你的
Eclipse
中編譯上面的代碼,會Crash!(別擔心,我已經提交了一個Bug。)我們繼續深挖下去……
在
Java
中有些類型的關系是不確定的!如果你有興趣知道更多古怪
Java
行為的細節,可以讀一下Ross Tate的論文『馴服Java
類型系統的通配符』 (由Ross Tate、Alan Leung和Sorin Lerner合著),或者也可以看看我們在子類型多態和泛型多態的關聯方面的思索。10. 類型交集(
Type intersections
)
Java
有個很古怪的特性叫類型交集。你可以聲明一個(泛型)類型,這個類型是2個類型的交集。比如:class Test<T extends Serializable & Cloneable> { }綁定到類
Test
的實例上的泛型類型參數T
必須同時實現Serializable
和Cloneable
。比如,String
不能做綁定,但Date
可以:// 編譯不通過! Test<String> s = null; // 編譯通過 Test<Date> d = null;
Java
8保留了這個特性,你可以轉型成臨時的類型交集。這有什么用? 幾乎沒有一點用,但如果你想強轉一個lambda
表達式成這樣的一個類型,就沒有其它的方法了。 假定你在方法上有了這個蛋疼的類型限制:<T extends Runnable & Serializable> 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
中包含的詭異在程度上僅僅被它解決問題的能力超過。</blockquote> 原文鏈接: Jooq 翻譯: ImportNew.com - Jerry Lee
譯文鏈接: http://www.importnew.com/13859.html