一個Java方法能使用多少個引數?

程式猿DD發表於2020-08-03

我最近給我fork的專案QuickTheories增加了一個介面:

@FunctionalInterface
public interface QuadFunction<A, B, C, D, E> {
    E apply(A a, B b, C c, D d);
}

這讓非常好奇一個方法能夠有多少個型別引數呢?據我所知,Java的語言規範並沒有提到這個問題。1

關於在實現上這個閾值的定義,我有兩個猜測:

  1. 編譯器會強制一個可預測的閾值,例如255或者65535。
  2. 由於實現細節的原因,編譯器的異常處理會施加意想不到的限制。

我不想通過我薄弱的C++技能來測試原始碼,所以我決定直接來測試編譯器2。我寫了一個Python指令碼,通過二分法找到一個會觸發錯誤的最小值。完整的程式碼請見連線Github Repo

最直接的辦法就是生成方法。幸運的是,我們不必使用任何已有的型別引數,只需要按照<A,B,C..>的形式來生成:

def write_type_plain(count):
    with open('Test.java', 'w') as f:
        f.write("public class Test {\n")
        f.write("public <")
        for i in range(count):
            if (i > 0):
                f.write(", ")
            f.write("A" + str(i + 1))
        f.write("> void testMethod() {}")
        f.write("}")

執行這個二分法的程式碼會有如下輸出:

>>> error: UTF8 representation for string "<A1:Ljava/lang/Objec..." is too long for the constant pool 
>>> largest type: 2776

這個錯誤讓人有點費解,但是從事後來看還是可以理解的。編譯器生成的類檔案包含多個字串,包括每個方法的方法簽名。這些字串儲存在常量池內,而常量池的內容有最大65535位元組數的限制,這個是JVM的所定義的。

所以,我之前的猜測都不是完全的正確。型別引數的最大個數是一個意料之外的值,而不是一個確定值。但是,編譯器的實現本身並不是導致錯誤的原因3。相反,是JVM類檔案的格式要求限制了型別引數可使用的數量。其實JVM對泛型本身一無所知。

這同時也表示型別引數的最大個數取決於你寫的方法程式碼4。我嘗試用另外一種型別引數的編碼方案(先前連結文中的write_type_compact),使用全部合法的ASCII字元。這個實現是有點繁瑣的,因為字元0-9是合法的,但不能作為識別符號的首字母,並且Java關鍵字也不能作為型別引數。我僅僅將ifdo替換為等長的UTF-8字元。採用這種更緊湊的編碼方案讓型別引數的個數從2776提升到了3123。

還是有一些不太方便的地方,例如_A是一個合法的Java識別符號,但是_不是。我的編碼在不使用_作為首字幕的情況下,最高生成了3392個2位元組的型別引數。所以我覺得不用考慮_作為首字母的情況了。

另外一個技巧

通過反編譯類檔案,我觀察到65536個字元中大部分都不是我生成的型別引數,而是重複的字串Ljava/lang/Object;。這是因為型別引數沒有包含額外的資訊,所以類檔案將其視為Object的繼承,並將它們編入方法簽名內。我通過修改我的生成器來優化這個問題。

迴圈的關鍵程式碼修改為:

s = type_var(i)
f.write(s)
if (s != 'A'):
    f.write(" extends A")

除開一個例項之外,所有的型別引數都從繼承java/lang/Object改為繼承A。這個修改將型別引數的數量提升到9851個。

型別引數的數量提升了非常多,而我所使用的編碼方法還可以繼續改進。例如使用非ASCII unicode識別符號,不過我已經比較滿意現在的效果了。

這些都不重要

在實際情況中是不太可能達到上述數量限制的。程式碼生成時可能會達到語言或者編譯器的某些極限,就算罕見的遇到了生成上百個型別引數的情況,那距離幾千個的限制仍然還相距很遠。

儘管如此,如果我是規則的制定者,我將不允許任何類或者方法使用超過255個型別引數的情況。即使隻影響了百萬分之一的程式,有明確的限制會更好。

  1. §4.4, §8.1.2, §9.1.2, §8.4.4, §8.8.4 這些章節都和方法或者類的型別引數有關,但是都沒有指明允許有多少個型別引數。
  2. 當我寫這段話時,我想起了Hotspot是C++寫的,javac是Java寫的。就算這樣我依然會選擇做程式碼實驗,而不是閱讀程式碼。閱讀別人程式碼是種煎熬
  3. 逗號之後的空格不會影響,因為編譯器會規範化它的輸出。
  4. 這也表示與我使用哪個JVM無關。為了完整性,我在Fedora 29上使用了1.8.0_191-b13版本的OpenJdk。

本文作者:justinblank, 翻譯:1 Way
原文連結:https://justinblank.com/experiments/howmanytypeparameterscanajavamethodhave.html
譯文首發:http://blog.didispace.com/howmanytypeparameterscanajavamethodhave/

本文有spring4all技術翻譯組完成,更多國外前沿知識和乾貨好文,歡迎關注公眾號:後端面試那些事兒。

相關文章