關(guān)于Java你不知道的十件事
作為 Java 書呆子,比起實(shí)用技能,我們會(huì)對(duì)介紹 Java 和 JVM 的概念細(xì)節(jié)更感興趣。因此我想推薦 Lukas Eder 在 jooq.org 發(fā)表的原創(chuàng)作品給大家。
你是從很早開始就一直使用 Java 嗎?那你還記得它的過去嗎?那時(shí),Java 還叫 Oak,OO 還是一個(gè)熱門話題,C++ 的 folk 者認(rèn)為 Java 是不可能火起來,Java 開發(fā)的小應(yīng)用程序 Applets 還受到關(guān)注。
我敢打賭,下面我要介紹的這些事,有一半你都不知道。下面讓我們來深入探索 Java 的神秘之處。
1. 沒有檢查異常這種事情
沒錯(cuò)!JVM 不會(huì)知道這些事情,只有 Java 語(yǔ)句知道。
如今大家都認(rèn)為檢查異常是個(gè)錯(cuò)誤。正如 Bruce Eckel 在布拉格 GeeCON 閉幕時(shí)所說,Java 之后再?zèng)]別的語(yǔ)言檢查異常,甚至 Java 8 在新的 Stream API 中也不再干這個(gè)事情(如果你的 Lambda 使用 IO 和 JDBC,這其實(shí)還是有點(diǎn)痛苦)。
如何證實(shí) 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 就能辦到。
這篇文章可以看到更詳細(xì)的相關(guān)內(nèi)容,或者在 Stack Overflow 上看。
2. 你可以定義僅在返回值有差異的重載函數(shù)
這樣的代碼無(wú)法編譯,對(duì)不?
class Test {
Object x() { return "abc"; }
String x() { return "123"; }
}
對(duì)。 Java 語(yǔ)言不允許兩個(gè)方法在同一個(gè)類中“等效重載”,而忽略其諸如throws自居或返回類型等的潛在的差異。
查看 Class.getMethod(String, Class…) 的 Javadoc。 其中說明如下:
請(qǐng)注意,類中可能有多個(gè)匹配方法,因?yàn)?Java 語(yǔ)言禁止在一個(gè)類聲明具有相同簽名但返回類型不同的多個(gè)方法,但 Java 虛擬機(jī)并不是如此。虛擬機(jī)中增加的靈活性可以用于實(shí)現(xiàn)各種語(yǔ)言特征。例如,可以用橋接方法實(shí)現(xiàn)協(xié)變參返回; 橋接方法和被重寫的方法將具有相同的簽名但擁有不同的返回類型。
哇哦,有道理。實(shí)際上下面的代碼暗藏著很多事情:
abstract class Parent<T> {
abstract T x();
}
class Child extends Parent<String> {
@Override
String x() { return "abc"; }
}
來看看為 Child 生成的字節(jié)碼:
// 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]
其實(shí)在字節(jié)碼中 T 真的只是 Object。這很好理解。
合成的橋方法實(shí)際是由編譯器生成的,因?yàn)?Parent.x() 簽名中的返回類型在實(shí)際調(diào)用的時(shí)候正好是 Object。在沒有這種橋方法的情況下引入泛型將無(wú)法在二進(jìn)制下兼容。因此,改變 JVM 來允許這個(gè)特性所帶來的痛苦會(huì)更小(副作用是允許協(xié)變凌駕于一切之上) 很聰明,不是嗎?
你看過語(yǔ)言內(nèi)部的細(xì)節(jié)嗎?不妨看看,在這里會(huì)發(fā)現(xiàn)更多很有意思的東西。
3. 所有這些都是二維數(shù)組!
class Test {
int[][] a() { return new int[0][]; }
int[] b() [] { return new int[0][]; }
int c() [][] { return new int[0][]; }
}
是的,這是真的。即使你的大腦解析器不能立刻理解上面方法的返回類型,但其實(shí)他們都是一樣的!類似的還有下面這些代碼片段:
class Test {
int[][] a = {{}};
int[] b[] = {{}};
int c[][] = {{}};
}
你認(rèn)為這很瘋狂?想象在上面使用 JSR-308 / Java 8 類型注解 。語(yǔ)法的可能性指數(shù)激增!
@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 [] = {{}};
}
類型注解。看起來很神秘,其實(shí)并不難理解。
或者換句話說:
當(dāng)我做最近一次提交的時(shí)候是在我4周的假期之前。
對(duì)你來說,上面的內(nèi)容在你的實(shí)際使用中找到了吧。
4. 條件表達(dá)式的特殊情況
可能大多數(shù)人會(huì)認(rèn)為:
Object o1 = true ? new Integer(1) : new Double(2.0);
是否等價(jià)于:
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
然而,事實(shí)并非如此。我們來測(cè)試一下就知道了。
System.out.println(o1);
System.out.println(o2);
輸出結(jié)果:
1.0
1
由此可見,三目條件運(yùn)算符會(huì)在有需要的情況下,對(duì)操作數(shù)進(jìn)行類型提升。注意,是只在有需要時(shí)才進(jìn)行;否則,代碼可能會(huì)拋出 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. 你還沒搞懂復(fù)合賦值運(yùn)算符
很奇怪嗎?來看看下面這兩行代碼:
i += j; i = i + j;
直觀看來它們等價(jià),是嗎?但可其實(shí)它們并不等價(jià)!JLS 解釋如下:
E1 op= E2 形式的復(fù)合賦值表達(dá)式等價(jià)于 E1 = (T)((E1) op (E2)),這里 T 是 E1 的類型,E1 只計(jì)算一次。
非常好,我想引用 Peter Lawrey Stack Overflow 上的對(duì)這個(gè)問題的回答:
使用 *= 或 /= 來進(jìn)行計(jì)算的例子
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'
現(xiàn)在看到它的作用了嗎?我會(huì)在應(yīng)用程序中對(duì)字符串進(jìn)行乘法計(jì)算。因?yàn)?,你懂?hellip;
6. 隨機(jī)整數(shù)
現(xiàn)在有一個(gè)更難的謎題。不要去看答案,看看你能不能自己找到答案。如果運(yùn)行下面的程序:
for (int i = 0; i < 10; i++) {
System.out.println((Integer) i);
}
… “有時(shí)候”,我會(huì)得到下面的輸出:
92 221 45 48 236 183 39 193 33 84
這怎么可能??
. spoiler… 繼續(xù)解答…
好了,答案在這里 (https://blog.jooq.org/2013/10/17/add-some-entropy-to-your-jvm/),這必須通過反射重寫 JDK 的 Integer 緩存,然后使用自動(dòng)裝箱和拆箱。不要在家干這種事情!或者,我們應(yīng)該換種方式進(jìn)行此類操作。
7. GOTO
這是我的最愛之一。Java也有GOTO!輸入下試試……
int goto = 1;
將輸出:
Test.java:44: error: <identifier> expected
int goto = 1;
^
這是因?yàn)間oto是一個(gè)未使用的關(guān)鍵字, 僅僅是為了以防萬(wàn)一……
但這不是最令人興奮的部分。令人興奮的部分是你可以使用 break、continue 和標(biāo)記塊來實(shí)現(xiàn) goto 功能:
向前跳:
label: {
// do stuff
if (check) break label;
// do more stuff
}
在字節(jié)碼中格式如下:
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);
在字節(jié)碼中格式如下:
2 iload_1 [check]
3 ifeq 9
6 goto 2 // Jumping backward
9 ..
8. Java 有類型別名
其它語(yǔ)言 (比如 Ceylon) 中,我們很容易為類型定義別名:
interface People => Set<Person>;
這里產(chǎn)生了 People 類型,使用它就跟使用 Set<Person> 一樣:
People? p1 = null;
Set</Person><Person>? p2 = p1;
People? p3 = p2;
Java 中我們不能在頂層作用域定義類型別名,但是我們可以在類或方法作用域中干這個(gè)事情。假如我們不喜歡 Integer、Long 等等名稱,而是想用更簡(jiǎn)短的 I 和 L,很簡(jiǎn)單:
class Test<I extends Integer> {
<L extends Long> void x(I i, L l) {
System.out.println(
i.intValue() + ", " +
l.longValue()
);
}
}
在上面的程序中,Test 類作用域內(nèi) Integer 被賦予 I 這樣的 “別名”,類似地,Long 在 x() 方法中被賦予 L 這樣的 “別名”。之后我們可以這樣調(diào)用方法:
new Test().x(1, 2L);
這種技術(shù)當(dāng)然不太會(huì)受重視。這種情況下,Integer 和 Long 都是 final 類型,也就是說,I 和 L 是事實(shí)上的別名(基本上賦值兼容性只需要考慮一種可能性)。如果我們使用非 final 類型 (比如 Object),那就是一般的泛型。
這些把戲已經(jīng)玩夠了?,F(xiàn)在來看看真正了不起的東西!
9. 某些類型的關(guān)系并不確定!
好了,這會(huì)很引人注目,先來杯咖啡提提神。思考一下下面兩個(gè)類型:
// 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>>>> {}
現(xiàn)在告訴我,類型 C 和 D 到底是什么?
它們存在遞歸,是一種類似 java.lang.Enum (但有略微不同)的遞歸方式??纯矗?/p>
public abstract class Enum<E extends Enum<E>> { ... }
在上面的描述中,enum 實(shí)際上只是單純的語(yǔ)法糖:
// This
enum MyEnum {}
// Is really just sugar for this
class MyEnum extends Enum<MyEnum> { ... }
認(rèn)識(shí)到這一點(diǎn)之后我們回過頭來看看前面提到的兩個(gè)類型,下面的代碼會(huì)編譯成什么樣?
class Test {
Type< ? super C> c = new C();
Type< ? super D<Byte>> d = new D<Byte>();
}
非常難回答的問題,不過 Ross Tate 已經(jīng)回答了。這個(gè)問題的答案是不可判定的:
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 中試著編譯一下,它會(huì)崩潰! (不用擔(dān)心,我提交了 BUG 報(bào)告)
讓這個(gè)事情沉下去…
Java 中某些類型的關(guān)系是不明確的!
如果你對(duì) Java 這個(gè)用法感到奇怪之余也感興趣,就去看看 Ross Tate 寫的 “在 Java 的類型系統(tǒng)中使用通配符” (與 Alan Leung 和 Sorin Lerner 合著),我們也在討論泛型多態(tài)中的相關(guān)子類多態(tài)性。
10. 類型交集
Java 有一個(gè)非常奇怪的特性叫類型交集。你可以申明某個(gè)(泛型)類型,而它實(shí)際上是兩個(gè)類型的交集,比如:
class Test<T extends Serializable & Cloneable> {
}
綁定到 Test 類型實(shí)例的泛型類型參數(shù) T 必須實(shí)現(xiàn) Serializable 和 Cloneable。比如,String 就不符合要求,但 Dete 滿足:
// Doesn't compile
Test<String> s = null;
// Compiles
Test<Date> d = null;
這個(gè)特性已經(jīng)在 Java 8 中使用。這很有用嗎?幾乎沒用,但是如果你希望某個(gè) Lambda 表達(dá)式是這種類型,還真沒別的辦法。假設(shè)你的方法有這種瘋狂的類型約束:
<T extends Runnable & Serializable> void execute(T t) {}
你想通過執(zhí)行它得到一個(gè)可以序列化 (Serializable) 的 Runnable 對(duì)象。Lambda 和序列化也有點(diǎn)奇怪。
如果 Lambda 的目標(biāo)類型和參數(shù)類型都可以序列化,那么你可以序列化這個(gè) Lambda
但是即使是這樣,他們都不能自動(dòng)實(shí)現(xiàn) Serializable 標(biāo)記接口。你必須強(qiáng)制轉(zhuǎn)換類型。但是當(dāng)你只扔給 Serializable 時(shí)…
execute((Serializable) (() -> {}));
… 那么 lambda 將不再是 Runnable 的。
因此要把它轉(zhuǎn)換為兩種類型:
execute((Runnable & Serializable) (() -> {}));
結(jié)論
一句話總結(jié)這篇文章就是:
Java 恰好是一種看起來神秘的語(yǔ)言,其實(shí)不然。






























