深度思考:为什么需要泛型?

简介: 深度思考:为什么需要泛型?

不知道大家平时在进行后端编程的时候有没有考虑过一个概念:泛型编程,就像面向对象、面向接口编程一样,很常用以致于用成为了大家广泛的习惯,在后端常用编程语言中,无论是Java、C++都支持泛型编程,而在2022年的3月份,随着Go1.18稳定版本的发布,Go自1.18版本起,也支持了“泛型(Generics)”这一特性,这同时也是Go语言在语法层面的一次重大改变。

虽然之前在使用Java进行编程时经常用到泛型,但是未曾思考过到底为什么需要泛型?没有泛型会怎样?泛型带来了什么作用?泛型的实现原理是怎样的?等等问题。

因为Go1.18版本发布已有几个月的时间,各个IDE也陆续支持Go语言泛型编码,因此也通过一些资料学习了Go语言泛型这个新特性,并且对此做了一些思考,想以一篇文章来向大家分享自己的思考经验和见解,同时也会以实际代码的方式使用Java、Go语言的泛型特性,剖析其原理,下面开始正文。

1 什么是泛型?

维基百科提到:最初泛型编程这个概念来自于缪斯·大卫和斯捷潘诺夫. 亚历山大合著的“泛型编程”一文。那篇文章对泛型编程的诠释是:“泛型编程的中心思想是对具体的、高效的算法进行抽象,以获得通用的算法,然后这些算法可以与不同的数据表示法结合起来,产生各种各样有用的软件”。说白了就是将算法与类型解耦,实现算法更广泛的复用。

在我看来,泛型是同接口类似,也是编程的一种规范也可以说是一种风格,泛型编程可以让开发者在编写代码时约束变量、容器、对象、结构体的类型,对类型清晰的掌握可以减少bug的产生,增强代码的可读性,让抽象变得更加具体和实用。基于泛型的程序,由于传入的参数不同,程序会实现不同的功能。这也被叫做一种多态现象,叫做参数化多态(Parametric Polymorphism)。

2 编程语言中泛型编程的实例

2.1 Java泛型编程

请移步这篇文章《玩转Java泛型》

2.2 Go泛型编程
package main
import "fmt"
type MyList[T any] struct {
   Items []Item[T]
}
type Item[T any] struct {
   Index int
   Value T
}
func (list *MyList[T]) AddItem(i T) {
   item := Item[T]{Value: i, Index: len(list.Items)}
   list.Items = append(list.Items, item)
}
func (list *MyList[T]) GetItem(index int) T {
   l := list.Items
   var val T
   for i := range l {
      if l[i].Index == index {
         val = l[i].Value
      }
   }
   return val
}
func (list *MyList[T]) Print() {
   for i := range list.Items {
      fmt.Println(list.Items[i])
   }
}
type MyHashMap[K comparable, V any] struct {
   Value map[K]V
}
func (m *MyHashMap[K, V]) SetValue(k K, v V) {
   m.Value[k] = v
}
func (m *MyHashMap[K, V]) GetValue(k K) V {
   return m.Value[k]
}
func (m *MyHashMap[K, V]) Print() {
   for k := range m.Value {
      fmt.Println(k, m.Value[k])
   }
}
func main() {
   list := MyList[int]{}
   list.AddItem(1)
   list.AddItem(2)
   item := list.GetItem(7)
   list.Print()
   hashMap := MyHashMap[string, int]{map[string]int{"A": 1, "B": 2}}
   hashMap.SetValue("s", 2)
   value := hashMap.GetValue("s")
   hashMap.Print()
}

具体Go泛型编程内容可以看下这篇文章哈:《一文搞懂Go1.18泛型新特性》

3 为什么需要泛型?

回答这个问题之前,我们不妨思考下,在一些场景下如果没有泛型会怎样:

