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
不理會受檢異常?試試下面的這段代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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. 可以有隻是返回類型不同的重載方法
下面的代碼不能編譯,是吧?
1
2
3
4
|
class
Test { Object
x() { return
"abc" ;
} String
x() { return
"123" ;
} } |
是的!Java
語言不允許一個類裏有2個方法是『重載一致』的,而不會關心這2個方法的throws
子句或返回類型實際是不同的。
但是等一下!來看看Class.getMethod(String,
Class...)
方法的Javadoc
:
注意,可能在一個類中會有多個匹配的方法,因爲儘管
Java
語言禁止在一個類中多個方法簽名相同只是返回類型不同,但是JVM
並不禁止。 這讓JVM
可以更靈活地去實現各種語言特性。比如,可以用橋方法(bridge method)來實現方法的協變返回類型;橋方法和被重載的方法可以有相同的方法簽名,但返回類型不同。
嗯,這個說的通。實際上,當寫了下面的代碼時,就發生了這樣的情況:
1
2
3
4
5
6
7
8
|
abstract
class
Parent<T> { abstract
T x(); } class
Child extends
Parent<String> { @Override String
x() { return
"abc" ;
} } |
查看一下Child
類所生成的字節碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//
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. 所有這些寫法都是二維數組!
1
2
3
4
5
|
class
Test { int [][]
a() { return
new
int [ 0 ][];
} int []
b() [] { return
new
int [ 0 ][];
} int
c() [][] { return
new
int [ 0 ][];
} } |
是的,這是真的。儘管你的人肉解析器不能馬上理解上面這些方法的返回類型,但都是一樣的!下面的代碼也類似:
1
2
3
4
5
|
class
Test { int [][]
a = {{}}; int []
b[] = {{}}; int
c[][] = {{}}; } |
是不是覺得這個很2B?想象一下在上面的代碼中使用JSR-308
/Java
8的類型註解。
語法糖的數目要爆炸了吧!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@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個代碼段是等價的:
1
|
Object
o1 = true
? new
Integer( 1 )
: new
Double( 2.0 ); |
等同於:
1
2
3
4
5
6
|
Object
o2; if
( true ) o2
= new
Integer( 1 ); else o2
= new
Double( 2.0 ); |
讓你失望了。來做個簡單的測試吧:
1
2
|
System.out.println(o1); System.out.println(o2); |
打印結果是:
1
2
|
1.0 1 |
哦!如果『需要』,條件運算符會做數值類型的類型提升,這個『需要』有非常非常非常強的引號。因爲,你覺得下面的程序會拋出NullPointerException
嗎?
1
2
3
4
5
6
|
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行代碼:
1
2
|
i
+= j; i
= i + j; |
直覺上認爲,2行代碼是等價的,對吧?但結果即不是!JLS
(Java
語言規範)指出:
複合賦值運算符表達式
E1 op= E2
等價於E1 = (T)((E1) op (E2))
其中T
是E1
的類型,但E1
只會被求值一次。
這個做法太漂亮了,請允許我引用Peter
Lawrey在Stack Overflow
上的回答:
使用*=
或/=
作爲例子可以方便說明其中的轉型問題:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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
這條其實是一個迷題,先不要看解答。看看你能不能自己找出解法。運行下面的代碼:
1
2
3
|
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的!打上這行代碼:
1
|
int
goto
= 1 ; |
結果是:
1
2
3
|
Test.java: 44 :
error: <identifier> expected int
goto
= 1 ; ^ |
這是因爲goto
是個還未使用的關鍵字,保留了爲以後可以用……
但這不是我要說的讓你興奮的內容。讓你興奮的是,你是可以用break
、continue
和有標籤的代碼塊來實現goto
的:
向前跳:
1
2
3
4
5
|
label:
{ //
do stuff if
(check) break
label; //
do more stuff } |
對應的字節碼是:
1
2
3
|
2
iload_1 [check] 3
ifeq 6
// 向前跳 6
.. |
向後跳:
1
2
3
4
5
6
|
label: do
{ //
do stuff if
(check) continue
label; //
do more stuff break
label; } while ( true ); |
對應的字節碼是:
1
2
3
4
|
2
iload_1 [check] 3
ifeq 9 6
goto
2
// 向後跳 9
.. |
8. Java
是有類型別名的
在別的語言中(比如,Ceylon
),
可以方便地定義類型別名:
1
|
interface
People => Set<Person>; |
這樣定義的People
可以和Set<Person>
互換地使用:
1
2
3
|
People?
p1 = null ; Set<Person>?
p2 = p1; People?
p3 = p2; |
在Java
中不能在頂級(top
level
)定義類型別名。但可以在類級別、或方法級別定義。 如果對Integer
、Long
這樣名字不滿意,想更短的名字:I
和L
。很簡單:
1
2
3
4
5
6
7
8
|
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
的『別名』。可以這樣來調用這個方法:
1
|
new
Test().x( 1 ,
2L); |
當然這個用法不嚴謹。在例子中,Integer
、Long
都是final
類型,結果I
和L
效果上是個別名
(大部分情況下是。賦值兼容性只是單向的)。如果用非final
類型(比如,Object
),還是要使用原來的泛型參數類型。
玩夠了這些噁心的小把戲。現在要上乾貨了!
9. 有些類型的關係是不確定的
好,這條會很稀奇古怪,你先來杯咖啡,再集中精神來看。看看下面的2個類型:
1
2
3
4
5
|
//
一個輔助類。也可以直接使用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
的聲明類似
(但有微妙的不同):
1
|
public
abstract
class
Enum<E extends
Enum<E>> { ... } |
有了上面的類型聲明,一個實際的enum
實現只是語法糖:
1
2
3
4
5
|
//
這樣的聲明 enum
MyEnum {} //
實際只是下面寫法的語法糖: class
MyEnum extends
Enum<MyEnum> { ... } |
記住上面的這點後,回到我們的2個類型聲明上。下面的代碼可以編譯通過嗎?
1
2
3
4
|
class
Test { Type<? super
C> c = new
C(); Type<? super
D<Byte>> d = new
D<Byte>(); } |
很難的問題,Ross
Tate
回答過這個問題。答案實際上是不確定的:
1
2
3
4
5
6
|
C是Type<? super
C>的子類嗎? 步驟 0 )
C <?: Type<? super
C> 步驟 1 )
Type<Type<? super
C>> <?: Type (繼承) 步驟 2 )
C (檢查通配符 ? super
C) 步驟
. . . (進入死循環) |
然後:
1
2
3
4
5
6
7
8
|
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>>> 步驟
. . . (進入永遠的展開中) |
試着在你的Eclipse
中編譯上面的代碼,會Crash!(別擔心,我已經提交了一個Bug。)
我們繼續深挖下去……
在
Java
中有些類型的關係是不確定的!
如果你有興趣知道更多古怪Java
行爲的細節,可以讀一下Ross Tate的論文『馴服Java
類型系統的通配符』 (由Ross
Tate、Alan Leung和Sorin Lerner合著),或者也可以看看我們在子類型多態和泛型多態的關聯方面的思索。
10. 類型交集(Type intersections
)
Java
有個很古怪的特性叫類型交集。你可以聲明一個(泛型)類型,這個類型是2個類型的交集。比如:
1
2
|
class
Test<T extends
Serializable & Cloneable> { } |
綁定到類Test
的實例上的泛型類型參數T
必須同時實現Serializable
和Cloneable
。比如,String
不能做綁定,但Date
可以:
1
2
3
4
5
|
//
編譯不通過! Test<String>
s = null ; //
編譯通過 Test<Date>
d = null ; |
Java
8保留了這個特性,你可以轉型成臨時的類型交集。這有什麼用? 幾乎沒有一點用,但如果你想強轉一個lambda
表達式成這樣的一個類型,就沒有其它的方法了。
假定你在方法上有了這個蛋疼的類型限制:
1
|
<T extends
Runnable & Serializable> void
execute(T t) {} |
你想一個Runnable
同時也是個Serializable
,這樣你可能在另外的地方執行它並通過網絡發送它。lambda
和序列化都有點古怪。
lambda
是可以序列化的:
如果
lambda
表達式的目標類型和它捕獲的參數(captured arguments
)是可以序列化的,則這個lambda
表達式是可序列化的。
但即使滿足這個條件,lambda
表達式並沒有自動實現Serializable
這個標記接口(marker
interface
)。 爲了強制成爲這個類型,就必須使用轉型。但如果只轉型成Serializable
…
1
|
execute((Serializable)
(() -> {})); |
… 則這個lambda
表達式不再是一個Runnable
。
呃……
So……
同時轉型成2個類型:
1
|
execute((Runnable
& Serializable) (() -> {})); |
結論
一般我只對SQL
會說這樣的話,但是時候用下面的話來結束這篇文章了:
原文鏈接: Jooq 翻譯: ImportNew.com - Jerry Lee
Java
中包含的詭異在程度上僅僅被它解決問題的能力超過。
譯文鏈接: http://www.importnew.com/13859.html