零、前言
List<E>的尖括号是什么意思?
什么是泛型?如何去理解它?
泛型有什么优点?
以上问题可以在本文中找到答案。
一、基础知识
在所有编程语言中,都有这么一种东西,叫做“数组”,比如C++中就有数组。
但是呢,传统数组最大的缺点就是:它的长度是固定的,如果不知道一个数组需要容纳多大的数据量,编写程序时就会很困难,数组建的太大了,就会浪费内存资源,数组太小了,容量不够就会报错。
所以,前人发明了各种各样的大小可变的数组,也就是“集合”,Collection。
Java集合类型一览(选自《HeadFirstJava》):
正因为集合没有固定的大小,并且有着许多很方便的API,因此集合被广泛使用。(从此以后,原始的数组就只出现在大学的C++课堂之中了)
众多的集合中,较为常见的就是ArrayList,我们就拿它来举例。
假如,需要一个集合来储存一些字符串,然后遍历输出它们:
public static void main(String[] args) {
// new对象,初始化
ArrayList<String> array = new ArrayList<String>();
// 加入元素
array.add("ABC");
array.add("DEF");
array.add("Test");
array.add("Debug");
//遍历输出
for (String i: array) {
System.out.println(i);
}
}
那么问题来了,ArrayList<String>中的<String>有什么作用呢?
如果把刚才的<String>去掉或者删掉,会发生什么呢?
- 把尖括号里面改成Double的包装类,但传入的还是String:
public static void main(String[] args) {
// 此处的集合是Double类型
ArrayList<Double> array = new ArrayList<Double>();
array.add("ABC");
array.add("DEF");
array.add("Test");
array.add("Debug");
for (String i: array) {
System.out.println(i);
}
}
答案是:无法通过编译,传入的类型与规定类型不匹配。
方法 Collection.add(Double)不适用
(参数不匹配; String无法转换为Double)
- 把尖括号里面去掉,但传入的还是String:
public static void main(String[] args) {
// 此处的集合没有声明类型
ArrayList array = new ArrayList();
array.add("ABC");
array.add("DEF");
array.add("Test");
array.add("Debug");
for (String i: array) {
System.out.println(i);
}
}
答案是:仍然无法通过编译,没有进行传入类型的安全检查。
我们可以从中看出一些规律:尖括号<>里面的内容,似乎是对这个集合的类型做了规定。
那为什么没有了这个尖括号,还是不能通过编译呢?
理论上,没有条件的约束,应该可以储存任何类型的数据啊?
就像这样:
public static void main(String[] args) {
// 没有类型约束
ArrayList array = new ArrayList();
// 字符串
array.add("ABC");
// 整数型
array.add(123);
// 浮点数
array.add(123.456);
// 对象
array.add(new Object());
for (String i: array) {
System.out.println(i);
}
}
当然,上面的代码是错的,100%不能通过编译。
假设能编译,那么问题又出现了:在不知道某个元素的类型的情况下,怎么使用统一的方法去处理它呢?比如Java内置的Print可以输出字符串,但怎么用System.out.println(i)来打印一个对象的内容呢?
所以这种约束条件,最大的好处就是增加了安全性,就好比不同口径的瓶子,只能装合适的物品,String类型的瓶子只能装String类型的对象
这就是今天的话题,泛型。
二、初识泛型
泛型是什么?从字面意思解释:“泛”是广泛的,“型”是一种特定类型,连起来就是“广泛的特定类型的对象”。
说它广泛,无论是什么对象,是只要符合规定的类型,集合(ArrayList)就可以处理它;
说它特定,是因为它必须严格符合约定的类型,否则就会被编译器拦截下来,无法通过编译。
如果说集合是各种瓶子,那么泛型就是对于瓶子的约束。这种约束是为了安全,如果没有了泛型的约束,集合就可以容纳任何类型的对象,啥都可以装进去,就像把绵羊放进老虎的集合中。
那么,怎么使用泛型呢?
首先,不同于方法的参数,参数是针对于某个方法来说的,而泛型是针对一个类或对象来说的。
再解释一下就是:把一个参数传给某个方法,这个方法接收到参数之后,就可以处理它;把一个泛型传给某个类,这个类接收到这个泛型之后,就可以new出一个只能处理这个泛型的对象。
所以泛型和参数有相似之处,关键点就是在尖括号<>里加上类。
泛型可以用在集合中:
- new一个对象:
new ArrayList<String>
- new对象并声明变量:
// 声明stringList变量是字符串类型的集合
// 并且链接到一个new出来的对象上
ArrayList<String> stringList = new ArrayList<String>;
泛型还能用在方法的参数中:
// 此方法接收的参数是List集合,
// 但必须是字符串类型的集合才可以
public void printString (List<String> list) {
...
}
为一个集合添加数据:
// 添加: add
// 删除: remove
// 其他用法请参考源码,写的很清楚
array.add("ABC");
三、深入泛型——原理
基本的用法说完了,那么就来看看,泛型的世界里有什么规律。
在之前的《从零起步,真正理解Javascript回调函数》中说到:
程序 = 数据结构 + 算法
如果执行一个写死的程序(比如输出HelloWorld),那么无论怎样运行,结果都是一样的。这样的程序是没有意义的。
那么,如果想让某个方法发挥作用,就要让它是可以“变化”的,如果把输出的数据单独拿出来,作为函数的参数,而方法不变,那么就可以根据不同的参数,用同样的方法输出不同的结果。
这就是函数。
反之,如果待处理的数据不变,而处理数据的方法改变,把方法作为参数,就有了回调函数。
以上两种变化都是对于函数的,而泛型是对于类来说的。
我们看一看ArrayList的源码(部分):
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
public E get(int index) {
Objects.checkIndex(index, this.size);
return this.elementData(index);
}
public E set(int index, E element) {
Objects.checkIndex(index, this.size);
E oldValue = this.elementData(index);
this.elementData[index] = element;
return oldValue;
}
}
可以看到,这个类的许多方法里面,都有一个E,这个E是就是泛型,它不是一个具体的类型,而是在new对象的时候,传入什么泛型,就是用什么泛型。
在类定义的时候,有:
ArrayList<E>
既然定义的时候是E,下面的方法里也是E,那么,在初始化ArrayList的时候,传入什么泛型,下面的E就会变化成什么泛型,比如
ArrayList<String> stringList = new ArrayList<String>;
此时传入的是String,那么上面的代码,等同于发生了如下变化:
// 所有的 E 都被替换为特定的类型
public class ArrayList<String> extends AbstractList<String> implements List<String>, RandomAccess, Cloneable, Serializable {
public String get(int index) {
Objects.checkIndex(index, this.size);
return this.elementData(index);
}
public String set(int index, String element) {
Objects.checkIndex(index, this.size);
String oldValue = this.elementData(index);
this.elementData[index] = element;
return oldValue;
}
}
此时,这个被new出来的集合对象,就只能处理字符串类型的数据了。
(有没有感觉像函数重载?泛型就可以想象成类的重载,而且不用手动写重载代码)
知道了泛型的原理,也就很容易明白一个道理:
泛型并非只能用于数组和集合中,我们也可以创建使用泛型的类,只不过由于Java的API很丰富,没有必要再去自己写了。
四、最终篇 集合套娃
已经讲了很多知识,接下来来实践一下,如果能明白这个实例,就真正理解泛型了。
有这么一道面试题:
// 要求:给变量赋值
List<Map<String,List<String>>> list = newArrayList<Map<String,List<String>>>();
要理解这道题,我们还需要一些知识的补充。
Java集合类型一览(选自《HeadFirstJava》):
集合有这么多种,但是有些是类,有些是接口。
众所众知,接口是不能New的,所以题目中的List和Map都是不能new的,但我们可以new它们的实现类,然后赋值给这个类型的变量。
接着看题,变量名是list,类型是List集合,泛型是<Map<String,List<String>>>
那么怎么理解呢?就像洋葱“一样一层一层的剥开它的心”。
- 最外层,是一个List,它的泛型是<Map>
- 第二层,是一个Map,它的泛型是<String, List>
- 最里层,又是一个List,它的泛型是<String>
Map为什么有两个泛型呢?
请参考函数的两个参数,Map就相当于两个参数的函数。
由于Map具有特殊性,它是“键值对”,Map的每一个“值”都必须有一个唯一的“键”,所以要写成 Map<键,值>。
知道了它的组成,就可以去给它赋值了,只不过赋值的过程相反,是从里到外的,先创建里面,再逐层创建外面。
第一步,创建最里面的List:
// new一个ArrayList对象
List inside = new ArrayList<String> ();
// 添加一个String元素
inside.add("This is test String");
第二步,把这个有值的List,放到一个Map中
// new一个HashMap对象
Map middle = new HashMap<String, List<String>> ();
// 添加第一步的List元素
middle.add("key", inside);
这样就得到了一个有值的Map。
第三步,把这个Map赋值给外层的List。
// new一个ArrayList对象,这就是题目中的list变量
List list = new ArrayList< Map< String, List<String> > >();
// 添加第二步的Map元素
outside.add(middle);
至此,一个集合套娃已完成。
总结
见到泛型别烦恼,
这个功能非常好。
泛型若想用得好,
关键在于尖括号。
如果用函数来类比:
- 函数是使用相同的方法,通过改变数据来产生不同结果。
- 回调是使用相同的数据,通过改变方法来产生不同的结果。
- 泛型是使用相同的类,通过改变对象的类型来产生不同的结果。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。