十七、 容器深入研究
17.1 完整容器分类法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LcSybBzy-1606267445804)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1594197006613.png)]
javase5新增:
- Queue接口(LinkedList已经为实现该接口做了修改)及其实现PriorityQueue和各种风格的BlockingQueue,其中BlockingQueue将在21章中解释。
- ConcurrentMap接口及其实现ConcurrentHashMap,它们也是用于多线程机制的,21章解释
- CopyOnWriteArrayList和CopyOnWriteArraySet,它们也是用于多线程机制的
- EnumSet和EnumMap,为使用enum而设计的Set和Map的特殊实现,19章解释
- 在Collections类中的多个便利方法
17.2 填充容器
Collections类也有一些实用的static方法,其中包括fill(),同Arrays一样只复制同一对象引用来填充整个容器的,并且只对List对象有用,但是所产生的列表可以传递给构造器或addAll方法。
Collections.addAll() 将一些元素添加到一个Collection中
Collections.nCopies() 复制一些元素到Collections中,返回一个List集合
Collections.fill() 复制元素填充到集合当中
17.2.1 一种Generator解决方案
17.2.2 Map生成器
17.2.3 使用Abstract类
每个java.util容器都有其自己的Abstract类,它们提供了该容器的部分实现,因此必须做的只是去实现那些产生想要的容器所必需的方法。
AbstractList 实现了List接口;
AbstractMap 实现了Map接口;
AbstractSet 实现了Set接口;
享元模式(GoF23):可在普通解决方案需要过多对象,或者产生普通对象太占用空间时使用享元。享元模式使得对象的一部分可以被具体化,因此,与对象中的所有事物都包含在对象内部不同,可以在更加高效的外部表中查找对象的一部分或整体。
17.3 Collection的功能方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zCkfgI34-1606267445806)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1594199530670.png)]
17.4 可选操作
执行各种不同的添加和移除的方法在Collection接口中都是可选操作
17.4.1 未获支持的操作
常见的来源背后由固定尺寸的数据结构支持的容器,当你用Arrays.asList()将数组转换为List时。还可通过使用Collections类中“不可修改”的方法,选择创建会抛出UnsupportedOperationException的容器(包括Map)
17.5 List的功能方法
17.6 Set和存储顺序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pSBlJVLO-1606267445806)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1594202353309.png)]
HashSet如果没其他限制,这应该是你默认的选择,它对速度进行了优化。
17.6.1 SortedSet
SortedSet中的元素可以保证处于排序状态,TreeSet是其唯一实现,SortedSet接口中的下列方法提供附加的功能:
Comparator comparator()返回当前Set使用的Comparator;或者返回null,表示以自然方式排序。
Object first()返回容器中的第一个元素。
Object last()返回容器中的最末一个元素。
SortedSet subSet(fromElement, toElement)生成此Set的子集,范围从fromElement(包含)到toElement(不包含)。
SortedSet headSet(toElement)生成此Set的子集,由小于toElement的元素组成
SortedSet tailSet(fromElement)生成此Set的子集,由大于或等于fromElement的元素组成
SortedSet是“按对象的比较函数对元素排序”,而不是指“元素插入的次序”,如需顺序用LinkedHashSet
17.7 队列
Queue仅有两个实现LinkedList和PriorityQueue,它们的差异在于排序行为而不是性能。
Queue接口包含:
boolean add(E e); 添加一个元素,添加失败时会抛出异常
boolean offer(E e); 添加一个元素,通过返回值判断是否添加成功
E remove(); 移除一个元素,删除失败时会抛出异常
E poll(); 移除一个元素,返回移除的元素
E element(); 在队列的头部查询元素,如果队列为空,抛出异常
E peek(); 在队列的头部查询元素,如果队列为空,返回null
17.7.1 优先级队列
PriorityQueue:
- 设计:列表中的每个对象都包含一个字符串和一个主要的以及次要的优先级值。
- 优先级排序:该列表的排序顺序也是通过实现Comparable而进行控制的:Queue queue=new PriorityQueue<>();
17.7.2 双向队列
概念:双向队列就像一个队列,但是可以在任意一段添加或移除元素。
实现:在LinkedList中包含支持双向队列的方法,但在Java标准类库中没有任何显式的用于双向队列的接口。因此,LinkedList无法去实现这样的接口,无法像前面转型到Queue那样向上转型为Deque。但是可以使用组合创建一个Deque类,并直接从LinkedList中暴露相关方法。
LinkedList中支持双向队列操作的相关方法:
queue.addFirst(); 向队列首部添加元素
queue.addList(); 向队列尾部添加元素
queue.getLast(); 获取队列尾部元素
queue.getFirst(); 获取队列首部元素
queue.removeFirst(); 移除队列首部元素
queue.removeLast(); 移除队列尾元素
queue.size(); 返回队列大小
17.8 理解Map
Map接口:
size(); 返回Map大小
isEmpty(); 判断Map集合是否为空
containsKey(); 判断Mpa集合是否包括Key键
containsVaule(); 判断Map集合是否包括Vaule
get(Object); 获得Map集合键为Object的Vaule
put(K,V); 添加一个K,V键值对
remove(Object); 移除K键值对
putAll(Map); 添加所有元素Map集合
clear(); 清空Map中所有集合元素
keySet(); 返回键Key的Set集合
values(); 返回值Vaule的Set集合
entrySet(); 返回Map.entrySet集合
remove(Objcet,Object); 移除K,V集合
replace(K,V,V); 替换键值对
17.8.1 性能
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4gcb7lY4-1606267445807)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1594373909450.png)]
HashMap使用的特殊值散列码,取代对键的缓慢搜索,因为java所有对象都能通过hashCode产生散列码,而HashMap是使用对象hashCode进行快速查询,因此key键也相对是无序的
17.8.2 SortedMap
TreeMap是其现阶段的唯一实现,可确保键处于排序状态。
Comparator comparator():返回当前Map使用的Comparator;或者返回null,表示以自然方式排序
T firstKey返回Map中的第一个键
T lastKey返回Map中的最末一个键
SortedMap subMap(fromKey,toKey)生成此Map的子集,范围由fromKey(包括)到toKey(不包含)的键确定
SortedMap headMap(toKey)生成此Map的子集,由键小于toKey的所有键值对组成
SortedMap tailMap(fromkey)生成此Map的子集,由键大于或等于fromKey的所有键值对组成
17.8.3 LinkedHashMap
特性1:LinkedHashMap在迭代遍历键值对时,为了提高速度,以元素的插入或者LRU顺序来返回键值对(不同于HashMap)。
特性2:LinkedHashMap使用链表维护内部次序(不同于HashMap)。
最近最少使用(LRU)算法:没有被访问过的元素就会出现在队列的前面;LinkedHashMap可以在构造器中设定,使之使用此算法。
17.9 散列与散列码
对于一个放入Map集合的对象,它的类必须同时实现hashCode()和equals()方法。
正确的equals必须满足5个条件:
自反性,对任意x,x.equals(x)一定返回true;
对称性,对任意x和y,如果x.equals(y)为true,则y.equals(x)也为true;
传递性,对任意x,y,z,如果有x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true;
一致性,对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回结果应该保持一致;
对任何不是null的x,x.equals(null)一定返回false。
17.9.1 理解hashCode()
如果不为你的键覆盖hashCode()和equals(),那么使用散列的数据结构(HashSet,HashMap,LinkedHashSet或LinkedHashMap)就无法正确处理你的键。
散列:目的在于想要使用一个对象来查找另一个对象。不过可以使用TreeMap或者自己实现的Map也可以达到这个目的。
17.9.2 为速度而散列
散列的价值在于速度,针对k-v查询(由于瓶颈位于键的查询速度):
方式1(普通方式):线性查询,最慢的查询方式;
方式2(次优方式):保持键的排序状态,然后使用Collections.binarySearch进行快速查询。
方式3(更优方式):散列则更进一步,使用数组(存储一组元素最快的数据结构)来表示键的信息(不是键本身)。但数组不能调整容量,所以数组并不保存键本身,而是通过键对象生成一个数字,将其作为数组的下标(这个数字就是散列码)。
关于散列码&k值查询:
不同的键可以产生相同的下标(散列码):也就是说,可能会有冲突,因此数组多大(可以固定size)就不重要了,任何键总能在数组中找到它的位置。
k值散列方式的查询过程:
step1:首先是计算散列码;
step2:然后使用散列码查询数组(保持了k值信息,非k值对象本身)。如果没有冲突,那就有了一个完美的散列函数。通常,如散列冲突由外部链接处理:数组并不直接保存值,而是保存值的list。然后对list中的值使用equals()方法进行线性的查询。这部分查询自然比较慢。
step3:但是如果散列函数好的话,数组的每个位置就只有较少的值。因此,不是查询整个list,而是快速地跳到数组的某个位置,只对很少的元素进行比较。这就是HashMap如此快的原因。
17.9.3 覆盖hashCode()
设计hashCode()时重要原则:
原则1:无论何时,对同一个对象调用hashCode()都应该生成同样的值。
原则2:此外,也不应该是hashCode()依赖于具有唯一性的对象信息,尤其是使用this的值。因为这样做无法生成一个新的键,使之与put中元素的键值对中的键相同。
举例:以String为例:程序中有多个String对象,都包含相同的字符串序列,那么这些String对象都映射到同一块内存区域。所以new String(“hello”)生成的两个实例,虽然相互独立的,但是对它们使用hashCode()应该产生相同的结果。
17.10 选择接口的不同实现
容器选型原则:容器之间的区别通常归结为由什么在背后支持它们。也就是说,所使用的接口是有什么样的数据结构实现的。
举例1: ArrayList底层由数组支持;而LinkedList是由双向链表实现,其中每个对象包含数据的同时还包含执行链表的前一个与后一个元素引用。因此,如果经常在表中插入或删除元素,LinkedList就比较合适;否则,应该使用速度更快的ArrayList。
举例2:Set中,HashSet是最常用的,查询速度最快;LinkedHashSet保持元素插入的次序;TreeSet基于Treemap ,生成一个总是处于排序状态的Set。
17.10.2 对List的选择
get和set测试:使用了随机数生成器来执行List的随机访问。在输出中,对于背后有数组支持的List和ArrayList,无论列表的大小如何变化,这些访问速度都很快。而对于LinkedList来说,访问时间对于较大的列表来说明显增加。很显然,如果你需要执行大量的随机访问,链表不是一个很好的选择。
interadd测试:使用迭代器在列表中间插入元素。对于ArrayList来说,当列表变大的时候,其开销会变得十分巨大,但是对于LinkedList来说,相对比较低廉,并且不会受着列表尺寸的变大而变大。
insert和remove测试:都使用了索引作为插入和删除点,而没有选择List两端的元素,LinkedList对于插入和删除操作来说十分低廉,并且不会随着列表尺寸的变大而变大。但是对于ArrayList来说,插入操作的代价特别高昂,并且随着列表尺寸的变大而变大。最佳的做法可能是将ArrayList做为默认首选
List容器选型总结:
最佳的做法可能是将ArrayList做为默认首选
只要需要使用额外的功能,或者当程序的性能因为经常从表中间进行插入和删除而变差的时候,才会选择LinkedList。
如果使用的是固定数量的元素,那么既可以选择使用背后有数组支撑的List,也可以选择真正的数组。
CopyOnWriteArrayList是List的一个特殊实现,专门用于并发编程。
17.10.4 对Set的选择
HashSet的性能基本上总是比TreeSet好,特别是在添加和查询元素时。
TreeSet存在的唯一原因是它可以维持元素的排序状态;所以,只有当需要一个排好序的Set时,才应该使用TreeSet。因为其内部结构支持排序,并且因为迭代是更有可能执行的操作,所以,用TreeSet迭代通常比用HashSet更快。
对于插入操作,LinkedHashSet比HashSet的代价更高,这是由维护链表所带来额外开销造成的。
17.10.5 对Map的选择
所有Map实现的插入操作都会随着Map的尺寸的变大而明显变慢。但是,查找的代价通常比插入小得多。
HashMap:Hashtable的性能大体上与HashMap相当,因为HashMap是用来替换Hashtable的(Hashtable已废弃),因此它们使用了相同的底层存储和查找机制。
TreeMap通常比HashMap要慢,并且不必进行特殊排序。一旦填充了一个TreeMap,就可以调用keySet()方法来获取键的Set视图,然后调用toArray()来产生由这些键构成的数组。之后可以使用Arrays.binarySearch()在排序数组中查找对象。当然,这只有HashMap的行为不可接受的情况下才有意义。因为hashMap本身就被设计为可以快速查找键。
LinkedHashMap在插入时比HashMap慢一点,因为它维护散列数据结构的同时还要维护链表(以保持插入顺序)。正是因为这给列表,使得其迭代速度更快。
IdentityHashMap则具有完全不同的性能,因为它使用==而不是equals来比较元素。
HashMap的性能因子
容量:表中的桶位数
初始容量:表在创建时所拥有的桶位数,HashMap和HashSet都具有允许指定初始化容量的构造器
尺寸:表中当前存储的项数
负载因子:尺寸/容量。空表的负载因子是0,而半满表的负载因子是0.5。以此类推。负载轻的表产生冲突的可能性小,因此对于插入和查找都是最理想的。HashMap和HashSet都具有允许指定负载因子的构造器,表示当负载情况达到该负载因子的水平时,容器将自动增加其容量,实现方式是是容量大致加倍,并重新将现有对象分布到新的桶位集中(成为再散列)。
HashMap使用默认的负载因子0.75,这个因子在时间和空间代价之间到达了平衡:
更高的负载因子:可以降低表所需的空间,但是会增加查找代价。因为查找是我们在大多数时间里所做的操作。
如果知道将要在HashMap中存储多少项,那么创建一个具有恰当大小的初始容量将可以避免自动再散列的开销。
17.11 实用方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sOvJkwbV-1606267445808)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1594435412108.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SDIJv272-1606267445810)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1594435430861.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TEenmqha-1606267445810)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1594435445375.png)]
17.11.2 设定Collection或者Map为不可修改的元素:
Collections.unmodifiableCollection()
Collections.unmodifiableList()
Collections.unmodifiableSet()
Collections.unmodifiableSortedSet()
Collections.unmodifiableMap()
Collections.unmodifiableSortedMap()
List<String> a = Collections.unmodifiableList(new ArrayList<String>());
17.11.3 Collection和Map的同步控制
Collections.synchronizedCollection()
Collections.synchronizedList()
Collections.synchronizedSet()
Collections.synchronizedSortedSet()
Collections.synchronizedMap()
Collections.synchronizedSortedMap()
直接将新生成的容器传递给了适当的“同步方法”;这样做就不会有任何机会暴露出不同步的版本。
List<String> a = Collections.synchronizedList(new ArrayList<String>());
快速报错
快速报错(fail-fast)机制:Java容器有一种保护机制,能够防止多个进程同时修改同一容器的内容。如果在迭代遍历某个容器的过程中,另一个进程介入其中,并且插入、删除或修改此容器内的某个对象,那就会出问题。Java容器类类库采用快速报错(fail-fast)机制。
ConcurrentModificationException异常:Java会探查容器上的任何除了进程所进行的操作以外的所有变化,一旦它发现其他进程修改了容器,就会立刻抛出ConcurrentModificationException异常。
内置规避并发报错的容器:ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet都使用了可以避免ConcurrentModificationException的技术。
17.12 持有引用
java.lang.ref类库:包含了一组类,这些类为垃圾回收提供了更大的灵活性。当存在可能会耗尽内存的大对象时,这些类显得特别有用。
有三个继承自抽象类Reference的类:SoftReference、WeakReference和PhantomReference。
当垃圾回收器正在考察的对象只能通过某个Reference对象才可获得时,上诉这些不同的派生类为垃圾回收器提供了不同级别的间接性指示。
对象是可获得的(reachable):是指此对象可在程序中的某处找到,这意味着在栈中有一个普通引用,它指向此对象。如果一个对象是可获得的,垃圾回收器就不能释放它,因为它仍然为程序所用。如果一个对象不是可获得的,那程序将无法使用它,所以将其回收是安全的。
对象引用四种类型(由强到弱排列,对应不同级别“可获得性”):
普通引用:
软引用:SoftReference:用以实现内存敏感的高速缓存。
弱引用:WeakReference:是为实现规范映射而设计,它不妨碍垃圾回收器回收映射的键或值。规范映射中对象的实例可以在程序的多出被同时使用,以节省存储空间。–常用于避免内存泄露场景。
虚引用:PhantomReference:用以调度回收前的清理工作,它比Java终止机制更灵活。
17.12.1 WeakHashMap
WeakHashMap用来保存WeakReference。它使得规范映射更易于使用,在这种映射中,每个值只保存一份实例以节省存储空间。当程序需要那个值的时候,便在映射中查询现有的对象,然后使用它。映射可将值做为其初始化的一部分,不过通常在需要的时候才生成值。
这是一种节约存储空间的技术,因为WeakHashMap允许垃圾回收器自动清理键和值,所以十分便。对于WeakHashMap添加键和值的操作,则没有什么特殊要求,映射会自动使用WeakReference包装他们。
清理工作,它比Java终止机制更灵活。
17.12.1 WeakHashMap
WeakHashMap用来保存WeakReference。它使得规范映射更易于使用,在这种映射中,每个值只保存一份实例以节省存储空间。当程序需要那个值的时候,便在映射中查询现有的对象,然后使用它。映射可将值做为其初始化的一部分,不过通常在需要的时候才生成值。
这是一种节约存储空间的技术,因为WeakHashMap允许垃圾回收器自动清理键和值,所以十分便。对于WeakHashMap添加键和值的操作,则没有什么特殊要求,映射会自动使用WeakReference包装他们。