一個你可能沒聽說過的 Java 語法

  • 2020 年 2 月 20 日
  • 筆記

當然這肯定也是標題黨了,比如群裡面的大佬 Glavo 就是反例,怎麼可能有 Glavo 沒聽說過的 Java 語法呢。

所以說這是什麼語法呢?

首先我是在提出這個問題之後自己撕烤的時候突然腦補出的這個語法。

問題中首先擺出了這麼一個語法:

void f(@NotNull List<@NotNull String> strings) {  }

函數 f 的參數的類型是 @NotNullList<@NotNullString>,表示這個參數本身不能為 null ,而它作為一個 List,它的成員也都不能是 null 。 這個看起來非常好理解,因為它實際上就是它看起來那樣,很符合直覺。

其實還有這種操作:

<E> void f(@NotNull WhatEver<@NotNull ? extends @NotNull List<@NotNull E>> whatEver) {  }

但是如果這個參數是一個數組呢?

void f(@NotNull String[] /* emmm... */ strings) {  }

這個時候,我甚至不知道這個 @NotNull 註解的對象是什麼(是參數?是 String?是 String[]?)。 在 Kotlin 中,我們可以寫 Array<String?>Array<String>?,分別是本身不能為 null 但成員可以為 null 的數組和本身可以為 null 但成員不能為 null 的數組,這樣的兩種不同的類型在 Java 裡面又應該怎麼表達呢。

在 SO 提問之餘,我就自己研究了一下。 我猜測,可能 [] 前面也能寫東西?於是我試了一下:

void f(@NotNull String @NotNull [] strings) {  }

這個程式碼居然編譯過了(提醒一下讀者:不是所有註解都可以這麼用的,如果你在使用自己寫的註解嘗試這個例子,請給你使用的註解加上 @Target({ElementType.TYPE_USE})。)。 我很是震精,於是我開始試圖了解它背後的含義。這個時候最方便的測試方法當然就是看 @NotNull 系列註解在 Kotlin 里的表現啦。 首先我們寫一個這樣的函數:

import org.jetbrains.annotations.Nullable;    public class A {    public static void main(@Nullable String[] args) {    }  }

然後我在 Kotlin 裡面調用它,發現它的簽名是這樣的:

說明 Kotlin 把這個註解同時應用到了 ArrayString 上。

而如果把註解寫在我之前猜的那個位置的話:

import org.jetbrains.annotations.Nullable;    public class A {    public static void main(String @Nullable [] args) {    }  }

Kotlin 就直接無視了它(感嘆號表示 Platform Type,是『未被標註為 @NotNull 或者 @Nullable 的意思』):

別急,在不知道這個東西的語義的時候先不要急著批判 Kotlin。 我們編譯一下這個程式碼里的兩個函數,看看位元組碼吧:

import org.jetbrains.annotations.Nullable;    public class A {    public static void main(String @Nullable [] args) {    }    public static void main(@Nullable Number [] args) {    }  }

然後使用這個命令看看位元組碼( javap-v 參數表示輸出額外資訊,這裡不需要 -c(顯示方法體)和 -p(顯示 private 的東西)):

$ gradle assemble  $ javap -v A.class

看到 javap 輸出了以下結果(已經省略了 80% 對本文無意義的內容了):

... 省略 ...  Constant pool:  ... 省略 ...    #16 = Utf8               Lorg/jetbrains/annotations/Nullable;  ... 省略 ...    public static void main(java.lang.String[]);  ... 省略 ...      RuntimeInvisibleTypeAnnotations:        0: #16(): METHOD_FORMAL_PARAMETER, param_index=0      public static void main(java.lang.Number[]);  ... 省略 ...      RuntimeInvisibleTypeAnnotations:        0: #16(): METHOD_FORMAL_PARAMETER, param_index=0, location=[ARRAY]      RuntimeInvisibleParameterAnnotations:        0:          0: #16()  }  ... 省略 ...

在常量池裡面我們可以看到 #16 就是 @Nullable 註解:

Constant pool:    #16 = Utf8               Lorg/jetbrains/annotations/Nullable;

然後在兩個測試函數中,可以看到 #16 註解在不同的地方生效了。 首先是 String@Nullable[]args 的第一個函數:

RuntimeInvisibleTypeAnnotations:    0: #16(): METHOD_FORMAL_PARAMETER, param_index=0

然後是 @NullableNumber[]args 的第二個函數:

RuntimeInvisibleTypeAnnotations:    0: #16(): METHOD_FORMAL_PARAMETER, param_index=0, location=[ARRAY]  RuntimeInvisibleParameterAnnotations:    0:      0: #16()

呃。。。好吧,首先很明顯第二個 @Nullable 同時生效於類型和參數本身了,而第一個只在類型上生效了。 不過我還是不知道他們各自在類型上生效時的位元組碼的意思(看不懂位元組碼真是對不起呢),於是就使用控制變數法,再寫兩個函數對比一下(之所以使用兩個不同的 List 實現,是因為 List 和數組不一樣,擦除了就一樣了所以 JVM 簽名就衝突叻):

import org.jetbrains.annotations.Nullable;    import java.util.ArrayList;  import java.util.LinkedList;    public class A {    public static void main(ArrayList<@Nullable String> args) {    }    public static void main(@Nullable LinkedList<Number> args) {    }  }

位元組碼出來是這樣的(已經省略了 90% 對本文無意義的內容了):

... 省略 ...  Constant pool:  ... 省略 ...    #20 = Utf8               Lorg/jetbrains/annotations/Nullable;  ... 省略 ...    public static void main(java.util.ArrayList<java.lang.String>);  ... 省略 ...      RuntimeInvisibleTypeAnnotations:        0: #20(): METHOD_FORMAL_PARAMETER, param_index=0, location=[TYPE_ARGUMENT(0)]      public static void main(java.util.LinkedList<java.lang.Number>);  ... 省略 ...      RuntimeInvisibleTypeAnnotations:        0: #20(): METHOD_FORMAL_PARAMETER, param_index=0      RuntimeInvisibleParameterAnnotations:        0:          0: #20()  }

和我想的差不多,寫在整個參數前面( @NullableList<String> 或者 @NullableString[])就是對外部的類型和參數同時進行註解,而寫在類型參數或者數組的 [] 前面( List<@NullableString> 或者 String@Nullable[])就是對類型參數進行註解。

再看看對於泛型類型,Kotlin 的處理方法吧。首先就是剛才那個 Java 程式碼,Kotlin 表示:

原來你丫不僅認識對參數的註解,還認識對類型參數的註解啊。

好了,謎底揭曉 ~ 於是我們可以說是 Kotlin 對這個語法的處理是錯誤的啦。 至於 Kotlin 是否能對二進位的 Java 程式碼中的這個語法正確處理呢,我已經沒有耐心去測試了(Kotlin 的 Java 和 JVM bytecode 前端就是 IntelliJ IDEA 的 Java 和 JVM bytecode 前端,但我也不想再去看了)。

關於 Kotlin 的這個問題我已經在 YouTrack 上開 issue 了,大家可以去圍觀或者 upvote(逃

這個寫法可以擴展到這些情況:

  • String@Nullable[]@Nullable[]
  • String@Nullable...

都是合法的 Java 程式碼哦。

其實最靠譜的參考還是 Java 標準 (https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.7.4)里對這個 case 的說明啦。