List<E>的尖括号是什么意思?
什么是泛型?如何去理解它?
泛型有什么优势?面试
以上问题能够在本文中找到答案。算法
在全部编程语言中,都有这么一种东西,叫作“数组”,好比C++中就有数组。编程
可是呢,传统数组最大的缺点就是:它的长度是固定的,若是不知道一个数组须要容纳多大的数据量,编写程序时就会很困难,数组建的太大了,就会浪费内存资源,数组过小了,容量不够就会报错。segmentfault
因此,前人发明了各类各样的大小可变的数组,也就是“集合”,Collection。数组
Java集合类型一览(选自《HeadFirstJava》):安全
正由于集合没有固定的大小,而且有着许多很方便的API,所以集合被普遍使用。(今后之后,原始的数组就只出如今大学的C++课堂之中了)数据结构
众多的集合中,较为常见的就是ArrayList,咱们就拿它来举例。dom
假如,须要一个集合来储存一些字符串,而后遍历输出它们:编程语言
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>有什么做用呢?ide
若是把刚才的<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)
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 ArrayList<String>
// 声明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>>>
那么怎么理解呢?就像洋葱“同样一层一层的剥开它的心”。
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);
至此,一个集合套娃已完成。
见到泛型别烦恼,
这个功能很是好。
泛型若想用得好,
关键在于尖括号。
若是用函数来类比: