泛型概述(上):特定与泛化

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

模板代码与泛型

泛型,可以看做一种“模板代码”。“模板代码”其实并不是一种时髦技术,很多语言都有自己的“模板代码”,比如C++也有“泛型”,不过人家叫模板类。

什么是“模板代码”呢?

ArrayList为例。我们在学习Java基础时,先学了数组,再接触集合。它们的分类是这样的:

|-Array

|-Collection

   |-List

      |-ArrayList

      |-LinkedList

   |-Set

      |-HashSet

      |-TreeSet

我们太习惯于把Array(数组)与Collection(集合)对立,以至于到最后甚至不知道Array(数组)和ArrayList有什么关联。

实际上,在List一脉中ArrayList是比较特殊的,ArrayList又称为“可变数组/动态数组”,ArrayList底层其实就是数组,只不过它的数组会自动扩容。

不论内部数组如何扩容,list引用不会受影响,始终指向ArrayList对象

也就是说:ArrayList = 数组 + 自动扩容

在JDK1.5引入泛型之前,ArrayList采取的方式是:在内部塞入一个Object[] array。

public class ArrayList {
    private Object[] array;
    private int size;
    public void add(Object e) {...}
    public void remove(int index) {...}
    public Object get(int index) {...}
}

如果用JDK1.5以前的ArrayList存储String类型,那么会有以下两个缺点(其实是问题的一体两面):

  • 需要强制转型
  • 强制转型容易出错

例如,代码必须这么写:

ArrayList list = new ArrayList();
list.add("Hello");
// 获取到Object,必须强制转型为String:
String first = (String) list.get(0);

为什么要强制转型?因为String是真正的类型,转型后才能使用String特有的方法,比如replace()。

OK,在确认必须强转的前提下,我们继续讨论。

强转会带来一个问题:很容易出现ClassCastException。

// JDK1.4可以这样做
list.add(new Integer(123));
// ERROR: ClassCastException:
String second = (String) list.get(1);

作为一种解决方法,JDK1.5之前的程序员可以为String类型单独编写一种ArrayList

public class StringArrayList {
    // 因为这种ArrayList只存String,所以不需要用Object[]兼容所有类型,只要String[]即可
    private String[] array;
    private int size;
    public void add(String e) {...}
    public void remove(int index) {...}
    public String get(int index) {...}
}

这样一来,存入和取出都被限定为String,且不需要强制转型,因为编译器会强制检查放入的类型:

StringArrayList list = new StringArrayList();
list.add("Hello");
String first = list.get(0);
// 编译错误: 不允许放入非String类型:
list.add(new Integer(123));

问题暂时解决。

然而,新的问题是,如果要存储Integer,还需要为Integer单独编写一种ArrayList

public class IntegerArrayList {
    private Integer[] array;
    private int size;
    public void add(Integer e) {...}
    public void remove(int index) {...}
    public Integer get(int index) {...}
}

如果还有其他类型,就要编写各种各样特定类型ArrayList

  • LongArrayList
  • DoubleArrayList
  • PersonArrayList
  • ...

这是不可能的,光JDK的class就有成千上万个,而且还不算普通Java用户编写的类。

为了解决新的问题,我们必须把ArrayList变成一种模板。

什么是模板呢?以设计模式中的模板方法模式为例:

/**
 * 验证码发送器
 *
 * @author qiyu
 * @date 2020-09-08 19:38
 */
public abstract class AbstractValidateCodeSender {

    /**
     * 生成并发送验证码
     */
    public void sendValidateCode() {
        // 1.生成验证码
        String code = generateValidateCode();

        // 2.把验证码存入Session
        // ....

        // 3.发送验证码
        sendCode();
    }

    /**
     * 具体发送逻辑,留给子类实现:发送邮件、或发送短信都行
     */
    protected abstract void sendCode();

    /**
     * 生成验证码
     *
     * @return
     */
    public String generateValidateCode() {
        return "123456";
    }

}

