Java容器基本要点
可将java容器类库划分为两个不同的概念:
1)Collection:一个独立元素的序列,这些元素都服从一条或多条规则。List按照插入顺序保存元素,set不插入重复元素。Queue按照队列规则确定对象产生顺序。
2)Map:一组成对的<key,value>对象,可以使用键值key来查找值。
1. List
List可以将元素维护在特定的序列中。
有两种类型的List:
1> 基本的ArrayList,它便于处理随机访问元素,但是在List的中间插入和移除元素时较慢。
2> LinkedList,它通过代价较低的在List中间进行的插入和删除操作,提供了优化的顺序访问。不善于处理随机访问。LinkedList还添加了可以使其用作栈、队列或双端队列访问的方法。
两者都是按照被插入的顺序保存元素。
而ArrayList 采用的是数组形式来保存对象的,这种方式将对象放在连续的位置中,所以最大的缺点就是插入删除时非常麻烦。
LinkedList 采用的将对象存放在独立的空间中,即是基于链表的数据结构,而且在每个空间中还保存下一个链接的索引,但是缺点就是查找非常麻烦,要从第一个索引开始。
ArrayList和LinkedList在性能上各有优缺点,都有各自所适用的地方,总的说来可以描述如下:
1. 对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
2.在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
3.LinkedList不支持高效的随机元素访问。
4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
2. Set
在Java中使用Set,可以方便地将需要的类型以集合类型保存在一个变量中.主要应用在显示列表,Set是一个不包含重复元素的collection。更确切地讲,set 不包含满足 e1.equals(e2) 的元素对 e1 和 e2,并且最多包含一个 null元素。正如其名称所暗示的,此接口模仿了数学上的 set 抽象。
HashSet,TreeSet,EnumSet三个类实现了Set接口:
2.1 HashSet
HashSet具有的特点:
1 HashSet使用散列函数实现,不能保证元素的排列顺序,顺序可能发生变化。
2 HashSet不是同步的,如果多个线程同时访问一个Set集合,如果多个线程同时访问一个HashSet,如果有2条或者以上的线程修改了HashSet集合时,必须通过代码来保证其是同步的(TreeSet和EnumSet也一样)。
3 集合元素可以是null。
HashSet判断两个元素相等是的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode方法返回的值也相等。
当向HashSet中添加可变对象是,必须非常的小心,如果修改HashSet中的对象时,有可能导致该对象与集合中的其他对象相等,从而导致HashSet无法准确访问该对象。
HashSet的子对象LinkedHashSet使用链表维护元素次序,使元素以插入顺序保存,性能略低于HashSet。
2.2 TreeSet
TreeSet是SortedSet接口的唯一实现,可以确保元素处于排序状态。与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet集合采用红黑树的数据结构来对元素进行排序,分自然排序和定制排序俩种,默认的情况下采用自然排序。
试图把一个对象添加到TreeSet时,该对象的类必须实现Comparable接口,否则出现异常(添加第一个时没事,添加第二个时,TreeSet调 用该对象的CompareTo(Object obj)方法时引发异常),而且向TreeSet添加的对象应该属于同一个类的对象,否则也会引发异常。
一些常用类已经实现了Comparable接口,比如BigDecimal,BigInteger,
Character,Boolean(true>false),String,Date,Time等。
对于TreeSet而言,判断两个对象不相等的标准是:equals方法返回false,或compareTo方法没有返回0,即使两个对象是同一个对象也会当做两个对象处理。所有当重写一个须放入TreeSet的类的equals方法时,应该保证与compareTo方法有一致的结果。
如果向TreeSet中添加一个可变对象后,并且后面的程序修改了该对象的属性,导致它与其他对象的大小发生了变化,但TreeSet不会再次调整 它们的顺序,甚至导致保存的这两个对象通过equals返回true,而compareTo确返回0,所有推荐HashSet和TreeSet集合中只放入不可变对象。
2.3 EnumSet
EnumSet是一个专门为枚举类设计的集合,内部以向量的方式存储,占用内存很少,运行效率很高。
EnumSet不允许加入null元素,否则会出现异常。当试图复制一个Collection集合里的元素来创建EnumSet集合时,必须保证Collection集合里的所有元素都是同一个枚举类的枚举值。
3 Map
Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个value。Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。在此引入负载因子的概念。
负载因子:尺寸/容量。在java语言中,通过负载因子(load factor)来决定何时对散列表进行再散列。例如:如果负载因子是0.75,当散列表中已经有75%的位置已经放满,那么将进行散列。负载因子越高(越接近1.0),内存的使用率越高,元素的寻找时间越长。负载因子越低(越接近0.0),元素的寻找时间越短,内存浪费越多。
如下是基本的Map实现。
1)HashMap Map是基于散列表的实现(取代了HashTable)。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量和负载因子,以调整容量的性能。
2)LinkedHashMap 类似于HashMap,但是迭代遍历时,取得“键值对”的顺序是其插入顺序,或者是最近最少使用的顺序。只比HashMap慢一点;而在迭代访问时反而更快,因为它使用链表维护内部次序。
3)TreeMap 基于红黑树的实现。查看“键值对”或“键”时,它们会被排序(次序由Comparator或Comparable决定)。TreeMap的特点在于,所得到的结果是经过排序的。TreeMap是唯一带有subMap()方法的Map,它可以返回一个子树。
4)WeakHashMap 弱键映射,允许释放映射所指向的对象,这是为解决某类特殊问题而设计的。如果映射之外没有引用指向某个“键”,则此“键”可以被垃圾收集器回收。
5)ConcurrentHashMap 一种线程安全的Map,它不涉及同步加锁。
6)IdentityHashMap 使用==代替equals()对“键”进行比较的散列映射,当且仅当 (k1==k2) 时,才认为两个键 k1 和 k2 相等,因此其允许有相同的Key值。
3.1 Hashtable类
Hashtable继承Map接口,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。
Hashtable通过initial capacity(初始容量)和load factor(负载因子)两个参数调整性能。通常缺省的load factor 0.75较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。
由于作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方法。hashCode和equals方法继承自根类Object,如果你用自定义的类当作key的话,要相当小心,按照散列函数的定义,如果两个对象相同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同,如果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,Map处理这些冲突的方法是在索引位置处插入一个链接列表,并简单地将元素添加到此链接列表。所以尽量定义好的hashCode()方法,能加快哈希表的操作。
如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。
Hashtable是同步的。
3.2 HashMap类
HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。Hashtable的应用非常广泛,HashMap是新框架中用来代替Hashtable的类,也就是说建议使用HashMap,不要使用Hashtable。
HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash操作。
3.3HashTable和HashMap区别
1> 继承不同。
public class Hashtable extends Dictionary implements Map public class HashMap extends AbstractMap implements Map
2> Hashtable 中的方法是同步的,而HashMap中的方法在缺省情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。
3> Hashtable中,key和value都不允许出现null值。在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
4> 两个遍历方式的内部实现上不同。Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
5> 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
6> Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。
4 Queue
Queue 接口与List、Set同一级别,都是继承了Collection接口。除了并发应用,Queue在java SE5 中仅有的两个实现是LinkedList和PriorityQueue。LinkedList实现了Queue接口。Queue接口窄化了对LinkedList的方法的访问权限(即在方法中的参数类型如果是Queue时,就完全只能访问Queue接口所定义的方法 了,而不能直接访问 LinkedList的非Queue的方法),以使得只有恰当的方法才可以使用。BlockingQueue 继承了Queue接口。
BlockingQueue成员详细介绍
1. ArrayBlockingQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一 个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任 何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
2.LinkedBlockingQueue
基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时 (LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以。
3. DelayQueue
DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
使用场景:
DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。
4. PriorityBlockingQueue
基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
5. SynchronousQueue
一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者 中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:
如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来 管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。