在當今軟件開發領域中,泛型是一種強大的編程特性,它能夠在不犧牲類型安全的前提下,實現代碼的複用和靈活性。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的泛型中,如果 T1
是 T2
的子類型,這並不意味着 Foo[T1]
和 Foo[T2]
之間存在任何關係。同樣地,即使 T1
是 T2
的超類型,Foo[T2]
和 Foo[T1]
之間也沒有任何關係。這種設計決定簡化了泛型的實現,並有助於保持Go代碼的簡潔和可讀性。然而,這也意味着某些依賴於協變性或逆變性的泛型編程技術可能無法直接應用於Go的泛型中。
這種情況在 Java
語言中得到了很好的解決:
// 協變性
private static void sort(List<? extends Number> list) {
}
// 逆變性
private static void reverse(List<? super Number> list) {
}