作為Java 控,我們總是對(duì)不太可能直接使用,但能使我們更了解 Java 和 Java 虛擬機(jī)(Java Virtual Machine,JVM) 的晦澀細(xì)節(jié)感興趣。這也是我將 Lukas Eder 在 jooq.org 上寫的這篇文章發(fā)布出來的原因。
你在Java發(fā)布的時(shí)候就開始使用了嗎?還記得那時(shí)它叫“Oak”,面向?qū)ο笠?(Object Oriented, OO )還是個(gè)熱門話題,C++ 程序員們覺得 Java 完全沒機(jī)會(huì)成功,Applet的出現(xiàn)也是一件新鮮大事?
我打賭下文中至少一半的內(nèi)容你都不知道。讓我們來看看這些令人驚喜的 Java 細(xì)節(jié)吧。
1. 受檢異常(checked exception)這件事是不存在的
是這樣的,JVM 完全不知道這件事,都是Java語言做的[只有Java語言這么干]。
現(xiàn)在,異常檢查被公認(rèn)為是個(gè)錯(cuò)誤,正如 Brue Eckel 在布拉格的 GeeCON 大會(huì)上的閉幕詞中所說, Java 后的其他語言都不再使用異常檢查了,就連 Java 8 都不愿在新的 Stream API 中使用它了(當(dāng)你在 lambda 表達(dá)式中使用 IO 或者 JDBC 時(shí),是很痛苦的)。
你想要證明 JVM 不知道異常檢查這件事嗎?嘗試以下代碼:
public class Test {
// No throws clause here
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;
}
}
這個(gè)不僅會(huì)編譯,還會(huì)拋出 SQLException ,你甚至不需要 Lombok 的 @SneakyThrows 標(biāo)簽。
更多詳情請(qǐng)參考這篇文章,
https://blog.jooq.org/2012/09/14/throw-checked-exceptions-like-runtime-exceptions-in-java/
或者 Stack Overflow 上的這篇文章。
http://stackoverflow.com/q/12580598/521799
2. 可以使用不同的返回值類型來重載方法
以下代碼是編譯不過的,對(duì)吧?
class Test {
Object x() { return "abc"; }
String x() { return "123"; }
}
是的,Java 不允許在一個(gè)類中通過不同的返回值類型和異常語句來重載方法。
不過稍等,Java 文檔中關(guān)于 Class.getMethod(String, Class…) 這樣寫道:
請(qǐng)注意,在一個(gè)類中會(huì)有多個(gè)匹配的方法,因?yàn)殡m然 Java 語法規(guī)則禁止一個(gè)類中存在多個(gè)方法函數(shù)簽名相同僅僅返回類型不同,但 JVM 允許。這樣提高了 JVM 的靈活性以實(shí)現(xiàn)各種語言特性。例如,可以用橋接方法(bridge method)來實(shí)現(xiàn)方法的協(xié)變返回類型,橋接方法和被重載的方法可以有相同的函數(shù)簽名和不同的返回值類型。
喔,這是合理的。事實(shí)上,以下代碼就是這樣執(zhí)行的,
abstract class Parent {
abstract T x();
}
class Child extends Parent {
@Override
String x() { return "abc";}
}
Child 類編譯后的字節(jié)碼是這樣的:
// 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 在字節(jié)碼中就是 Object,這個(gè)很好理解。
合成橋接方法是編譯器自動(dòng)生成的,因?yàn)?Parent.x() 簽名的返回值類型被認(rèn)為是 Object。如果沒有這樣的橋接方法是無法在兼容二進(jìn)制的前提下支持泛型的。因此,修改 JVM 是實(shí)現(xiàn)這個(gè)特性最簡(jiǎn)單的方法了(同時(shí)實(shí)現(xiàn)了協(xié)變式覆蓋)。很聰明吧。
你明白語言的內(nèi)部特性了嗎?這里有更多細(xì)節(jié)。
http://stackoverflow.com/q/442026/521799
3. 這些都是二維數(shù)組
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[][] = {{}};
}
你認(rèn)為很瘋狂是不是?如果使用 JSR-308 / Java 8 類型注解的話,語句的數(shù)量會(huì)爆炸性增長(zhǎng)的!
@Target(ElementType.TYPE_USE)
@interface Crazyy {}
class Test {
@Crazyy int[][] ?a1 = {{}};
int @Crazyy [][] a2 = {{}};
int[] @Crazyy [] a3 = {{}};
@Crazyy int[] b1[] ?= {{}};
int @Crazyy [] b2[] = {{}};
int[] b3 @Crazyy [] = {{}};
@Crazyy int c1[][] ?= {{}};
int c2 @Crazyy [][] = {{}};
int c3[] @Crazyy [] = {{}};
}
類型注解,它的詭異性只是被他強(qiáng)大的功能掩蓋了。
換句話說:
當(dāng)我在4周假期之前的最后一次代碼提交中這么做的話
為以上所有內(nèi)容找到相應(yīng)的實(shí)際用例的任務(wù)就交給你啦。
4. 你不懂條件表達(dá)式
你以為你已經(jīng)很了解條件表達(dá)式了嗎?我告訴你,不是的。大多數(shù)人會(huì)認(rèn)為以下的兩個(gè)代碼片段是等效的:
Object o1 = true ? new Integer(1) : new Double(2.0);
與下邊的等效嗎?
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
答案是并非如此,我們做個(gè)小測(cè)試。
System.out.println(o1);
System.out.println(o2);
程序的輸出是:
1.0
1
是的,在確有必要的情況下,條件表達(dá)式會(huì)升級(jí)數(shù)字類型。你希望這個(gè)程序拋出一個(gè)空指針異常嗎?
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);
更多細(xì)節(jié)請(qǐng)看這里。
https://blog.jooq.org/2013/10/08/java-auto-unboxing-gotcha-beware/
5. 你也不懂復(fù)合賦值運(yùn)算符
很詭異嗎?讓我們來看以下兩段代碼:
i += j;
i = i + j;
直覺上,他們是等價(jià)的吧?事實(shí)上不是,Java 語言規(guī)范(Java Language Standard,JLS)中這樣寫道:
符合賦值表達(dá)式 E1 op= E2 與 E1 = (T)((E1) op (E2)) 是等價(jià)的,這里 T 是 E1 的類型,期望 E1 只被求值一次。
很美吧,我想引用 Peter Lawrey 在Stack Overflow 上回復(fù),
http://stackoverflow.com/a/8710747/521799
這種類型轉(zhuǎn)換很好的一個(gè)例子是使用 *= or /=
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'
這個(gè)很有用吧?我會(huì)將它們應(yīng)用到我的程序里。原因你懂的。
6. 隨機(jī)數(shù)
這更像是一道題,先別看結(jié)果。看你自己能否找到答案。當(dāng)我運(yùn)行以下程序時(shí),
for (int i = 0; i < 10; i++) {
System.out.println((Integer) i);
}
有時(shí),我會(huì)得到以下輸出:
92
221
45
48
236
183
39
193
33
84
這是怎么回事?
答案已經(jīng)在前面劇透了……
答案在這里,需要通過反射來重載 JDK 中的 Integer 緩存,然后使用自動(dòng)裝箱(auto-boxing)和自動(dòng)拆箱(auto-unboxing)。千萬不要這么做,我們假設(shè)如果再做一次。
https://blog.jooq.org/2013/10/17/add-some-entropy-to-your-jvm/
我在4周假期之前的最后一次代碼提交中這么做了。
7. GOTO
這是我喜歡的一個(gè)。Java 有 GOTO 語句!輸入以下:
int goto = 1;
結(jié)果將會(huì)是:
Test.java:44: error: expected
int goto = 1;
這是因?yàn)?goto 是一個(gè)保留的關(guān)鍵字,以防萬一……
但這不是最激動(dòng)人心的部分。最給力的是你可以通過 break、continue 以及標(biāo)簽代碼塊來實(shí)現(xiàn) goto。
向前跳轉(zhuǎn)
label: {
// do stuff
if (check) break label;
// do more stuff
}
字節(jié)碼:
2 ?iload_1 [check]
3 ?ifeq 6 ? ? ? ? ?// Jumping forward
6 ?..
向后跳轉(zhuǎn)
label: do {
// do stuff
if (check) continue label;
// do more stuff
break label;
} while(true);
字節(jié)碼:
2 ?iload_1 [check]
3 ?ifeq 9
6 ?goto 2 ? ? ? ? ?// Jumping backward
9 ?..
8. Java 支持類型別名(type aliases)
在其它語言中(例如:Ceylon),定義類型別名是很容易的。
interface People => Set;
People 類型通過這個(gè)方法就可以與 Set 互換使用了:
People? ? ? ?p1 = null;
Set? p2 = p1;
People? ? ? ?p3 = p2;
在 Java 中,頂層代碼里是不能定義類型別名的,但是我們可以在類和方法的作用域內(nèi)這么做。假設(shè)我們不喜歡 Integer,[、]Long 這些名字,想要短一點(diǎn)的如 I 和 L,這是小菜一碟:
class Test {
void x(I i, L l) {
System.out.println(
i.intValue() + ", " +
l.longValue()
);
}
}
以上代碼中,Integer 在 Test 類中用別名 I 替換, Long 在 x() 方法中用別名 L 替換。我們可以這樣調(diào)用以上方法:
new Test().x(1, 2L);
這個(gè)技術(shù)別太當(dāng)真。在上邊的例子里,Integer 和 Long 都是 final 類型, 也就是說 I 和 L 效果上是類型別名(大多數(shù)情況下,賦值兼容是單向的)。如果我們用非 final 的類型(例如 Object),就需要使用原來的泛型了。
以上是一些雕蟲小技,下面才是真正有用的!
9. 一些類型之間的關(guān)系是不確定的!
這個(gè)會(huì)很有趣的,所以來一杯咖啡然后集中注意力。假設(shè)以下兩種類型:
// A helper type. You could also just use List
interface Type {}
class C implements Type> {}
class D
implements Type>>> {}
類型 C 和 D 到底是什么意思呢?
他們包含了遞歸,很像 java.lang.Enum ,但又稍有不同。考慮以下代碼:
public abstract class Enum> { ... }
以上定義中, enum 的實(shí)現(xiàn)是一個(gè)純粹的語法糖。
// This
enum MyEnum {}
// Is really just sugar for this
class MyEnum extends Enum { ... }
記住這個(gè),讓我們?cè)倩氐絼偛拍莾蓚€(gè)類型。下邊的代碼可以通過編譯嗎?
class Test {
Type< ? super C> c = new C();
Type< ? super D> d = new D();
}
這是個(gè)很難的問題,Ross Tate 已經(jīng)回答了。答案是不確定的:
C 是 的子類型嗎?
Step 0) C
Step 1) Type>
Step 2) C ?(checking wildcard ? super C)
Step . . . (cycle forever)
然后
D 是 > 的子類型嗎?
Step 0) D >
Step 1) Type>>> >
Step 2) D >>
Step 3) Type>> >
Step 4) D> >>
Step . . . (expand forever)
嘗試在 Eclipse 中編譯以上代碼,Eclipse 會(huì)掛掉的!(不要擔(dān)心,我已經(jīng)提過 bug 了)
理解下這個(gè)…
Java 中的一些類型的關(guān)系是不確定的!
如果你想了解更多關(guān)于 Java 的這個(gè)特性,請(qǐng)閱讀 Ross Tate 與 Alan Leung 和 Sorin Lerner 共同編著的論文 “Taming Wildcards in Java’s Type System”或者我們自己總結(jié)的correlating subtype polymorphism with generic polymorphism。
《 Taming Wildcards in Java’s Type System 》
http://www.cs.cornell.edu/~ross/publications/tamewild/tamewild-tate-pldi11.pdf
《 correlating subtype polymorphism with generic polymorphism 》
https://blog.jooq.org/2013/06/28/the-dangers-of-correlating-subtype-polymorphism-with-generic-polymorphism/
10. 類型交集(Type intersections)
Java 有個(gè)特性叫做類型交集。你可以聲明一個(gè)泛型,這個(gè)泛型是兩個(gè)類型的交集,例如:
class Test {
}
綁定到 Test 類的實(shí)例的泛型類型參數(shù) T 需要同時(shí)實(shí)現(xiàn) Serializable 和 Cloneable。例如,String 是不能綁定的,但 Date 可以:
// Doesn't compile
Test s = null;
// Compiles
Test d = null;
Java 8 中保留了這個(gè)功能,你可以將類型轉(zhuǎn)換為臨時(shí)的類型交集。這有用嗎?幾乎沒用,但如果你想要將lambda表達(dá)式強(qiáng)制轉(zhuǎn)換為這個(gè)類型,除此就別無他法了。我們假設(shè)你的方法有這個(gè)瘋狂的類型限制:
void execute(T t) {}
你想要同時(shí)支持 Runnable 和 Serializable,是為了以防萬一要在網(wǎng)絡(luò)的另一處執(zhí)行它。Lambda 和序列化都有些古怪:
Lambda 表達(dá)式可以被序列化:
如果一個(gè) lambda 表達(dá)式的返回值和輸入?yún)?shù)可以被序列化,則這個(gè)表達(dá)式是可以被序列化的。
但即使這是真的,它也不會(huì)自動(dòng)繼承 Serializable 接口。你需要轉(zhuǎn)換才能成為那個(gè)類型。但如果你只是轉(zhuǎn)換為 Serializable…
execute((Serializable) (() -> {}));
lambda 就不支持 Runnable 了。
所以,
把它轉(zhuǎn)換為兩個(gè)類型:
execute((Runnable & Serializable) (() -> {}));
結(jié)論
我經(jīng)常只這么說 SQL,但現(xiàn)在要用下邊的話來總結(jié)這篇文章了:
Java 語言的詭異性只是被它解決問題的能力掩蓋了。