關於 Java 你不知道的 10 件事

来源:http://www.cnblogs.com/wl2014/archive/2017/12/11/8024153.html
-Advertisement-
Play Games

作為 Java 書呆子,比起實用技能,我們會對介紹 Java 和 JVM 的概念細節更感興趣。因此我想推薦 Lukas Eder 在 jooq.org 發表的原創作品給大家。 你是從很早開始就一直使用 Java 嗎?那你還記得它的過去嗎?那時,Java 還叫 Oak,OO 還是一個熱門話題,C++ ...


作為 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<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 implements Type<Type <? super C>> {}
class D<P> implements Type<Type <? super D<D<P>>>> {}

現在告訴我,類型 C 和 D 到底是什麼?

它們存在遞歸,是一種類似 java.lang.Enum (但有略微不同)的遞歸方式。看看:

public abstract class Enum<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<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 的目標類型和參數類型都可以序列化,那麼你可以序列化這個 Lambda

但是即使是這樣,他們都不能自動實現 Serializable 標記介面。你必須強制轉換類型。但是當你只扔給 Serializable 時…

execute((Serializable) (() -> {}));

… 那麼 lambda 將不再是 Runnable 的。

因此要把它轉換為兩種類型:

execute((Runnable & Serializable) (() -> {}));

結論

一句話總結這篇文章就是:

Java 恰好是一種看起來神秘的語言,其實不然。

 

 

歡迎加入學習交流群569772982,大家一起學習交流。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 首先,把連接資料庫的語句做成工具類,因為會一直用到這幾句 代碼如下: 工具里包含兩個靜態方法都可以直接使用,一個是獲得連接getConn得到連接類Connection的對象,後面連接資料庫直接DBUtil.getConn()就可以了 還有一個是用完資料庫要關閉與資料庫的連接,使用DBUtil.clo ...
  • 1. 創建Java的類 如果說Java的一切都是對象,那麼類型就是決定了某一類對象的外觀與行為。可是類型的關鍵字不是type,而是class,創建一個新的類型要用下麵的代碼: 1 2 3 class AClassName{ //類型的主體 } 1 2 3 class AClassName{ //類型 ...
  • Description 輸入一個正整數n,求有幾對素數x,y,滿足n=x+y((3,2)和(2,3)視為相同的素數對)。 Input 有多組數據,每行輸入一個偶整數n(3<n<32768)。n等於0時,輸入結束。 Output 輸出相應的答案。 Sample Input 6 10 12 0 Samp ...
  •   在分析了hashCode方法和equals方法之後,我們對hashCode方法和equals方法的相關作用有了大致的瞭解。在通過查看HashMap類的相關源碼的時候,發現其中存在一個int hash(int h)的方法。在HashMap中該方法的源碼如下:  &em ...
  • 文章核心 其實,本不想把標題寫的那麼恐怖,只是發現很多人幹了幾年java以後,都自認為是一個不錯的java程式員了,可以拿著上萬的工資都處宣揚自己了,寫這篇文章的目的並不是嘲諷和我一樣做java的同行們,只是希望讀者看到此篇文章後,可以和我一樣,心平氣和的爭取做一個優秀的程式員。 講述方向 由於一直 ...
  • Properties類與.properties文件:   Properties類繼承自Hashtable類並且實現了Map介面,也是使用一種鍵值對的形式來保存屬性集的類,不過Properties有特殊的地方,就是它的鍵和值都是字元串類型。而.properties文件是由“鍵=值” ...
  • 使用過濾器改進應用程式 一、過濾器的目的 過濾器是可以攔截訪問資源的請求、資源的響應或者同時攔截兩者的應用組件。過濾器可以檢測和修改請求和響應,同時也可以拒絕、重定向或轉發請求。javax.servlet.Filter介面實現了過濾器技術,使用HttpServletRequest和HttpServl ...
  • String是最常使用的Java類之一,整理的了一些重要的String知識分享給大家。 作為一個Java新手程式員,對String進行更深入的瞭解很有必要。如果你是有幾年Java開發經驗,可以根據目錄選擇性的閱讀以下內容。 1、什麼是String,它是什麼數據類型? String是定義在 java. ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...