本文出自伯特的《 LoulanPlan 》,转载务必注明作者及出处。
对于 Java 开发者而言,泛型是必须掌握的知识点。泛型本身并不复杂,但由于涉及的概念、用法较多,所以打算通过系列文章去讲解,旨在全面、通俗的介绍泛型及其使用。如果你是初学者,可以通过本文了解泛型,并满足企业级开发的需求;如果你对泛型已有一定的了解,可以通过本文进行巩固,加深对泛型的理解。
作为系列文章的第一篇,本文将带你了解 Java 泛型的前生今世,看看泛型的诞生之于开发者的意义。
对于集合框架中的 List
及其实现类,想必大家都不陌生。同时,泛型诞生之后即被广泛运用于 Java 集合框架。所以,我们就以 List
作为观察对象,看看在泛型诞生之前,Oracel 的工程师们是如何进行设计的。
摘自 JDK 1.4 的 List.java
源码:
public interface List extends Collection { //添加元素 boolean add(Object o); //查询元素 Object get(int index); }
可以看出 List
是通过 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
类型数据,显然是脏数据; ClassCastException
,安全性低; 问题还真不少!
上述问题究其根本,是无法限制数据类型引起的。也就是说,如果我们基于 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
类型数据,会在编译期进行类型检查时报错,导致编译失败; add()
方法的参数类型,所以不用担心在 get()
时内部强转会引发异常。 简直完美。同理,可以包装出一系列 StringList, LongList,以及自定义数据的集合包装类 PeopleList, DataList 等。
但人无完人,类亦无完类啊。包装类虽解决了编码上的数据类型问题,可在工程效率方面却捉襟见肘:
仍需努力!
虽然包装类存在缺陷,但其对于理解泛型思想是很有意义的。不知 Oracle 的工程师们,是否受此启发设计出的泛型呢?
如果你试着多写几个数据类型的包装类,就会发现各包装类之间的区别和联系:
既然如此,如果我们能够弱化数据类型,使其不再受具体的业务场景限制,就可以做到专注于通用的算法逻辑,从而提升复用性。
那么,如何弱化数据类型呢?有人说了,使用 Object 就很弱化啊。咳,麻烦你从头开始看。。。
JDK 5(即 JDK 1.4 之后的 1.5) 引入了 泛型(Generic Type)
的概念,其通过“参数化类型”实现数据类型的弱化,使得程序内部不需要关心具体的数据类型,而是让业务在调用时作为参数传入。泛型将传入的数据类型传递给编译器,这样编译器就可以在编译期间进行类型检查,确保程序的安全性,并且可以插入相应的强转以避免开发人员显示强转。
上面这段话值得多读几遍,尤其是“参数化类型”可以说是泛型的核心所在。如果还有点蒙没关系,继续往下看。
Java 中方法的声明大家都不陌生,如果某个方法需要对整数进行加法运算,我们可以在声明方法时添加整数类型的参数,外部调用时必须传入相应的整数数据。这里,将数据抽象为参数的过程,可以理解为“参数化实参”。
那么,“参数化类型”可以理解为是“参数化数据”的进一步抽象:将数据类型抽象为参数,即 类型形参 。如此一来,数据类型可以像形参一样,在调用时动态指定。如此,就达到了使用通用逻辑动态处理不同数据类型的目的。
下面,我们通过 JDK 源码中有关泛型的运用来巩固这一概念。
泛型诞生后,即对 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
的简单运用,可以看出引入泛型后集合不失普适性,依然可以针对各种类型对象进行操作。同时,泛型为集合框架增加了编译时类型安全性,并避免了在使用过程中的强转操作。
有关泛型的前生今世就介绍到这儿了。至此,我们通过相关示例一步步引出了泛型,了解了泛型诞生前后在一些编码场景下的差异。最后还通过实例简单使用了泛型,但泛型的运用远不止如此...
下一篇将进一步介绍泛型的各种运用场景,掌握泛型的用武之地。