深入浅出 Java 泛型之(一):前生今世
本文出自伯特的《LoulanPlan》,转载务必注明作者及出处。
对于 Java 开发者而言,泛型是必须掌握的知识点。泛型本身并不复杂,但由于涉及的概念、用法较多,所以打算通过系列文章去讲解,旨在全面、通俗的介绍泛型及其使用。如果你是初学者,可以通过本文了解泛型,并满足企业级开发的需求;如果你对泛型已有一定的了解,可以通过本文进行巩固,加深对泛型的理解。
作为系列文章的第一篇,本文将带你了解 Java 泛型的前生今世,看看泛型的诞生之于开发者的意义。
1. 泛型之前:通用数据类型
对于集合框架中的 List
及其实现类,想必大家都不陌生。同时,泛型诞生之后即被广泛运用于 Java 集合框架。所以,我们就以 List
作为观察对象,看看在泛型诞生之前,Oracel 的工程师们是如何进行设计的。
摘自 JDK 1.4 的 List.java
源码:
public interface List extends Collection {
//添加元素
boolean add(Object o);
//查询元素
Object get(int index);
}
可以看出 List
是通过 Object
类型管理的数据,如此设计的好处显而易见:
具备通用性,因为所有的类都是 Object 的直接或间接子类,所以适用于任意类型的对象。
同时,弊端也是不可忽视的。下面就通过使用 List
存、取数据来看看都有哪些问题:
//构造对象
List list = new ArrayList();
//存
list.add(1);
list.add("2");//①
//取
int num1 = (int)list.get(0);
int num2 = (int)list.get(1);//②
由于使用 Object
,编译器无法判断存、取数据的实际类型,导致上述几行代码暴露出许多问题:
-
无法限制存储数据类型,不够健壮:在 ① 处可以添加
String
类型数据,显然是脏数据; - 取出时强转代码冗余,可读性差:取出数据时必须显示强转为 int 类型;
- 由于 ① 处在编译时无法检查出错误,导致 ② 处的强转在运行时引发
ClassCastException
,安全性低;
问题还真不少!
2. 泛型萌芽:数据类型的包装
上述问题究其根本,是无法限制数据类型引起的。也就是说,如果我们基于 List
包装出相应类型的 XxxList
,就可以解决问题。
举个例子,包装用于存储 Integer
数据类型的 IntegerList
:
public class IntegerList {
List list = new ArrayList();
//限制外部只能添加整型数据
public boolean add(Integer data) {
return list.add(data);
}
//内部进行强转,调用者可以直接赋值为整型
public Integer get(int index) {
return (Intrger)list.get(index);
}
}
包装内依然使用 List
管理数据,但我们对外暴露的接口限制了数据类型,规避了直接访问 List
的接口可能引发的问题。
下面一起来看看如何使用包装类:
//构造对象
IntegerList list = new IntegerList();
//存
list.add(1);
list.add("2");//①
//取
int num1 = list.get(0);
怎么样,一个包装类轻松解决问题:
- 在 ① 处试图添加
String
类型数据,会在编译期进行类型检查时报错,导致编译失败; - 在取出数据时,无需重复强转,直接赋值给 int 类型的数据;
- 因为限制了
add()
方法的参数类型,所以不用担心在get()
时内部强转会引发异常。
简直完美。同理,可以包装出一系列 StringList, LongList,以及自定义数据的集合包装类 PeopleList, DataList 等。
但人无完人,类亦无完类啊。包装类虽解决了编码上的数据类型问题,可在工程效率方面却捉襟见肘:
- 复用性低:每一个包装类只适用于一种数据类型,无法复用核心逻辑;
- 维护成本高:复用性低必然会增加后期维护的成本。
仍需努力!
3. 泛型登场:参数化类型
虽然包装类存在缺陷,但其对于理解泛型思想是很有意义的。不知 Oracle 的工程师们,是否受此启发设计出的泛型呢?
如果你试着多写几个数据类型的包装类,就会发现各包装类之间的区别和联系:
- 区别:数据类型不同;
- 联系:操作数据的方法相同,即核心算法逻辑是一致的。
既然如此,如果我们能够弱化数据类型,使其不再受具体的业务场景限制,就可以做到专注于通用的算法逻辑,从而提升复用性。
那么,如何弱化数据类型呢?有人说了,使用 Object 就很弱化啊。咳,麻烦你从头开始看。。。
JDK 5(即 JDK 1.4 之后的 1.5) 引入了 泛型(Generic Type)
的概念,其通过“参数化类型”实现数据类型的弱化,使得程序内部不需要关心具体的数据类型,而是让业务在调用时作为参数传入。泛型将传入的数据类型传递给编译器,这样编译器就可以在编译期间进行类型检查,确保程序的安全性,并且可以插入相应的强转以避免开发人员显示强转。
上面这段话值得多读几遍,尤其是“参数化类型”可以说是泛型的核心所在。如果还有点蒙没关系,继续往下看。
Java 中方法的声明大家都不陌生,如果某个方法需要对整数进行加法运算,我们可以在声明方法时添加整数类型的参数,外部调用时必须传入相应的整数数据。这里,将数据抽象为参数的过程,可以理解为“参数化实参”。
那么,“参数化类型”可以理解为是“参数化数据”的进一步抽象:将数据类型抽象为参数,即类型形参。如此一来,数据类型可以像形参一样,在调用时动态指定。如此,就达到了使用通用逻辑动态处理不同数据类型的目的。
下面,我们通过 JDK 源码中有关泛型的运用来巩固这一概念。
4. 泛型的简单运用
泛型诞生后,即对 Java 集合框架进行了大刀阔斧的修改,引入了泛型。下面仍然以 List
作为观察对象,看看泛型带来了哪些改变。
//摘自 JDK 5 版本的 List 源码
public interface List<E> extends Collection<E> {
//添加元素
boolean add(E e);
//指定下标查询元素
E get(int index);
//指定下标移除元素
E remove(int index);
}
可以看出,List<E>
通过在类 List
后追加 <>
标识其为泛型类,包含的元素 E
即“类型形参“,以支持开发者在使用时指定实际类型。下面看看在代码中如何使用泛型 List
:
//构造对象
List<Integer> list = new ArrayList();
//存
list.add(1);
list.add("2");//①
//取
int num1 = list.get(0);
int num2 = list.get(1);
首先,我们构造了 List<Integer>
类型的对象,所以在运行时 List<E>
中的形参会被当做 Integer
去出处理,我们可以想象出一个虚拟的 List
类:
public interface List extends Collection<E> {
boolean add(Integer e);
Integer get(int index);
Integer remove(int index);
}
接下来,和文章开头一样,我们对集合进行了相关操作,可以看出使用泛型解决了我们之前遇到的所有问题:
- ① 处的代码在编译期间会出错:由于声明的是
Integer
类型的List
,显然无法接收String
类型的数据。 - 从虚拟
List
可以知道,取出元素时不需要显示强转,自然也不会在运行时抛出异常。
通过对泛型 List
的简单运用,可以看出引入泛型后集合不失普适性,依然可以针对各种类型对象进行操作。同时,泛型为集合框架增加了编译时类型安全性,并避免了在使用过程中的强转操作。
5. 总结
有关泛型的前生今世就介绍到这儿了。至此,我们通过相关示例一步步引出了泛型,了解了泛型诞生前后在一些编码场景下的差异。最后还通过实例简单使用了泛型,但泛型的运用远不止如此...
下一篇将进一步介绍泛型的各种运用场景,掌握泛型的用武之地。