public class Main {
    static class Score {
        String name;
        int num;
        public Score(String name, int num) {
            this.name = name;
            this.num = num;
        }
    }
    public static int getSum(List<Score> scores) {
        int sum = 0;
        for (Score score : scores) {
            sum += score.num;
        }
        return sum;
    }
    public static void main(String[] args) {
        List<Score> scores = Arrays.asList(
                new Score("zs", 100),
                new Score("ls", 80),
                new Score("ww", 90.5) //编译不通过
        );
        int sum = getSum(scores);
        System.out.println(sum);
    }
}

没有泛型时解决上述问题:

public class Main {
    static class Score {
        String name;
        int num;
        public Score(String name, int num) {
            this.name = name;
            this.num = num;
        }
    }
    static class Score2 {
        String name;
        float num;
        public Score2(String name, float num) {
            this.name = name;
            this.num = num;
        }
    }
    public static int getIntSum(List<Score> scores) {
        int sum = 0;
        for (Score score : scores) {
            sum += score.num;
        }
        return sum;
    }
    public static float getFloatSum(List<Score2> scores) {
        float sum = 0;
        for (Score2 score : scores) {
            sum += score.num;
        }
        return sum;
    }
    public static void main(String[] args) {
        List<Score> scores = Arrays.asList(
                new Score("zs", 100),
                new Score("ls", 80)
        );
        List<Score2> scores2 = Arrays.asList(
                new Score2("zs", 89.5f),
                new Score2("ls", 80.5f)
        );
        int sum = getIntSum(scores);
        float sum2 = getFloatSum(scores2);
        System.out.println(sum+sum2);
    }
}

接下来我们引入泛型:

public class Main {
    static class Score<T> {
        String name;
        T num;
        public Score(String name, T num) {
            this.name = name;
            this.num = num;
        }
    }
    public static int getIntSum(List<Score<Integer>> scores) {
        int sum = 0;
        for (Score<Integer> score : scores) {
            sum += score.num;
        }
        return sum;
    }
    public static float getFloatSum(List<Score<Float>> scores) {
        float sum = 0;
        for (Score<Float> score : scores) {
            sum += score.num;
        }
        return sum;
    }
    public static void main(String[] args) {
        List<Score<Integer>> scores = Arrays.asList(
                new Score("zs", 100),
                new Score("ls", 80)
        );
        List<Score<Float>> scores2 = Arrays.asList(
                new Score("zs", 89.5f),
                new Score("ls", 80.5f)
        );
        int sum = getIntSum(scores);
        float sum2 = getFloatSum(scores2);
        System.out.println(sum+sum2);
    }
}

所以,使用泛型的原因:

  • 泛化
  • 类型安全
  • 消除强制类型转换
  • 向后兼容

图示:

4 总结泛型的实现原理

大多数静态类型语言的泛型实现都是在编译期进行,也就是编译的前端实现,主要的技术包括类型擦除、具体化和基于元编程等进行的,比如Java的泛型就是基于类型擦除实现,在编译前端进行类型检查即可,编译之后的字节码不管有没有泛型都是一样的,运行时也是如此。而Go语言的泛型实现则不同,Go使用类似于具体化的方式实现泛型,就是在运行时使用类型信息,根据类型参数创建不同的具体类型的变量。

参考:

https://time.geekbang.org/column/article/485140

https://baike.baidu.com/item/%E6%B3%9B%E5%9E%8B/4475207?fr=aladdin

https://time.geekbang.org/column/article/283229

