概述
数据结构是组织数据的方式,数据结构这门课程是专门讲组织数据有哪几种方式的。Java中的集合就是对这些数据组织方式的实现。或者叫她们容器,取存放数据之意。(线程安全的集合放在多线程内容里面考虑)
Collection接口和Map接口
这两个个接口是Java集合相关内容最基本的接口,里面定义的方法都是每个集合相关类必须要有的,当然,这些方法都非常的基础,比如添加一个元素,删除一个元素,清空什么的。
Collection底下有三类主要的子接口:List、Queue和Set。
- List是一个有序集合,有索引,可以按照下标访问,但是从具体实现上来讲,循环数组实现的List更适合按照下标的随机访问,链表实现的列表挨个迭代比较方便。
- Set接口等同于Collection接口,但是方法的行为有更加严谨的定义,比如set集里面的元素不能重复,集的equals和hashCode方法也要注意只要元素相同就算相同。
- 还有SortedSet,SortedMap,NavigableSet,NavigableMap等这些需要实现排序,搜索等相关功能的接口。
下面简要介绍一些Java库中的集合:
集合类型 | 描述 |
---|---|
ArrayList | 一种可以动态增长和缩减的索引序列 |
LinkedList | 一种可以在任何位置高效的进行插入和删除操作的有序序列 |
ArrayDeque | 一种用循环数组实现的双端队列 |
HashSet | 一种没有重复元素的无序集合 |
TreeSet | 一种有序集 |
EnumSet | 一种包含枚举类型值的集 |
LinkedHashSet | 一种可以记住元素插入次序的集 |
PriorityQueue | 一种允许高效删除最小元素的集合 |
HashMap | 一种存储键值关系的数据结构 |
TreeMap | 一种键值有序排列的映射表 |
EnumMap | 一种键值属于枚举类的映射表 |
LinkedHashMap | 一种可以记住键值项添加次序的映射表 |
WeakHashMap | 一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityHashMap | 一种用“==”而不是equals比较键值的映射表 |
Collection集合
1. 链表
LinkedList是链表实现的列表,要在这种列表中间添加元素,要由这个列表的迭代器来添加,要是调用列表自己的add方法,也就是Collection中规定的add方法它只会在最后添加。当然像ArrayList这种数组实现的列表,在中间添加和删除的开销都很大,不合适。还有,不是所有的迭代器都要有add方法的,比如无序的HashSet,它的迭代器在迭代中途添加元素是没有意义的,所以普通的Iterrator接口没有add方法,但是ListIterator就有。
关于迭代器:再用“光标”类比时要格外小心。remove操作与BACKSPACE键的工作方式不太一样。在调用next之后,remove方法确实与BACKSPACE键一样删除了迭代器左侧的元素。但是,如果调用previous就会讲右侧的元素删除掉,并且不能连续调用两次remove。add方法只依赖于迭代器的位置。而remove方法依赖于迭代器的状态。
迭代器还有?:迭代器的set方法用一个新元素取代调用next或previous方法返回的上一个元素。可以想想,对某个迭代器遍历的时候,另一个迭代器要修改这个集合(类似于同时对一个文件又读又写)。链表迭代器的设计可以让它能检测到这种修改,所以如果迭代器发现它的集合被另一个迭代器修改了或者被集合自身的方法修改了,就会抛出一个ConcurrentModificationException异常,如下所示:
List<String> list = ...;
ListIterator<String> iter1 = list.ListIterator();
ListIterator<String> iter2 = list.ListIterator();
iter1.next();
iter1.remove();
iter2.next(); //此时抛出ConcurrentModificationException
对这种情况,如果要给一个链表附加多个迭代器,那么只能有一个迭代器可以修改链表的结构,其它的迭代器都只能读,这是对添加和删除而言的,对于set来说,不算结构性修改,所以可以在多个迭代器中都进行set(这个特性很重要,有很多Collections类的算法需要这个特性)。
不要使用链表中索引相关的方法,链表结构随机访问的效率特别低。
2. 数组列表
ArrayList封装了一个动态再分配的对象数组。Java还有个动态再分配的对象数组Vector,这个Vector是同步的,如果有多个线程同时访问Vector,可以保证线程安全,相应的,为了保证线程安全代码要在同步操作上浪费大量时间,如果你的程序本身是单线程的,或者通过设计避免了多线程同时访问ArrayList的时候,那你就非常适合使用ArrayList。
数组比起链表来说,非常支持随机访问,非常适合使用索引相关的方法,简单高效,但是对访问中途的添加和删除来说开销会很大。
3. 散列集
数组和链表可以按照指定的顺序排列元素,但是如果想查看某个元素,但是不知道它的位置,就得遍历列表查询。有一种众所周知的数据结构,可以快速的查找所需要的对象,叫散列表(hashTable)。散列表为每一个对象的实例计算一个整数值(对象的hashCode方法),不同的实例散列码是不同的,相同的对象散列码要相同,默认的散列码是对象的地址。
Java中的散列表是用链表数组存储的,每个链表表示一个桶(bucket),桶里面装的是同义词(明明是不同的实例或者说不同的键,但是哈希值是一样的,就叫同义词)。每个对象都有自己的散列值 ,用这个散列值对桶的总数取余,可以得到这个对象应该在第几个桶中(也就是数组下标)。如果桶里面没有对象那就直接存储,如果有对象就要和桶中的对象挨个比较,看看是已经存过这个对象了,还是有同义词(这叫散列冲突,可以想象,桶数越多,散列值分布约均匀,冲突的可能性就越小)。
桶的初始数目是可以控制的,通常,将桶数设置为预计元素个数的75%~150%。有些研究人员认为最好将桶数设置为一个素数,以防键的聚集。标准类库使用的桶数是2的幂,默认值为16(为表大小提供的任何值都将被自动的转换为2的下一个幂)。
当然,不可能把桶的初始数目设置成无限大,所以总会有存满的情况,这时候就要再散列(rehashed):创建一个桶数更多的散列表,把旧表中的数据挪到新表中。默认的警戒线是0.75,如果表中75%的位置以及有元素的时候就会发生再散列。
Java中的hashSet就是基于散列表的集,上面写的求余数只是散列函数的一种,实际上有多种方式可以将对象变成地址的。散列函数选的不恰当的话会产生很多冲突,导致性能降低。
4. 树集
树集比起散列表有所改进,它是有顺序的,每将一个元素添加到树中时,都会把它放在正确的位置上,添加元素到树集要比散列集慢一些,但是都比检查列表中的重复元素要快很多,树集查找元素的正确位置平均需要logn次比较。(Java中使用树集必须实现compareable接口,或者构造树集的时候必须提供一个比较器。)
Java中的树集TreeSet还有一些别的接口用来提供附加的一些功能,例如NavigableSet接口。
5.队列与双端队列
队列是一头删除一头添加,双端队列顾名思义就是两面都可以删除或者添加元素。对应的接口是Queue和Deque。
6. 优先级队列(堆)
优先级队列中的元素可以按照任意顺序插入,但是删除的时候总是删除最小的元素,它实际上是一个最小堆,插入的时候会堆会自我调整到合适的位置,取出的时候就总是最小的那个。最典型的示例是任务调度,以任意的顺序添加任务,但是总是优先级最高的任务先执行。同样,元素要实现Compareable接口或者构造堆的时候传入一个比较器。
Map映射
看完Map相关的映射,只觉得有各种花里胡哨的映射?!
1. 映射基本操作
HashMap和TreeMap比起集来说都是针对键的散列,和键所对应的值没有关系。遍历映射用forEach;查找元素时如果要指定元素不存在时的返回值可以使用getOrDefault方法返回一个默认值。
2. 更新映射项
Map有个merge方法,如果key没冲突的话(意思就是这个键没绑定值,或者值为null)就直接设置到map中,发生冲突则执行指定的方法合并或删除冲突key。
还有个compute方法,和put方法类似,但是put方法在以及存在键的情况下会返回旧的值,这个方法会返回指定方法的结果。
3. 映射视图
映射是有视图(视图是实现了Collection接口或者某个子接口的类)的,一个映射有三种视图:键集、值集合(“集”和“集合”不一样,值是集合不是集),键/值对集。键集是可以删除映射中的元素的,但是不能添加。键/值对集也不能添加映射中的元素,个人觉得不要在视图里面操作映射比较好。
4. 弱散列映射
映射中某个键的最后一次引用已经消亡,不在有任何途径引用这个这个值的对象了,那么就无法删除这个键值对,垃圾收集器只能看到映射是活动的,但是映射中的某个键不用了它是看不到的,所以设计了WeakHashMap类解决这个问题。
这种机制内部运行情况如下,WeakHashMap使用弱引用保存键。弱引用对象将引用保存到另外一个对象中,在这里就是散列键。对于弱引用的对象垃圾回收器有特殊的处理方法,对于弱引用,会将引用这个对象的弱引用放入队列中。WeakHashMap会周期性的检查队列,找出新添加的弱引用。一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来,于是WeakHashMap将删除对应的条目。
5. 链接散列集与映射
LinkedHashSet和LinkedHashMap类用来记住插入元素项的顺序,本来HashSet和HashMap都是没有顺序的,但是把每个元素都加上链表的结构,就能记住插入的顺序了?。(花里胡哨)她们的迭代器都是先插入的先访问,每次访问的时候都会把收到影响的元素从当前位置删除,放在链表的尾部。这个特性可以实现高速缓存的“最近最少使用”原则。当表满的时候可以把前面几项删掉,因为越排在后面操作的次数越多。这个过程可以通过覆盖removeEldestEntry方法来自动化。
6. 其它
枚举集与映射,EnumSet和EnumMap,键是枚举,感觉没啥卵用。标识散列映射,IdentityHashMap有特殊作用,在这个类中键的散列值不是用hashCode函数计算的,而是用System.identityHashCode方法计算(根据对象内存地址来计算散列码,比较键相等使用“==”而不是equals方法)。
视图与包装器
视图可以获取到一个对象对原集合进行操作,也就是通过视图来操作集合,我上面说了不要在视图里面操作集合,但是这里书上说视图技术在集合框架里有非常有用的应用。
1. 轻量级集合包装器
有的时候会遇到调用的方法需要一个集合类型的参数,但是你手里有的参数并不是集合类型的,这时候你要New一个对象,就不够优雅,怎么优雅呢?用视图对象,因为你不需要对临时使用一下的集合做一些特别大的修改,很多时候都不用修改。
Arrays的asList方法就会返回一个实现了List接口的视图。它可以用访问底层数组的get和set方法,但是改变数组大小的方法就不行了,会抛异常。这个视图可以理解成一个轻量级的List,只实现了List接口的一部分功能。
还有Collections类的nCopies(n, anObject);方法,返回一个List视图,包含n个元素,所有元素都是anObject对象。以此类推,还有,singleton方法(Set接口),singletonList,singletonMap,emptySet乱七八糟的视图。
2. 子范围
可以为很多集合建立子范围视图,子范围视图也是视图,操作子范围视图会改变原集合的。对于有索引的按照索引取子范围,对于有序集和映射,子范围可以使用排序顺序建立子范围。(子范围就是sub什么什么得到的一个子序列)。
3. 不可修改的视图
不是很懂为什么要有这些不可修改的视图。这些视图对现有的集合增加了运行时检查,如果发现视图对集合进行修改会抛出异常,并保持集合不变。ummodifiableCollection方法、synchronizedCollection和checkedCollection一样,equals方法是Object的equals方法。而unmodifiableSet类和unmodifiableList类却使用底层集合的equals方法和hashCode方法。
4. 同步视图
集合本身是线程不安全的,类库的设计者使用视图机制来确保常规集合的线程安全,而不是线程安全的类。可以使用Collections中的某些静态方法把集合转换成具有同步访问方法的集合。
5. 受查视图
会在add的时候检测插入的对象是否是正确的类型,如果不是就会抛出ClassCastException。
6. 关于可选操作的说明
视图是有局限性的,虽然实现了相应的接口,但是不具有相应接口的某些功能,这就和接口的理念不符合。接口是用来声明某些功能的,但是视图实现了接口,却不具有接口的功能,这种功能可选的情况是一种妥协,为了避免出现大量类似的接口,只好做了一些重用,不应该在用户的设计中使用。
算法
Java的集合框架内置了一些针对接口的算法实现,比如基本的排序、二分查找等。但是不如C++的几十种算法……
集合框架的排序算法内部是把所有元素放在一个数组里面排好序之后再复制回列表。而且用的是归并排序,好处在于如果有一个按照姓名排序的员工列表,再按照工资排序的话,会产生一个按照工资排序,工资相同的再按照姓名排序的列表。
还有些其他的简单工具,二分查找、最大元素、复制元素到另一个列表、用常量填充列表、逆置等。如果要把数组转换成集合可以使用Arrays的asList方法。从集合得到数组时,可以使用集合的toArray方法,但是参数要传一个所需类型且长度为0的数组,这样转换方法内部会创建一个相同类型的数组。或者传一个指定大小的数组,内部不会创建新数组。这个过程的意思是转换方法把你传给它的的数组填满集合的元素。
遗留的集合
Hashtable,过时的同步映射,等同于HashMap,只不过HashMap不同步,现在要并发访问的时候要用ConcurrentHashMap,具体要看多线程相关内容。
枚举,Enumeration接口,遗留的集合使用Enumeration接口对元素序列进行遍历,现在用Iterator接口。
属性映射,property map,键值都是字符串;表可以保存到文件中,也可以从文件中加载;Java平台类叫Properties,通常用于程序的特殊配置选项。
栈,Stack类,有push和pop方法,扩展为Vector类,可以让栈使用不属于栈操作的insert和remove方法。
位集,BitSet,使用位集比Boolean对象的ArrayList更加高效。