作者简介:大家好,我是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
进群,大家一起学习,一起进步,一起对抗互联网寒冬