相关文章
|
Java 调度
多线程之线程池的七个参数
多线程之线程池的七个参数
528 0
|
1月前
|
存储 缓存 监控
JVM 运行时数据区全解:从底层原理到 OOM 根因定位全链路实战
JVM运行时数据区是Java内存管理的核心,分为线程私有区域(程序计数器、虚拟机栈、本地方法栈)和线程共享区域(堆、方法区)。不同区域有明确的OOM触发规则:堆内存不足引发Java heap space异常,元空间不足导致Metaspace异常,直接内存溢出表现为Direct buffer memory错误。排查OOM需结合异常类型、堆dump、GC日志等现场数据,使用MAT等工具分析内存泄漏点。
458 1
|
缓存 API 数据库
GraphQL(一)基础介绍及应用示例
本文为GraphQL的基础介绍及应用示例,主要介绍GraphQL的应用场景、优缺点及基础语法与使用。 GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。
|
SQL Oracle 关系型数据库
案例分析:你造吗?有个ORA-60死锁的解决方案
这段时间应用一直被一个诡异的 ORA-00060 的错误所困扰,众所周知,造成 ORA-00060 的原因是由于应用逻辑,而非 Oracle 数据库自己,之所以说诡异(“诡异”可能不准确,只能说这种场景,以前碰见的少,并未刻意关注),是因为这次不是常见的,由于读取数据顺序有交叉,导致ORA-0006.
2867 0
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
本文深入剖析了Redisson中可重入锁的释放锁Lua脚本实现及其获取锁的两种方式(阻塞与非阻塞)。释放锁流程包括前置检查、重入计数处理、锁删除及消息发布等步骤。非阻塞获取锁(tryLock)通过有限时间等待返回布尔值,适合需快速反馈的场景;阻塞获取锁(lock)则无限等待直至成功,适用于必须获取锁的场景。两者在等待策略、返回值和中断处理上存在显著差异。本文为理解分布式锁实现提供了详实参考。
517 11
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
|
6月前
|
IDE Java Maven
使用mvn generate-sources生成在target目录下的代码和类应该如何调用
Maven项目中,执行`mvn generate-sources`后,生成代码位于`target/generated-sources`。该目录会自动加入编译类路径,Maven后续阶段可直接编译。IDE(如IntelliJ IDEA)通常自动识别为源码根目录,若未识别,可刷新Maven项目即可正确调用生成代码。
405 7
|
7月前
|
XML Java 数据格式
Bean的生命周期:从Spring的子宫到坟墓
Spring 管理 Bean 的生命周期,从对象注册、实例化、属性注入、初始化、使用到销毁,全程可控。Bean 的创建基于配置或注解,Spring 在容器启动时扫描并生成 BeanDefinition,按需实例化并填充依赖。通过 Aware 回调、初始化方法、AOP 代理等机制,实现灵活扩展。了解 Bean 生命周期有助于更好地掌握 Spring 框架运行机制,提升开发效率与系统可维护性。
|
存储 SQL 关系型数据库
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log、原理、写入过程;binlog与redolog区别、update语句的执行流程、两阶段提交、主从复制、三种日志的使用场景;查询日志、慢查询日志、错误日志等其他几类日志
1117 35
MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log
|
存储 安全 Java
Java——String类详解
String 是 Java 中的一个类,用于表示字符串,属于引用数据类型。字符串可以通过多种方式定义,如直接赋值、创建对象、传入 char 或 byte 类型数组。直接赋值会将字符串存储在串池中,复用相同的字符串以节省内存。String 类提供了丰富的方法,如比较(equals() 和 compareTo())、查找(charAt() 和 indexOf())、转换(valueOf() 和 format())、拆分(split())和截取(substring())。此外,还介绍了 StringBuilder 和 StringJoiner 类,前者用于高效拼接字符串,后者用于按指定格式拼接字符串
1604 1
Java——String类详解
|
存储 SQL 关系型数据库
面试官:你能聊聊 binlog、undo log、redo log 吗?
本文详细解析了MySQL数据库中的三种日志:binlog、undo log和redo log。binlog用于记录数据库的所有表结构变更及数据修改,支持归档、主从复制和数据恢复;undo log用于事务回滚,确保事务的原子性和实现多版本控制;redo log则用于crash-safe,确保数据库异常重启后已提交记录不丢失。文章通过实例和图表,深入浅出地介绍了每种日志的特点、应用场景及其实现机制。适合数据库开发者和运维人员阅读。
976 2