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

呃,你是不是寫 Java已經有些年頭了?還依稀記得這些吧: 那些年,它還叫做 Oak;那些年, OO還是個熱門話題;那些年, C++同學們覺得 Java是沒有出路的;那些年, Applet還風頭正勁……

但我打賭下面的這些事中至少有一半你還不知道。這周我們來聊聊這些會讓你有些驚訝的Java內部的那些事兒吧。

1. 其實沒有受檢異常(checked exception

是的!JVM纔不知道這類事情,只有Java語言纔會知道。

今天,大家都贊同受檢異常是個設計失誤,一個Java語言中的設計失誤。正如 Bruce Eckel 在布拉格的GeeCON會議上演示的總結中說的, Java之後的其它語言都沒有再涉及受檢異常了,甚至Java 8的新式流APIStreams API)都不再擁抱受檢異常 (lambda的方式使用IOJDBC,這個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週休假前的最後一個提交裏,我寫了這樣的代碼,然後。。。

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

譯註:然後,親愛的同事你,就有得火救啦,哼,哼哼,哦哈哈哈哈~】

請找出上面用法合適的使用場景,還是留給你作爲一個練習吧。

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行代碼是等價的,對吧?但結果即不是!JLSJava語言規範)指出:

複合賦值運算符表達式 E1 op= E2 等價於 E1 = (T)((E1) op (E2)) 其中TE1的類型,但E1只會被求值一次。

這個做法太漂亮了,請允許我引用Peter LawreyStack 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/), 和用反射覆蓋JDKInteger緩存,然後使用自動打包解包(auto-boxing/auto-unboxing)有關。 同學們請勿模仿!或換句話說,想想會有這樣的狀況,再說一次:

在我4週休假前的最後一個提交裏,我寫了這樣的代碼,然後。。。

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

譯註:然後,親愛的同事你,就有得火救啦,哼,哼哼,哦哈哈哈哈~】

7. GOTO

這條是我的最愛。Java是有GOTO的!打上這行代碼:

?
1
int goto 1;

結果是:

?
1
2
3
Test.java:44: error: <identifier> expected
    int goto 1;
        ^

這是因爲goto是個還未使用的關鍵字,保留了爲以後可以用……

但這不是我要說的讓你興奮的內容。讓你興奮的是,你是可以用breakcontinue和有標籤的代碼塊來實現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)定義類型別名。但可以在類級別、或方法級別定義。 如果對IntegerLong這樣名字不滿意,想更短的名字:IL。很簡單:

?
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類級別中IInteger的『別名』,在x方法級別,LLong的『別名』。可以這樣來調用這個方法:

?
1
new Test().x(1, 2L);

當然這個用法不嚴謹。在例子中,IntegerLong都是final類型,結果IL 效果上是個別名 (大部分情況下是。賦值兼容性只是單向的)。如果用非final類型(比如,Object),還是要使用原來的泛型參數類型。

玩夠了這些噁心的小把戲。現在要上乾貨了!

9. 有些類型的關係是不確定的

好,這條會很稀奇古怪,你先來杯咖啡,再集中精神來看。看看下面的2個類型:

?
1
2
3
4
5
// 一個輔助類。也可以直接使用List
interface Type<T> {}
 
class implements Type<Type<? super C>> {}
class D<P> implements Type<Type<? super D<D<P>>>> {}

類型CD是啥意思呢?

這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 LeungSorin Lerner合著),或者也可以看看我們在子類型多態和泛型多態的關聯方面的思索。

10. 類型交集(Type intersections

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

?
1
2
class Test<T extends Serializable & Cloneable> {
}

綁定到類Test的實例上的泛型類型參數T必須同時實現SerializableCloneable。比如,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會說這樣的話,但是時候用下面的話來結束這篇文章了:

Java中包含的詭異在程度上僅僅被它解決問題的能力超過。

原文鏈接: Jooq 翻譯: ImportNew.com Jerry Lee
譯文鏈接: http://www.importnew.com/13859.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章