集合框架指的是容器类。Java中大量持有对象的方式有数组和容器类两种方式。数组相较于容器类的优点在于:①随机访问效率高:由于是连续的存储空间,可以计算地址直接访问 ②类型确定:数组在创建时即可确定元素的具体类型 ③可存储基本数据类型。早期容器类默认存储Object类对象,这样无法在编译期对放入容器的元素类型进行检查,取出元素时也必须强制向下转型,而这不一定会成功。随着泛型的引入,容器类也可以声明确定的数据类型了。并且,由于自动包装机制,容器类也可以存储基本数据类型,而不是必须存储包装器类型的数据和引用类型数据。所以,数组的优势只剩下效率。如果效率高于灵活性,选择数组;否则,请选择容器类。所有的容器类尺寸都可以自动增长,但是牺牲了一定的效率。一般而言,优先使用容器类,以获取最大的灵活性。
容器类主要分为集合类和映射类:集合类存储的是单一的数据元素,比如一组对象。映射类存储的是相关联的键-值对,可以通过相应的方法获得键值对的键集合视图、值集合视图和键值对集合视图。对应这两个类,集合框架的顶层接口分别为Collection接口和Map接口。
集合类的继承层次为:
主要有三个继承的接口:Set:特点是没有重复元素;List:相当于线性表,保证了线性序列的特点;Queue:先进先出的队列。
映射类的继承层次为:
主要有HashMap、TreeMap和LinkedHashMap三个派生的接口;Hashtable一般由HashMap替代,不建议使用。映射类Map又称为关联数组或字典,它可以将一组对象映射到另一组对象中(不要求类型相同)。
以上实线表示继承(extends),虚线表示实现(implement)。集合类和映射类的继承层次加在一起,构建了完整的集合框架。
集合框架的设计理念:提供一套“小而美”的API。API需要对程序员友好,增加新功能时能让程序员们快速上手。
为实现这个设计理念,需要实现以下几点:
一、保证最顶层的核心接口足够小:最顶层的接口(也就是Collection与Map接口)只需要实现一个集合应该具备的基本操作即可。最顶层的接口并不会区分该集合是否可变(mutability),是否可更改(modifiability),是否可改变大小(resizability)这些细微的差别。一些操作是可选的,在实现时抛出UnsupportedOperationException
即可表示集合不支持该操作。集合的实现者必须在文档中声明那些操作是不支持的。
核心接口Collection接口的方法内容如下:
主要包括基本操作(计算元素个数、判断容器是否为空、判断是否包含某个元素、添加元素、移除元素)、迭代操作(获取迭代器对象的方法)、批量操作(判断是否包含一些元素、添加一些元素(并集)、移除一些元素(差集)、保留指定的一些元素(交集)、清空容器)和数组操作(将容器内容转换为数组存储形式)等。这些操作很显然是作为容器应当具备的常规操作,所以在最顶层接口中以抽象方法的形式给出(同时看出很多操作比数组灵活太多)。具体接口如下所示:
核心接口Map接口的方法内容如下:
常规操作与集合类相似,只是部分方法名称有所改变(get put等);同时由于存储的是键值对,所以提供了返回键集合、值集合、键值对集合的方法keySet()、values()和entrySet()。映射类相当于数学中的函数,y=f(x)。所以,键是不会重复的;对于每一个键都有唯一的值与之映射。因此键集合、键值对集合都是Set类型(没有重复元素),值集合是普通的集合类(一般用List存储)。
注意:Map接口中有一个内部类,接口Entry,该接口表示键值对对象。
二、根据需要扩展核心接口:在实际应用中对于具体的集合可能会有特别的操作需求,那么只需要扩展(extends)核心接口即可。
核心接口Collection主要的子接口有:
1、List接口:核心接口Collection接口中的数据元素是无序存储的。List接口直接继承了Collection接口,增加了有关元素位置的操作,允许有重复的元素。实现该接口的集合类是一个以列表形式存储数据的集合,可以对插入列表的每个元素的位置进行精确控制。所以该接口中增加了很多包含参数index的方法。List就是线性表,与数组一样是有顺序的线性容器。因为List类建立了元素与索引之间的关系,所以可以通过索引访问元素。
2、Set接口:直接继承于Collection接口,扩展的地方是要求元素不能重复。如果向实现Set接口的集合类中插入已经存在的元素,会插入失败。如果进一步要求对存储的元素进行排序,那么可以继续扩展Set接口,得到继承于Set接口的SortedSet接口。
3、Queue接口:直接继承于Collection接口。它与别的接口不一样的地方在于:Queue主要用于存储数据,而不是处理数据(A collection designed for holding elements prior to processing)。是一种在处理元素前用于保存元素的 collection。队列为Collection接口中的插入、提取和检查操作都提供了第二种形式:一种抛出异常(操作失败时),另一种返回一个特殊值(null 或 false,具体取决于操作)。比如说Collection接口中插入操作为add(E e),而Queue额外提供了插入方法offer(E e)。队列通常(但并非一定)以 FIFO(先进先出)的方式排序各个元素,不过优先级队列和 LIFO 队列(或堆栈)例外。对此也要求要扩展相应的方法。
核心接口Map的主要子接口有:
1、SortedMap接口:如果我不仅想要存储键值对,还希望集合中的键值对按照键进行排序,那么可以直接扩展Map接口,得到SortedMap接口。
三、实现接口的骨干方法:有了以上的接口层次,如果我想设计一个集合,直接实现这些接口即可。但是重写所有的抽象方法的代码量仍然是比较大的,JDK为了方便设计集合,已经提前实现了这些接口的主要操作,也就是对应的Abstract类。比如实现了Collection接口的主要方法的AbstractCollection抽象类,实现了Map接口的主要方法的AbstractMap抽象类,实现了List接口的AbstractList抽象类等等。如果想设计自定义的集合类,直接继承这些抽象类即可。
四、提供集合框架的具体实现类
集合类体系的具体实现类主要有:
List接口的具体实现:
1、ArrayList类:其实,就是顺序表。继承自AbstractList类,实现的是List接口。长度可以动态改变(就地扩容)的数组线性表类,因为是基于数组实现的,所以优点是:随机访问速度极快(O(1)),但是缺点是:不适合频繁的插入和删除操作(O(n))。特点:用默认的构造器初始化得到的ArrayList长度为10,就地扩容时容量变为:“原容量*3/2+1”。ArrayList是线程不安全的。请注意,尽管ArrayList底层使用数组实现该顺序表,所以具备了数组的随机访问速度快的优点,但是就效率而言还是比不过数组,这是因为:①空间利用率不高②扩容时移动大量的元素耗费时间。尽管如此,它在操作的灵活性上还是远超数组。
2、Vector类:ArrayList线程安全的版本。继承自AbstractList类,实现的是List接口。与ArrayList基本相同,区别就是:①扩容方案是变为原来的容量的两倍。②是线程安全的。所以一般在多线程中使用Vector代替ArrayList。
3、LinkedList类:继承自AbstractSequentialList类,实现的是List和Queue接口。是一个以链表形式实现的集合类。因为是基于双向循环链表实现,所以优点是:插入和删除操作效率高(O(1)),缺点是:随机访问速度慢,因为需要遍历链表(O(n))。
Set接口的具体实现:
1、HashSet类:继承自AbstractSet类,实现的是Set接口。受哈希表支持,基于hash算法来存储无重复的数据的类。其实HashSet底层是基于HashMap实现的。HashMap存储的是键值对,要求键不能重复,如果单独看键,将值设定为固定值,其实HashMap的键就是Set集合!HashSet访问速度最快,但是不保证集合的迭代顺序,特别是不保证集合的迭代顺序永久不变(因为可以会发生再哈希resize)。
2、LinkedHashSet类: 继承自HashSet类,实现的是Set接口。通过查看LinkedHashSet的源码可以发现,其底层是基于LinkedHashMap来实现的。对于LinkedHashSet而言,它和HashSet主要区别在于LinkedHashSet中存储的元素是在哈希算法的基础上增加了链式表的结构。速度比HashSet慢点,但是维持插入顺序。
3、TreeSet类:继承自AbstractSet类,实现的是SortedSet接口。TreeSet是一种排序二叉树。存入Set集合中的值,会按照值的大小进行相关的排序操作。底层算法是基于红黑树来实现的。TreeSet中的元素会按照相关的值进行排序,所以该方法比前两种都慢。
Queue接口的具体实现:1、PriorityQueue类:表示一个基于优先级堆的无界优先级队列。优先级队列是无界的,但是有一个内部容量,控制着用于存储队列元素的数组大小。它通常至少等于队列的大小。随着不断向优先级队列添加元素,其容量会自动增加。
2、各种Queue和栈的行为,都有LinkedList提供支持。
映射类体系的具体实现类主要有:
1、HashMap类:继承AbstractMap类,实现了Map接口。HashMap基于hash表实现,若key的hash值相同则使用链表方式进行保存(拉链法),即链表的数组。新建一个HashMap时,默认的话会初始化一个大小为16,负载因子为0.75的空的HashMap。HashMap至多允许一个key为null,允许多个value为null。
2、LinkedHashMap类:继承自HashMap,实现了Map接口。同样也是基于hash算法实现,但是LinkedHashMap与HashMap的不同之处在于,LinkedHashMap并不是一个”链表的数组“,而是一个双重链表。该类线程不安全,如果你想在多线程中使用,那么需要使用Collections.synchronizedMap方法进行外部同步。
3、HashTable类:继承于Dictionary类,实现了Map接口。此类实现一个哈希表,该哈希表将键映射到相应的值。HashTable不允许任何null元素,它是线程安全的。该类一般被废弃,由HashMap替代。
4、TreeMap类:继承AbstractMap类,实现了SortedMap接口。基于红黑树实现了键值对的排序。该类线程不安全,如果在多线程中使用,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。
补充:1、Collection接口中有将集合类转化为数组的方法toArray(),所以任何集合类都可以通过该方法实现集合和数组的交互。
2、Map接口有获取键集合视图的方法keySet(),有获取值集合视图的方法values(),有获取键值对集合视图的方法entrySet(),通过这三个方法可以实现映射类和集合类的交互。
总结:集合框架就是由一系列接口、接口的骨干实现抽象类以及具体实现类组成的模板。最顶层的接口包含核心方法,根据实际需求衍生了子接口以及子接口的子接口。为方便实现接口,对这些接口进行了初步实现,得到了骨干实现抽象类。最后封装好了完善的具体实现类,用作不同的存储和操作需求。
集合框架分为两个大枝,集合类体系和映射类体系,二者可以(转化)交互。集合类也可以与数组(转化)交互。
单一的元素请选择Collection类:
①List类是有顺序的线性表,允许元素重复:如果频繁随机访问,采用ArrayList实现;频繁在容器内部进行插入、删除操作,请选择LinkedList。
②Set类不允许重复的元素:为了查找速度,不在乎元素存储顺序,请选择HashSet;要求元素顺序与插入顺序一致,选择LinkedHashSet,但是比前者慢;为了得到一个按值有序的容器,可以使用TreeSet,最慢。
③Queue类和Stack都可以由LinkedList实现。
键值对的存储请选择Map类:
为了速度请选择HashMap;为了保证有序请选择TreeMap;为了保证与插入顺序一致,请选择LinkedHashMap(与Set一样)。
在具体使用某个容器类时,对象创建一定是使用具体的实现类,但是引用有两种选择:
①List<Type> list=new ArrayList<>();
这种方式选择上层接口作为引用(向上转型),因为具体实现类对父类接口的实现(其实就是继承),需要实现其所有的抽象方法(其实就是重写方法),而这种重写是覆盖,所以通过list上层接口引用不会影响调用具体实现类中的方法。这种方法还有个优点是灵活,如果需要切换另一种LIst实现类直接向下转型即可。
②ArrayList<Type> list=new ArrayList<>();
作为最常见的对象创建方式,这种方式优点是:可能某些具体实现类不仅实现了父类接口的全部抽象方法(即重写),还增加了在该情景下对数据独特的操作方法。这时候如果向上转型,显然会丢失这部分方法;所以直接将引用类型确定为具体实现类本身即可。
另外,所有的Collection接口实现类都提供带有Collection<T>参数的构造器,这是为了方便实现容器类之间的转换。使用该方式还有一些额外的效果,比如:将List容器转存至Set容器,自动消除了重复元素,如果是TreeSet还能按值排序;将List容器转存至Map容器时,可以将每个元素出现的次数作为值,元素作为键,这样可以统计频度,等等。
数组和容器都可以使用for-each方式进行迭代遍历,但是该方式只能读。容器类提供了iterator()方法获取一个轻量级对象——Iterator迭代器对象。迭代器对象进行容器类的遍历时需要注意:①单向移动,向后遍历 ②可以移除元素,但是每次只能移除一个元素,重复调用remove()方法会空指针异常。其实移除元素,移除的是迭代器游标位置的元素,初始时游标指向第一个元素之前。
另外,List类还额外提供了listIterator()方法来获取ListIterator,这种迭代器威力更加强大,可以自由的双向移动,还可以设置初始化游标的位置。不仅可以移除元素,还可以插入、修改元素。