对于上面的模板,我们可以有多种实现方式:

/**
 * 短信验证码发送
 *
 * @author mx
 * @date 2023-11-21 21:44
 */
public class SmsValidateCodeSender extends AbstractValidateCodeSender {

    @Override
    protected void sendCode() {
        // 通过阿里云短信发送
    }
}
/**
 * QQ邮箱验证码发送
 *
 * @author mx
 * @date 2023-11-21 22:45
 */
public class EmailValidateCodeSender extends AbstractValidateCodeSender {

    @Override
    protected void sendCode() {
        // 通过QQ邮箱发送
    }
}

所谓模板,就是“我能做的都给你做了,少量易变动的东西我留出来,你自己DIY去”。

同理,ArrayList<T>也是一种模板,能写的方法都给你写了,但变量类型我定不了,于是抽成类型参数

public class ArrayList<T> {
    private T[] array;
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

T可以是任何class类型,反正我已经帮你参数化了,你自己定。

这样一来,我们就实现了:只需编写一次模版,可以创建任意类型的ArrayList

// 创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<>();
// 创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<>();
// 创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<>();

因此,泛型类就是一种模板类,例如ArrayList<T>,然后使用者可以自己选择将模板填充为什么类型:

// 嘿嘿,我想把ArrayList<T>填充为ArrayList<String>,专门收纳String类型
ArrayList<String> strList = new ArrayList<>();

你可以理解为此时ArrayList内部自动被赋值成这样(编译器层面):

public class StringArrayList {
    private String[] array;
    private int size;
    public void add(String e) {...}
    public void remove(int index) {...}
    public String get(int index) {...}
}

编译器针对类型作检查:

strList.add("hello"); // OK
String s = strList.get(0); // OK,因为上面add()保证了只能添加String类型,所以无需强制转型
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!

这样一来,既实现了编写一次万能匹配,又能通过编译器保证类型安全:这就是泛型。

形式类型参数、实际类型参数

需要说明的是,泛型是一种技术,而不是单指ArrayList<T>中的T。像ArrayList<T>的T,Map<K, V>中K和V,统称类型参数(Type Parameter),也叫形式类型参数,它只是泛型这个技术的组成部分。

使用泛型时,比如ArrayList<String>,T被替换为String,可以看做是对T的“赋值”,这里的String称为实际类型参数(actual type parameter)。

实际类型参数用来为形式类型参数赋值,把ArrayList<T>由泛化通用的模板变为特定类型的类。你可以把泛型理解为:变量是对数据的抽取,而泛型是对变量类型的抽取,抽取成类型参数,抽象层次更高。

上面我用ArrayList举例说明了什么是代码模板,接下来我从实际开发场景切入,从另一个角度聊聊什么是类型参数。

举个例子,当你看到同事A写了以下代码:

// 获取教师列表
public List<User> listTeachers() {
    return jdbcTemplate.execute("select * from t_user where user_type=1");
}

// 获取学生列表
public List<User> listStudents() {
    return jdbcTemplate.execute("select * from t_user where user_type=2");
}

你肯定会下意识地建议他:哦,我的天哪,你应该把userType提取为方法参数:

public List<User> listUser(Integer userType) {
    return jdbcTemplate.execute("select * from t_user where user_type=" + userType);
}

从某种程度来说,把SQL中的user_type=1、user_type=2提升为方法入参,就是为了通用性,解决了硬编码(hard code)问题。但在绝大部分初学者的认知里,对于:

  • 1
  • 2
  • "a"
  • "b"

他们往往只能达到以下层次:

  • private Integer value
  • private String value

对于Java这种语言来说,一个变量其实应该至少包含两部分(访问权限暂不讨论):

  • 变量类型
  • 变量值

大部分人只能想到抽取变量,无法达到抽取变量类型的层次。那怎么才能达到抽取变量类型的层次呢?或者说,什么场景下需要抽取变量类型呢?

假设你有一天你发现同事A又在写bug:

public final class MapUtil {
    // 私有构造
    private MapUtil() { }

