Java & Go 泛型對比

FunTester發表於2024-03-20

在當今軟體開發領域中,泛型是一種強大的程式設計特性,它能夠在不犧牲型別安全的前提下,實現程式碼的複用和靈活性。Java 作為一種老牌的物件導向程式語言,在其長期的發展過程中,已經積累了豐富的泛型經驗和應用場景。而 Go 語言作為一種相對較新的程式語言,也在不斷探索和發展其泛型特性,以滿足現代軟體開發的需求。本文將對 Java 和 Go 語言的泛型進行比較和介紹,探討它們的實現方式、語法特點以及適用場景,幫助讀者更好地理解和應用泛型程式設計。

隨著 Go 語言 1.18 版本的釋出,泛型正式成為了 Go 語言的一部分,填補了原本的短板。透過引入型別引數,使得函式和資料結構可以接受任意型別的引數,從而提升了程式碼的可複用性和靈活性。這項特性經過長時間的設計和討論,在新版本中,開發者可以透過type關鍵字來定義泛型函式和泛型型別,以及使用泛型約束來限制泛型型別引數的行為。這些新特性的引入,將為 Go 語言的開發者們帶來更為豐富和靈活的程式設計體驗。

泛型的引入為 Go 語言帶來了一種更為優雅和靈活的程式設計方式。透過型別引數的引入,函式和資料結構可以接受任意型別的引數,避免了之前透過介面和型別斷言等方式實現類似功能的冗餘性和複雜性。在新版本中,開發者可以使用type關鍵字定義泛型函式和泛型型別,以及使用泛型約束來限制泛型型別引數的行為,從而提升了程式碼的可讀性和可維護性。

Go 語言 1.18 版本的泛型特性經過了長時間的設計和討論,以確保其能夠滿足廣大開發者的需求,並且與現有的 Go 語言生態無縫銜接。這些新特性的引入,將為 Go 語言的開發者們帶來更為豐富和靈活的程式設計體驗,幫助他們更好地應對複雜的程式設計場景。相信隨著更多開發者開始使用泛型特性,Go 語言的生態和社群將會變得更加豐富和多樣,為未來的 Go 語言程式設計帶來更多的可能性和機會。

語法

讓我們首先看一下 Go 語言的泛型例子:

// Print[T any] //  @Description: 列印型別  
//  @param t 任意型別  
//  
func Print[T any](t T) {  
    fmt.Printf("printing type: %T\n", t)  
}  

//  
//  Tree[Tany]  
//  @Description: 樹結構  
//  
type Tree[T any] struct {  
    left, right *Tree[T]  
    data        T  
}

下面看一下 Java 泛型:

/** 列印任意型別  
 * @param t 任意型別  
 * @param <T> 任意型別  
 */  
public static <T> void print(T t) {  
    System.out.println("printing type: " + t.getClass().getName());  
}  


/**  
 * @param <T> 樹的資料型別  
 */  
class Tree<T> {  
    private Tree<T> left, right;  
    private T data;  
}

這兩個示例展示了在 Go 語言和 Java 中實現泛型的方式。雖然兩者都可以實現泛型,但它們的語法和實現方式有所不同。

在 Go 語言中,泛型是透過在函式或型別上使用型別引數來實現的。在函式 Print[T any](t T) 中,[T any] 表示型別引數,any 表示型別約束,即可以接受任意型別的引數。在型別 Tree[T any] 中,[T any] 表示型別引數,any 同樣表示型別約束,表示可以是任意型別的引數。

而在 Java 中,泛型是透過使用尖括號 <T> 來定義型別引數,並在函式或類宣告中使用這些型別引數。在函式 print(T t) 中,<T> 表示型別引數,表示該函式可以接受任意型別的引數。在類 Tree<T> 中,<T> 同樣表示型別引數,表示該類可以是任意型別的資料型別。

總的來說,雖然 Go 語言和 Java 都支援泛型,但它們的語法和實現方式略有不同。Go 語言的泛型實現相對簡潔和直觀,而 Java 的泛型實現更加靈活和強大。

一個區別:Go 需要型別引數被型別顯式約束(例如: T any ),而 Java 則沒有( T 本身被隱式地推斷為 java.lang.Object )。如果在 Go 中沒有提供約束,將導致類似於下面的錯誤:

syntax error: missing type constraint

我懷疑差異在於 Java 的統一型別層次結構(每個物件都是 java.lang.Object)。而 Go 語言則沒有這樣的模型。

型別開關

當我在 Go 語言中試圖獲取一個泛型的 type 值時,就會報錯,例子如下:

func print[T any](t T) {  
    switch t.(type) {  
    case string:  
       fmt.Println("printing a string: ", t)  
    }  
}

報錯:

./fun_test.go:126:9: cannot use type switch on type parameter value t (variable of type T constrained by any)

但是當我把泛型替換成 interface{} 時,編譯透過了。當然這是 Go 語言的特殊設計,並不像 Java 那樣,所以物件均是 java.lang.Object 子類。懷著這樣的疑問,我們將 Go 語言泛型型別引數進行約束,如下:

func print[T int64|float64](t T) {
    switch t.(type) {
        case int64:   fmt.Println("printing an int64: ", t)
        case float64: fmt.Println("printing a float64: ", t)
    }
}

依然得到了如下報錯:

