在当今软件开发领域中,泛型是一种强大的编程特性,它能够在不牺牲类型安全的前提下,实现代码的复用和灵活性。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) {  
}


↙↙↙阅读原文可查看相关链接,并与作者交流