。(點擊上方公眾號,可快速關注)
編譯:ImportNew - 心靈是一棵開花的樹,
如有好文章投稿,請點擊 → 這裡了解詳情
作為Java 控,我們總是對不太可能直接使用,但能使我們更了解 Java 和 Java 虛擬機(Java Virtual Machine,JVM) 的晦澀細節感興趣。這也是我將 Lukas Eder 在 jooq.org 上寫的這篇文章發布出來的原因。
你在Java發布的時候就開始使用了嗎?還記得那時它叫「Oak」,面向對象也 (Object Oriented, OO )還是個熱門話題,C++ 程式設計師們覺得 Java 完全沒機會成功,Applet的出現也是一件新鮮大事?
我打賭下文中至少一半的內容你都不知道。讓我們來看看這些令人驚喜的 Java 細節吧。
1. 受檢異常(checked exception)這件事是不存在的
是這樣的,JVM 完全不知道這件事,都是Java語言做的[只有Java語言這麼幹]。
現在,異常檢查被公認為是個錯誤,正如 Brue Eckel 在布拉格的 GeeCON 大會上的閉幕詞中所說, Java 後的其他語言都不再使用異常檢查了,就連 Java 8 都不願在新的 Stream API 中使用它了(當你在 lambda 表達式中使用 IO 或者 JDBC 時,是很痛苦的)。
你想要證明 JVM 不知道異常檢查這件事嗎?嘗試以下代碼:
public class Test {
// No throws clause here
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 標籤。
更多詳情請參考這篇文章,
https://blog.jooq.org/2012/09/14/throw-checked-exceptions-like-runtime-exceptions-in-java/
或者 Stack Overflow 上的這篇文章。
http://stackoverflow.com/q/12580598/521799
2. 可以使用不同的返回值類型來重載方法
以下代碼是編譯不過的,對吧?
class Test {
Object x() { return "abc"; }
String x() { return "123"; }
}
是的,Java 不允許在一個類中通過不同的返回值類型和異常語句來重載方法。
不過稍等,Java 文檔中關於 Class.getMethod(String, Class…) 這樣寫道:
請注意,在一個類中會有多個匹配的方法,因為雖然 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 </String><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 是實現這個特性最簡單的方法了(同時實現了協變式覆蓋)。很聰明吧。
你明白語言的內部特性了嗎? 這裡有更多細節。
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[][] = {{}};
}
你認為很瘋狂是不是?如果使用 JSR-308 / Java 8 類型註解的話,語句的數量會爆炸性增長的!
@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 [] = {{}};
}
類型註解,它的詭異性只是被他強大的功能掩蓋了。
換句話說:
當我在4周假期之前的最後一次代碼提交中這麼做的話
為以上所有內容找到相應的實際用例的任務就交給你啦。
4. 你不懂條件表達式
你以為你已經很了解條件表達式了嗎?我告訴你,不是的。大多數人會認為以下的兩個代碼片段是等效的:
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
是的,在確有必要的情況下,條件表達式會升級數字類型。你希望這個程序拋出一個空指針異常嗎?
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);
更多細節請看這裡。
https://blog.jooq.org/2013/10/08/java-auto-unboxing-gotcha-beware/
5. 你也不懂複合賦值運算符
很詭異嗎?讓我們來看以下兩段代碼:
i += j;
i = i + j;
直覺上,他們是等價的吧?事實上不是,Java 語言規範(Java Language Standard,JLS)中這樣寫道:
符合賦值表達式 E1 op= E2 與 E1 = (T)((E1) op (E2)) 是等價的,這裡 T 是 E1 的類型,期望 E1 只被求值一次。
很美吧,我想引用 Peter Lawrey 在 Stack Overflow 上回復,
http://stackoverflow.com/a/8710747/521799
這種類型轉換很好的一個例子是使用 *= 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'
這個很有用吧?我會將它們應用到我的程序裡。原因你懂的。
6. 隨機數
這更像是一道題,先別看結果。看你自己能否找到答案。當我運行以下程序時,
for (int i = 0; i < 10; i++) {
System.out.println((Integer) i);
}
有時,我會得到以下輸出:
92
221
45
48
236
183
39
193
33
84
這是怎麼回事?
答案已經在前面劇透了……
答案在這裡,需要通過反射來重載 JDK 中的 Integer 緩存,然後使用自動裝箱(auto-boxing)和自動拆箱(auto-unboxing)。千萬不要這麼做,我們假設如果再做一次。
https://blog.jooq.org/2013/10/17/add-some-entropy-to-your-jvm/
我在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 // Jumping forward
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 // Jumping backward
9 ..
8. Java 支持類型別名(type aliases)
在其它語言中(例如:Ceylon),定義類型別名是很容易的。
interface People => Set<Person>;
People 類型通過這個方法就可以與 Set<Person> 互換使用了:
People? p1 = null;
Set</Person><Person>? p2 = p1;
People? p3 = p2;
在 Java 中,頂層代碼裡是不能定義類型別名的,但是我們可以在類和方法的作用域內這麼做。假設我們不喜歡 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()
);
}
}
以上代碼中,Integer 在 Test 類中用別名 I 替換, Long 在 x() 方法中用別名 L 替換。我們可以這樣調用以上方法:
new Test().x(1, 2L);
這個技術別太當真。在上邊的例子裡,Integer 和 Long 都是 final 類型, 也就是說 I 和 L 效果上是類型別名(大多數情況下,賦值兼容是單向的)。如果我們用非 final 的類型(例如 Object),就需要使用原來的泛型了。
以上是一些雕蟲小技,下面才是真正有用的!
9. 一些類型之間的關係是不確定的!
這個會很有趣的,所以來一杯咖啡然後集中注意力。假設以下兩種類型:
// A helper type. You could also just use List
interface Type<T> {}
class C implements Type<Type <? super C>> {}
class D<P> implements Type<Type <? super D<D<P>>>> {}
類型 C 和 D 到底是什麼意思呢?
他們包含了遞歸,很像 java.lang.Enum ,但又稍有不同。考慮以下代碼:
public abstract class Enum<E extends Enum<E>> { ... }
以上定義中, enum 的實現是一個純粹的語法糖。
// This
enum MyEnum {}
// Is really just sugar for this
class MyEnum extends Enum<MyEnum> { ... }
記住這個,讓我們再回到剛才那兩個類型。下邊的代碼可以通過編譯嗎?
class Test {
Type< ? super C> c = new C();
Type< ? super D<Byte>> d = new D<Byte>();
}
這是個很難的問題,Ross Tate 已經回答了。答案是不確定的:
C 是 <? super C> 的子類型嗎?
Step 0) C <?: Type<? super C>
Step 1) Type<Type<? super C>> <?: Type (inheritance)
Step 2) C (checking wildcard ? super C)
Step . . . (cycle forever)
然後
D 是 <? super D<Byte>> 的子類型嗎?
Step 0) D<Byte> <?: Type<? super C<Byte>>
Step 1) Type<Type<? super D<D<Byte>>>> <?: Type<? super D<Byte>>
Step 2) D<Byte> <?: Type<? super D<D<Byte>>>
Step 3) Type<Type<? super C<C>>> <?: Type<? super C<C>>
Step 4) D<D<Byte>> <?: Type<? super D<D<Byte>>>
Step . . . (expand forever)
嘗試在 Eclipse 中編譯以上代碼,Eclipse 會掛掉的!(不要擔心,我已經提過 bug 了)
理解下這個…
Java 中的一些類型的關係是不確定的!
如果你想了解更多關於 Java 的這個特性,請閱讀 Ross Tate 與 Alan Leung 和 Sorin Lerner 共同編著的論文 「Taming Wildcards in Java’s Type System」或者我們自己總結的 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 有個特性叫做類型交集。你可以聲明一個泛型,這個泛型是兩個類型的交集,例如:
class Test<T extends Serializable & Cloneable> {
}
綁定到 Test 類的實例的泛型類型參數 T 需要同時實現 Serializable 和 Cloneable。例如,String 是不能綁定的,但 Date 可以:
// Doesn't compile
Test<String> s = null;
// Compiles
Test<Date> d = null;
Java 8 中保留了這個功能,你可以將類型轉換為臨時的類型交集。這有用嗎?幾乎沒用,但如果你想要將lambda表達式強制轉換為這個類型,除此就別無他法了。我們假設你的方法有這個瘋狂的類型限制:
<T extends Runnable & Serializable> void execute(T t) {}
你想要同時支持 Runnable 和 Serializable,是為了以防萬一要在網絡的另一處執行它。Lambda 和序列化都有些古怪:
Lambda 表達式可以被序列化:
如果一個 lambda 表達式的返回值和輸入參數可以被序列化,則這個表達式是可以被序列化的。
但即使這是真的,它也不會自動繼承 Serializable 接口。你需要轉換才能成為那個類型。但如果你只是轉換為 Serializable…
execute((Serializable) (() -> {}));
lambda 就不支持 Runnable 了。
所以,
把它轉換為兩個類型:
execute((Runnable & Serializable) (() -> {}));
結論
我經常只這麼說 SQL,但現在要用下邊的話來總結這篇文章了:
Java 語言的詭異性只是被它解決問題的能力掩蓋了。
看完本文有收穫?請轉發分享給更多人
關注「ImportNew」,提升Java技能