    // 把userList转为userMap
    public static Map<Long, User> listToMap(List<User> list) {
        if (CollectionUtils.isEmpty(list)) {
            return Collections.emptyMap();
        }
        Map<Long, User> userMap = Maps.newHashMap();
        for (User user : list) {
            userMap.put(user.getId, user);
        }
        return userMap;
    }

    // 把departmentList转为departmentMap
    public static Map<Long, Department> listToMap(List<Department> list) {
        if (CollectionUtils.isEmpty(list)) {
            return Collections.emptyMap();
        }
        Map<Long, Department> departmentMap = Maps.newHashMap();
        for (Department department : list) {
            departmentMap.put(department.getId, department);
        }
        return departmentMap;
    }
}

你看到上面的代码,又开始阴阳怪气地说:哦,我的天哪,你应该...

“闭上你的嘴,我TMD知道要用泛型!同事A愤怒地骂道。只见他在你面前飞快地重构MapUtil:

public final class MapUtil {
    private MapUtil() { }

    public static <V, K> Map<K, V> listToMap(List<V> list, Function<V, K> keyExtractor) {
        if (CollectionUtils.isEmpty(list)) {
            return Collections.emptyMap();
        }
        Map<K, V> res = Maps.newHashMap();
        for (V v : list) {
            K k = keyExtractor.apply(v);
            if (k == null) {
                continue;
            }
            res.put(k, v);
        }
        return res;
    }
}

重构后的代码,和原先的两个方法在结构上几乎一模一样(忽略keyExractor这个函数式接口),只是变量类型换成了类型参数,即“对变量类型进行抽取”(所以在泛型里,List<T>中的T叫类型参数),而代码也更加通用了。

把变量类型抽取成类型参数T构造出模板代码,再通过实际类型参数赋值(比如ArrayList<T>变成ArrayList<User>),把类型特定化,最后配合编译器在编译期对相关操作的变量类型进行约束,这就是泛型。抽取变量,我们早就习以为常,但抽取变量类型,却从未听说。这也是初学者觉得泛型抽象的根本原因。

最后,强调一下,泛型是对引用类型的抽取,基本类型是无法抽取的,必须是确定的。

源码解析:ArrayList与泛型

这篇文章主要是为了告诉大家一个概念:“泛型是实现模板代码的一种手段”。文章中经过一次次演化,我们的ArrayList最终变成了这样:

public class ArrayList<T> {
    private T[] array; // 我们以为ArrayList<T>内部会有个T[] array
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

但泛型数组其实是非常特殊的,Java并不能直接实现泛型数组。ArrayList内部实际上仍然沿用了之前的Object[]。

那么,ArrayList为什么还能进行类型约束和自动类型转换呢?

请看下一篇。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我! 基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip
毕设新项目基于python3.7+django+sqlite开发的学生就业管理系统源码+使用说明(含vue前端源码).zip 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我! 学生就业管理系统(前端) ## 项目开发环境 - IDE: vscode - node版本: v12.14.1 - npm版本: 6.13.4 - vue版本: @vue/cli 4.1.2 - 操作系统: UOS 20 ## 1.进入项目目录安装依赖 ``` npm install ``` ## 2.命令行执行进入UI界面进行项目管理 ``` vue ui ``` ## 3.编译发布包(请注意编译后存储路径) #### PS:需要将编译后的包复制到后端项目的根目录下并命名为'static' 学生就业管理系统(后端) ## 1.项目开发环境 - IDE: vscode - Django版本: 3.0.3 - Python版本: python3.7.3 - 数据库 : sqlite3(测试专用) - 操作系统 : UOS 20 ## 2.csdn下载本项目并生成/安装依赖 ``` pip freeze > requirements.txt pip install -r requirements.txt ``` ## 3.项目MySQL数据库链接错误 [点击查看解决方法](https://www.cnblogs.com/izbw/p/11279237.html)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值