./fun_test.go:126:9: cannot use type switch on type parameter value t (variable of type T constrained by int64 | float64)

看來這似乎是 Go 語言特殊的設計,並不希望泛型功能被使用或者泛型本身並不是具有某個型別屬性的型別。我們再看一下 Java 是如何處理此類情況:

/** 列印任意型別  
 * @param t 任意型別  
 * @param <T>  
 */  
public static <T> void print(T t) {  
    switch(t) {  
        case String s:  
            System.out.println("字串型別: " + s);  
        default   :  
            System.out.println("非字串型別: " + t.getClass().getName());  
    }  
}

這段程式碼如何遇到報錯:

java: -source 17 中不支援 switch 語句中的模式
  (請使用 -source 21 或更高版本以啟用 switch 語句中的模式)

請切換 21 及以上 SDK 版本,但其實沒有必要,實際編碼也用不到這個語法。

型別約束

在 Go 語言中,型別引數約束 T any 表示 T 不受任何特定介面的約束。換句話說,T 實現了 interface{}(但不完全如此;參考第二章節)。在 Go 語言中,如果一個型別引數被約束為 T any,則該型別引數 T 不受任何特定介面的限制。也就是說,任何實現了空介面 interface{} 的型別都可以作為型別引數 T 的實際型別。但需要注意的是,並非所有型別引數 T 都實現了 interface{} 介面,具體取決於上下文和型別約束的情況。

在 Go 語言中,我們可以透過指示除 any 之外的東西來進一步約束 T 的型別集,例如:

// Tree[Tany]  
// @Description: 樹結構  
type Tree[T any] struct {  
    left, right *Tree[T]  
    data        T  
}

等價的 Java 程式碼如下:

class Tree<T extends Integer> {
    private Tree<T> left, right;
    private T data;
}

在 Go 語言中,型別引數宣告可以指定具體型別(如 Java),並且可以內聯或引用宣告:

// PrintInt64[T int64] //  @Description: 列印64位整數  
//  @param t  
//  
func PrintInt64[T int64](t T) {  
    fmt.Printf("%v\n", t)  
}  

// PrintInt64[T Int64Type] //  @Description: 列印64位整數  
//  @param t  
//  
func PrintInt64[T Int64Type](t T) {  
    fmt.Printf("%v\n", t)  
}  
//  
//  Bit64Type 64位整數型別  
//  @Description: 64位整數型別  
//  
type Bit64Type interface {  
    int64  
}

當然這段程式碼會報 此包中重新宣告的 'PrintInt64' 檢查異常,可以暫時忽略。

聯合型別

Go 和 Java 都支援聯合型別作為型別引數,但它們的方式非常不同。

Go 只允許具體型別的聯合型別。程式碼如下:

// GOOD
func PrintInt64OrFloat64[T int64|float64](t T) {
    fmt.Printf("%v\n", t)
}

type someStruct {}

// GOOD
func PrintInt64OrSomeStruct[T int64|*someStruct](t T) {
    fmt.Printf("t: %v\n", t)
}

// BAD
func handle[T io.Closer | Flusher](t T) { // 在聯合中不能將介面與方法結合使用
    err := t.Flush()
    if err != nil {
        fmt.Println("failed to flush: ", err.Error())
    }

    err = t.Close()
    if err != nil {
        fmt.Println("failed to close: ", err.Error())
    }
}

type Flusher interface {
    Flush() error
}

Java 只允許介面型別的聯合型別,或者非介面型別和介面型別之間的聯合型別。

// GOOD  
public static class Tree<T extends Closeable & Flushable> {  
    private Tree<T> left, right;  
    private T data;  
}  

// GOOD  
public static <T extends Number & Closeable> void printNumberAndClose(T t) {  
    System.out.println(t.intValue());  
    try {  
        t.close();  
    } catch (IOException e) {  
        System.out.println("io exception: " + e.getMessage());  
    }  
}  

// BAD  
public static <T extends Integer & Float> void printIntegerOrFloat(T t) {  
    System.out.println(t.toString()); // 模糊的呼叫  
    System.out.println(t.isNaN());  
}

變異性

Go 的泛型提案不包括對協變性和逆變性的支援。這意味著泛型型別中的型別之間的關係不受型別引數的子型別關係的影響。換句話說,在 Go 的泛型中,如果 T1T2 的子型別,這並不意味著 Foo[T1]Foo[T2] 之間存在任何關係。同樣地,即使 T1T2 的超型別,Foo[T2]Foo[T1] 之間也沒有任何關係。這種設計決定簡化了泛型的實現,並有助於保持 Go 程式碼的簡潔和可讀性。然而,這也意味著某些依賴於協變性或逆變性的泛型程式設計技術可能無法直接應用於 Go 的泛型中。

這種情況在 Java 語言中得到了很好的解決:

// 協變性  
private static void sort(List<? extends Number> list) {  
}  

// 逆變性  
private static void reverse(List<? super Number> list) {  
}
  • 2021 年原創合集
  • 2022 年原創合集
  • 2023 年原創合集
  • 介面功能測試專題
  • 效能測試專題
  • Java、Groovy、Go、Python
  • 單元&白盒&工具合集
  • 測試方案&BUG&爬蟲&UI 自動化
  • 測試理論雞湯
  • 社群風采&影片合集
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章