Java,Golang,Rust 泛型的大体对比小记

引子

在我重构JVM尝试实现泛型时,在阅读《深入理解JVM》这本书时,对Java泛型的实现原因和应用场景有了一定了解,书中也将其与C#泛型进行了简单对比。当我去年在学习Golang时,也发现了Golang在1.8后才提供了泛型,并不是原生自带而是与Java类似在中间版本加入的特性,网上也有众多言论在抨击Golang泛型 "代码丑陋","使用别扭"。在近期我学习完Rust的泛型和trait后,对Rust泛型的 zero_abstract 也有了一定的理解,所以基于这个背景条件,我对三者进行了比对学习,并且总结成这篇小记。

一,Java的泛型

在java中,泛型的实现方式叫 "类型擦除式泛型 "(Type Erasure Generics),它只在程序源码中存在,在编译后的字节码文件中,全部泛型都被替代为原来的裸类型 ,所以对于 ArrayList<Integer> 和 ArrayList<String> 来说他们都时一个类型(Raw Tyoe, 裸类型)。而C#则采取的是 "具现化式类型", 即泛型在源码以及编译后的中间语言中或是在运行时都是存在的。举例:即我们不能使用 instanceof 对泛型进行实例判断,但是C#可以。

Java 5.0 引入泛型时,可以采取两种做法:

  • 需要泛型化的类型,以前有的就不办,平行的添加一套泛型化版本的新类型。
  • 直接把已有的类型泛型化。

考虑到Java在当时已经是诞生的第10年,遗留代码量极大,再加之Java已经存在了2套容器,如Vector和Arraylist,所以如果再添加一套会让语言显得特别臃肿。所以开发者采取了第二种方式。

本质上,Java的类型擦除是将所有的泛型设置为裸类型(所有泛化类型的共同父类型),在编译时将类似于ArrayList<Integer>或者ArrayList<String>粗暴的转化为ArrayList类型,在元素访问时插入了从Object到Integer或者String的强制转型代码。在运行时进行了所有的类型操作。

二. Rust 泛型

Rust的泛型是语言原生支持的,实现方式为 "单态化 ",使得 Rust 在性能上具有 零成本抽象(zero-cost abstraction)。可以对特征(trait)对泛型进行约束:

rust 复制代码
trait Stack<T> {
    fn push(&mut self, item: T);
    fn pop(&mut self) -> Option<T>;
}

零成本抽象的实现,其实本质上就是在编译时期,Rust就会生成对应调用版本的代码,比如我希望实现一个 fn add(i: T, j:T) -> T{} ,在调用时,我传入的时i32,那么在编译时Rust就会自动生成其对应的代码(即元编程),在运行时就不需要和Java一样进行类型擦除和强转,所以其效率是极高的。

但是这样做也会有缺陷------代码膨胀

三. Golang 泛型

Golang 采用字节码共享(monomorphization + type erasure)的方式处理泛型:

  • 部分类型(如 intfloat64)会进行代码展开(类似 C++ 模板)。
  • 但大多数类型在编译时仍使用 interface{} 实现(类似 Java 泛型的类型擦除)。

这导致泛型代码的性能有时不如直接用 interface{},因为 Go 需要在运行时做类型转换。

在大家诟病Golang泛型的几大原因:

  1. 其采用的表示方式为 ` ` 而不是大家一直以来熟悉的 `< >`在潜意识里 ` ` 会被我们和数组联系起来,所以不太直观。

  2. Go 泛型无法 直接用于接口类型,而只能通过普通的 struct 组合使用:

    Go 复制代码
    // ❌ 错误,Go 不支持泛型接口
    type Stack[T any] interface { 
        Push(T)
        Pop() T
    }
    
    // ✅ 正确
    type Stack[T any] struct {
        items []T
    }
  3. Golang 泛型也不能用于方法的类型接收者 ,而是只能用指针接收者:

    Go 复制代码
    ​
    type MyType[T any] struct {
        value T
    }
    
    // ❌ 不能这样写
    func (m MyType[T]) Get() T {
        return m.value
    }
    
    func (m *MyType[T]) Get() T { // ✅ 只能用指针接收者
        return m.value
    }
    
    ​
  4. 泛型不支持运算符重载

四. 小结

为什么Golang泛型被喷,我想大概还是因为其实现方式的妥协,作为一个追求高性能的编程语言,在泛型的实现中并没有保持语言本身的追求,而是采取了妥协中庸的解决办法,杂糅了Rust和Java的泛型,导致其不仅仅在语法习惯上让广大码农感觉不适。同时由于在某些条件下其采用了类型擦除式泛型的实现,也让其性能不如其对标的c/cpp。

相关推荐
-Thinker3 分钟前
【无标题】
java·开发语言·算法·图搜索
王五周八6 分钟前
Tesseract OCR的Java使用(附安装包,非常详细)
java·开发语言·ocr
旧书包的青春7 分钟前
2026年6月11日
java
实在智能RPA20 分钟前
航空Agent落地效果评估指标:2026年企业级智能自动化价值度量体系拆解
java·网络·人工智能·ai·自动化
星栈22 分钟前
写 Makepad Demo 不难,难的是把它写成项目
前端·rust
程序员二叉26 分钟前
【JUC】AQS底层深度拆解|独占/共享模式|队列原理全详解
java·开发语言·面试·juc
踏着七彩祥云的小丑27 分钟前
Go 学习第6天:结构体 + 切片 + range遍历
开发语言·学习·golang·go
地铁潜行者31 分钟前
消息堆积后,为什么一扩容消费者,MySQL 先被打崩了?
java·后端
地铁潜行者34 分钟前
订单状态更新成功了,分账消息却没发出去:聊聊本地消息表的一致性坑
java·后端
亦暖筑序34 分钟前
Java 8老系统SQL Agent实战:AI生成候选SQL,安全引擎拦截后再执行
java·人工智能·sql