關於 Java 你不知道的 10 件事
作為 Java 書呆子,比起實用技能,我們會對介紹 Java 和 JVM 的概念細節更感興趣。因此我想推薦 Lukas Eder 在 jooq.org 發表的原創作品給大家。
你是從很早開始就一直使用 Java 嗎?那你還記得它的過去嗎?那時,Java 還叫 Oak,OO 還是一個熱門話題,C++ 的 folk 者認為 Java 是不可能火起來,Java 開發的小應用程式 Applets 還受到關注。
我敢打賭,下面我要介紹的這些事,有一半你都不知道。下面讓我們來深入探索 Java 的神祕之處。
1. 沒有檢查異常這種事情
沒錯!JVM 不會知道這些事情,只有 Java 語句知道。
如今大家都認為檢查異常是個錯誤。正如 Bruce 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 就能辦到。
這篇文章可以看到更詳細的相關內容,或者在 Stack Overflow 上看。
2. 你可以定義僅在返回值有差異的過載函式
這樣的程式碼無法編譯,對不?
class Test { Object x() { return "abc"; } String x() { return "123"; } }
對。 Java 語言不允許兩個方法在同一個類中“等效過載”,而忽略其諸如throws自居或返回型別等的潛在的差異。
檢視 Class.getMethod(String, Class…) 的 Javadoc。 其中說明如下:
請注意,類中可能有多個匹配方法,因為 Java 語言禁止在一個類宣告具有相同簽名但返回型別不同的多個方法,但 Java 虛擬機器並不是如此。虛擬機器中增加的靈活性可以用於實現各種語言特徵。例如,可以用橋接方法實現協變參返回; 橋接方法和被重寫的方法將具有相同的簽名但擁有不同的返回型別。
哇哦,有道理。實際上下面的程式碼暗藏著很多事情:
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><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[][] = {{}}; }
你認為這很瘋狂?想象在上面使用 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. 條件表示式的特殊情況
可能大多數人會認為:
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. 你還沒搞懂複合賦值運算子
很奇怪嗎?來看看下面這兩行程式碼:
i += j; i = i + j;
直觀看來它們等價,是嗎?但可其實它們並不等價!JLS 解釋如下:
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. 隨機整數
現在有一個更難的謎題。不要去看答案,看看你能不能自己找到答案。如果執行下面的程式:
for (int i = 0; i < 10; i++) { System.out.println((Integer) i); }
… “有時候”,我會得到下面的輸出:
92 221 45 48 236 183 39 193 33 84
這怎麼可能??
. spoiler… 繼續解答…
好了,答案在這裡 (https://blog.jooq.org/2013/10/17/add-some-entropy-to-your-jvm/),這必須通過反射重寫 JDK 的 Integer 快取,然後使用自動裝箱和拆箱。不要在家幹這種事情!或者,我們應該換種方式進行此類操作。
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 有型別別名
其它語言 (比如 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() ); } }
在上面的程式中,Test 類作用域內 Integer 被賦予 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 是 Type<? 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 是 Type<? 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 中試著編譯一下,它會崩潰! (不用擔心,我提交了 BUG 報告)
讓這個事情沉下去…
Java 中某些型別的關係是不明確的!
如果你對 Java 這個用法感到奇怪之餘也感興趣,就去看看 Ross Tate 寫的 “在 Java 的型別系統中使用萬用字元” (與 Alan Leung 和 Sorin Lerner 合著),我們也在討論泛型多型中的相關子類多型性。
10. 型別交集
Java 有一個非常奇怪的特性叫型別交集。你可以申明某個(泛型)型別,而它實際上是兩個型別的交集,比如:
class Test<T extends Serializable & Cloneable> { }
繫結到 Test 型別例項的泛型型別引數 T 必須實現 Serializable 和 Cloneable。比如,String 就不符合要求,但 Dete 滿足:
// Doesn't compile Test<String> s = null; // Compiles Test<Date> d = null;
這個特性已經在 Java 8 中使用。這很有用嗎?幾乎沒用,但是如果你希望某個 Lambda 表示式是這種型別,還真沒別的辦法。假設你的方法有這種瘋狂的型別約束:
<T extends Runnable & Serializable> void execute(T t) {}
你想通過執行它得到一個可以序列化 (Serializable) 的 Runnable 物件。Lambda 和序列化也有點奇怪。
如果 Lambda 的目標型別和引數型別都可以序列化,那麼你可以序列化這個 Lambda
但是即使是這樣,他們都不能自動實現 Serializable 標記介面。你必須強制轉換型別。但是當你只扔給 Serializable 時…
execute((Serializable) (() -> {}));
… 那麼 lambda 將不再是 Runnable 的。
因此要把它轉換為兩種型別:
execute((Runnable & Serializable) (() -> {}));
結論
一句話總結這篇文章就是:
Java 恰好是一種看起來神祕的語言,其實不然。
相關文章
- 你不知道Java的10件事Java
- 關於 Java 物件序列化您不知道的 5 件事Java物件
- 關於JavaScript陣列,你所不知道的3件事JavaScript陣列
- 關於 Java Collections API 您不知道的 5 件事,第 1 部分JavaAPI
- 關於軟體開發,你老闆不知道的7件事
- 關於MongoDB,你可能不知道的十件事MongoDB
- 關於 Git 和 Github 你不知道的十件事Github
- 關於Java序列化你不知道的事Java
- 關於 NoSQL 資料庫你應該瞭解的 10 件事SQL資料庫
- 關於MongoDB你需要知道的幾件事MongoDB
- 關於 Web Workers 你需要了解的 7 件事Web
- 關於iPhone SE你應該知道的20件事iPhone
- 關於面試你不知道的幾點面試
- 關於NoSQL,你必須知道的九件事SQL
- 關於Java你不知道的那些事之Java8新特性[HashMap優化]JavaHashMap優化
- 39個你不知道的關於亞馬遜的真相亞馬遜
- 關於Go,你可能不注意的7件事Go
- 關於 PHP 7 你必須知道的五件事PHP
- 關於HTTP/3背後你所不知道的HTTP
- 關於Java垃圾回收被誤解的7件事Java
- 深入洞見:你所不知道的Java 物件序列化的5件事兒Java物件
- 你應該知道的10件關於Java 6的事情Java
- 關於Linux中“!”你不知道的驚歎用法Linux
- 【機器學習】關於機器學習那些你不知道的“民間智慧”機器學習
- 關於 Git 你所不知道的一些事Git
- 關於程式設計,大學沒有傳授的10件事程式設計
- 告訴你關於結對程式設計的7件事程式設計
- 關於iPad Pro,蘋果沒告訴你的8件事iPad蘋果
- docker – 你應該知道的10件事Docker
- 你應該知道的10件關於Java 6的事情(轉)Java
- 關於網校系統原始碼,你不知道的事情原始碼
- 關於Handler同步屏障你可能不知道的問題
- 關於檔案傳輸協議,你不知道的事協議
- 關於 Google 你可能還不知道的26個事實Go
- 關於執行緒池,那些你還不知道的事執行緒
- 關於Docker你不知道的事——虛擬化歷史Docker
- 關於javascript你不得不知道歷史JavaScript
- 在使用Redux前你需要知道關於React的8件事ReduxReact