為什麼在允許某些Unicode字符的註釋中執行Java代碼?




comments (7)

以下代碼產生輸出“Hello World!” (沒有,試試看)。

public static void main(String... args) {

   // The comment below is not a typo.
   // \u000d System.out.println("Hello World!");
}

原因是Java編譯器將Unicode字符\u000d解析為一個新行並轉換為:

public static void main(String... args) {

   // The comment below is not a typo.
   //
   System.out.println("Hello World!");
}

因此導致評論被“執行”。

既然這可以用來“隱藏”惡意代碼或者一個邪惡的程序員可以設想的東西, 為什麼它允許評論

為什麼Java規範允許這樣做?


唯一能夠回答為什麼Unicode轉義符被實現的人才是編寫規範的人。

一個可能的原因是,有人希望允許整個BMP作為Java源代碼的可能字符。 但是這提出了一個問題:

  • 你想能夠使用任何BMP字符。
  • 您希望能夠合理簡單地輸入任何BMP字符。 一種方法是使用Unicode轉義符。
  • 你希望保持詞彙規範易於人類閱讀和書寫,並且相當容易實現。

當Unicode轉義輸入時,這非常困難:它創建了一個新的詞法分析規則。

簡單的解決方法是分兩步進行搜索:首先搜索並用所代表的字符替換所有Unicode轉義,然後解析生成的文檔,就好像Unicode轉義不存在一樣。

對此的好處是指定起來很容易,所以它使規格更簡單,並且易於實現。

缺點是,你的榜樣。


編譯器不僅將Unicode轉義符轉換為它們表示的字符,然後它將程序解析為標記,但是它在放棄註釋和空白之前這樣做。

該程序包含一個Unicode轉義(\ u000d),位於其唯一註釋中。 正如註釋告訴你的,這個轉義代表了換行字符,編譯器在丟棄註釋之前正確翻譯它

它依賴於平台,在某些平台上,比如UNIX,它可以在其他平台上工作,比如Windows,它不會。 雖然輸出可能與裸眼看起來相同,但如果將其保存在文件中或通過管道連接到另一個程序進行後續處理,則很容易造成問題。


這是一個有意的設計選擇,可以回溯到Java的原始設計。

對於那些詢問“誰希望在註釋中使用Unicode轉義?”的人,我認為他們是本族語使用拉丁字符集的人。 換句話說,在Java的原始設計中,人們可以在Java程序中任何合法的地方使用任意的Unicode字符,最常見的是在註釋和字符串中。

這可以說是程序(如IDE)中用來查看源文本的一個缺點,即這些程序無法解釋Unicode轉義並顯示相應的字形。


Unicode解碼發生在任何其他詞彙翻譯之前。 這樣做的關鍵好處是它可以在ASCII和其他編碼之間來回切換。 你甚至不需要弄清楚評論開始和結束的地方!

正如JLS第3.3節所述,這允許任何基於ASCII的工具來處理源文件:

[...] Java編程語言規定了一種將用Unicode編寫的程序轉換為ASCII的標準方法,該程序將程序轉換為可由基於ASCII的工具處理的格式。 [...]

這為平台獨立性(支持的字符集的獨立性)提供了基本保證,這一直是Java平台的關鍵目標。

能夠在文件中的任何位置編寫任何Unicode字符都是一個很好的功能,在使用非拉丁語言編寫代碼時,在註釋中尤其重要。 它可以以這種微妙的方式乾擾語義的事實只是一個(不幸的)副作用。

關於這個主題有很多小問題,Joshua Bloch和Neal Gafter的Java Puzzlers包含以下變體:

這是一個合法的Java程序嗎? 如果是這樣,它打印什麼?

\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020
\u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079
\u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020
\u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063
\u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028
\u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020
\u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b
\u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074
\u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020
\u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b
\u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d

(這個程序原來是一個普通的“Hello World”程序。)

在對益智遊戲的解決方案中,他們指出了以下幾點:

更嚴重的是,這個謎題有助於強化前三個教訓: 當你需要插入不能以任何其他方式表示的字符到你的程序中時,Unicode轉義是非常重要的。 在所有其他情況下避免它們。

來源: Java:在評論中執行代碼?


由於這還沒有解決,在這裡解釋一下,為什麼Unicode轉義的翻譯發生在任何其他源代碼處理之前:

它背後的想法是,它允許在不同的字符編碼之間對Java源代碼進行無損的轉換。 今天,Unicode支持得到了廣泛的支持,這看起來並不是問題,但是當時來自西方國家的開發人員不容易從他的亞洲同事那裡接收一些包含亞洲字符的源代碼,並做出一些改變(包括編譯和測試)並將結果發回,而不會損壞某些東西。

因此,Java源代碼可以用任何編碼編寫,並允許標識符,字符和String文字和註釋中的各種字符。 然後,為了無損傳輸它,目標編碼不支持的所有字符都被它們的Unicode轉義替換。

這是一個可逆的過程,有趣的一點是,翻譯可以通過一個工具完成,該工具不需要知道關於Java源代碼語法的任何內容,因為翻譯規則不依賴於它。 這在編譯器內部轉換為其實際Unicode字符時獨立於Java源代碼語法發揮作用。 這意味著您可以在兩個方向上執行任意數量的翻譯步驟,而無需更改源代碼的含義。

這是另一個奇怪的功能,甚至沒有提到的原因: \uuuuuuxxxx語法:

當翻譯工具轉義字符並遇到已經是轉義序列的序列時,它應該在序列中插入一個附加的u ,將\ucafe轉換為\uucafe 。 含義不會改變,但當轉換到另一個方向時,該工具應該只刪除一個u並用Unicode字符替換包含單個u的序列。 這樣,即使Unicode轉義字符在來迴轉換時仍保留其原始格式。 我想,沒有人使用過這個功能......


我會完全無效地加上這一點,只是因為我無法幫助自己,而我還沒有看到它的存在,因為它包含一個隱藏的前提是錯誤的,即代碼是在一條評論!

在Java中,源代碼\ u000d在任何情況下都與ASCII CR字符等效。 無論它出現在哪裡,它都是一條簡單而簡單的結尾。 問題中的格式是誤導性的,字符序列實際上在語法上對應的是:

public static void main(String... args) {
   // The comment below is no typo. 
   // 
 System.out.println("Hello World!");
}

因此,恕我直言,最正確的答案是:代碼執行,因為它不在註釋中; 它在下一行。 Java中不允許執行“註釋中的代碼”,就像您期望的那樣。

大部分的困惑源於這樣一個事實,即語法熒光筆和IDE不夠複雜,無法將這種情況考慮在內。 他們要么根本不處理unicode轉義,要么在解析代碼之後而不是之前執行它,就像javac一樣。


我同意@zwol這是一個設計錯誤; 但我更批判它。

\u轉義在字符串和字符文字中很有用; 這是它應該存在的唯一地方。 它應該像其他轉義一樣處理,如\n ; 和"\u000A" 應該完全是"\n"

評論中沒有任何意見 - 沒有人可以閱讀。

同樣,在程序的其他部分中使用\uxxxx也沒有意義。 唯一的例外可能是在被強制包含一些非ASCII字符的公共API中 - 我們最後一次看到的是什麼?

設計師在1995年有他們的理由,但20年後,這似乎是錯誤的選擇。

(問題給讀者 - 為什麼這個問題不斷得到新的投票?這個問題是從某個受歡迎的地方連接起來的嗎?)







comments