歡迎光臨
每天分享高質量文章

關於 Java 你可能不知道的 10 件事

(點選上方公眾號,可快速關註)


來源:ImportNew – Jerry Lee

呃,你是不是寫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用起來還是有些痛苦的。)

http://www.geecon.cz/speakers/?id=2

想證明JVM不理會受檢異常?試試下麵的這段程式碼:

public class Test {

 

    // 方法沒有宣告throws

    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;

    }

}

不僅可以編譯透過,並且也丟擲了SQLException,你甚至都不需要用上Lombok的@SneakyThrows。

更多細節,可以再看看這篇文章,或Stack Overflow上的這個問題

這篇文章

http://blog.jooq.org/2012/09/14/throw-checked-exceptions-like-runtime-exceptions-in-java/


問題

http://stackoverflow.com/q/12580598/521799

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 {

    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 [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[][] = {{}};

}

是不是覺得這個很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);

關於這一條的更多的資訊可以在這裡找到。

http://blog.jooq.org/2013/10/08/java-auto-unboxing-gotcha-beware/

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上的回答

http://stackoverflow.com/a/8710747/521799

使用*=或/=作為例子可以方便說明其中的轉型問題:

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: 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;

這樣定義的People可以和Set互換地使用:

People?      p1 = null;

Set? p2 = p1;

People?      p3 = p2;

在Java中不能在頂級(top level)定義型別別名。但可以在類級別、或方法級別定義。 如果對Integer、Long這樣名字不滿意,想更短的名字:I和L。很簡單:

class Test {

    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 {}

 

class C implements Type> {}

class D

implements Type>>> {}

型別C和D是啥意思呢?

這2個型別宣告中包含了遞迴,和java.lang.Enum的宣告類似 (但有微妙的不同):

public abstract class Enum> { … }

有了上面的型別宣告,一個實際的enum實現只是語法糖:

// 這樣的宣告

enum MyEnum {}

 

// 實際只是下麵寫法的語法糖:

class MyEnum extends Enum { … }

記住上面的這點後,回到我們的2個型別宣告上。下麵的程式碼可以編譯透過嗎?

class Test {

    Type super C> c = new C();

    Type super D> d = new D();

}

很難的問題,Ross Tate回答過這個問題。答案實際上是不確定的:

C是Type super C>的子類嗎?

 

步驟 0) C

步驟 1) Type>

步驟 2) C (檢查萬用字元 ? super C)

步驟 . . . (進入死迴圈)

然後:

D是Type super D>的子類嗎?

 

步驟 0) D >

步驟 1) Type>>> >

步驟 2) D >>

步驟 3) List>> >

步驟 4) D> >>

步驟 . . . (進入永遠的展開中)

試著在你的Eclipse中編譯上面的程式碼,會Crash!(別擔心,我已經提交了一個Bug。)

我們繼續深挖下去……

在Java中有些型別的關係是不確定的!

如果你有興趣知道更多古怪Java行為的細節,可以讀一下Ross Tate的論文『馴服Java型別系統的萬用字元』 (由Ross Tate、Alan Leung和Sorin Lerner合著),或者也可以看看我們在子型別多型和泛型多型的關聯方面的思索

『馴服Java型別系統的萬用字元』

http://www.cs.cornell.edu/~ross/publications/tamewild/tamewild-tate-pldi11.pdf


子型別多型和泛型多型的關聯方面的思索

http://blog.jooq.org/2013/06/28/the-dangers-of-correlating-subtype-polymorphism-with-generic-polymorphism/

10. 型別交集(Type intersections)

Java有個很古怪的特性叫型別交集。你可以宣告一個(泛型)型別,這個型別是2個型別的交集。比如:

class Test {

}

系結到類Test的實體上的泛型型別引數T必須同時實現Serializable和Cloneable。比如,String不能做系結,但Date可以:

// 編譯不透過!

Test s = null;

 

// 編譯透過

Test d = null;

Java 8保留了這個特性,你可以轉型成臨時的型別交集。這有什麼用? 幾乎沒有一點用,但如果你想強轉一個lambda運算式成這樣的一個型別,就沒有其它的方法了。 假定你在方法上有了這個蛋疼的型別限制:

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中包含的詭異在程度上僅僅被它解決問題的能力超過。

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