Java中的集合类
实线边框的是实现类,比如ArrayList,LinkedList,HashMap等
折线边框的是抽象类,比如AbstractCollection,AbstractList,AbstractMap等,
点线边框的是接口,比如Collection,Iterator,List等。
上述所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含hashNext(),next(),remove()三种方法。它的一个子接口LinkedIterator在它的基础上又添加了三种方法,分别是add(),previous(),hasPrevious()。也就是说如果实现Iterator接口,那么在遍历集合中元素的时候,只能往后遍历,被遍历后的元素不会再遍历到,通常无序集合实现的都是这个接口,比如HashSet,HashMap;而那些元素有序的集合,实现的一般都是LinkedIterator接口,实现这个接口的集合可以双向遍历,既可以通过next()访问下一个元素,又可以通过previous()访问前一个元素,比如ArrayList。
1、ArrayList、LinkedList、Vector
List都是有序的:后面add的成员自动添加到末尾;ArrayList是直接加到数组的下一个index处;LinkedList添加到链表的下一个节点中。
List允许元素重复:根据value进行remove的时候,只删除首次找到的对象(允许add多个null元素,操作逻辑相同);
List动态调整大小的秘密:
ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用ensureCapacity()来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用Arrays.copyof()方法将元素拷贝到新的数组。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常耗时。拷贝的时候,保证了老元素的下标不变化,否则调整了一次大小,数据的位置都变化了,那还了得?
ArrayList的默认容量是10,但是不能直接用,因为内部数组维护了一个size,只有调用了add,或者初始化的时候直接传了一个array过来的情况下,size才会变化;其他操作例如get会先判断size是否超出边界size,超出就会报错。
Linked不需要扩容动作。
ArrayList调整大小的时候,大量使用System.arraycopy()方法。该方法被标记了native,调用了系统的C/C++代码,在JDK中是看不到的,该函数实际上最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。
ArrayList也采用了快速失败的机制:有一个modCount记录修改的次数,一旦发现有多线程试图改变list结构,则立即报错。
(1)、ArrayList是实现了基于动态数组的数据结构,LinkedList基于双向链表的数据结构(头结点不存储数据)。对于随机访问get和set,ArrayList直接通过下标访问,LinkedList则要从链表头部依次移动指针。这种情况下ArrayList完胜。
(2)、当需要插入数据的时候,如果是在集合的前段(大概集合容量的前1/10)处插入数据时,linkedlist性能明显优于arraylist;当在集合的中部甚至靠后的位置插入大量数据时,arraylist的性能反而远远优于linkedlist。原因推测是arrayList会在末尾预留空间,如果有数据从末尾插入,直接写到数组里面去就好了,不需要其他特殊的操作。
(3)、LinkedList还实现类栈和队列的操作方法,因此也可以作为栈、队列和双端队列来使用。
(4)、Vector现在用的比较少了,其实现类似ArrayList,不同点在于Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好。两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同的,Vector增加原来空间的一倍,ArrayList增加原来空间的50%
http://pengcqu.iteye.com/blog/502676#bc2374415
2、HashSet、TreeSet、LinkedHashSet
HashSet无序不可重复,允许使用null
HashSet是如何实现不重复的?
HashSet底层是基于HashMap实现的。该元素作为key放入HashMap,value设置一个虚拟的Object。
由于HashMap的put()方法添加key-value对时,当新放入HashMap的Entry中key与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true), 新添加的Entry的value会将覆盖原来Entry的value,但key不会有任何改变, 因此如果向HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入HashMap中,原来的元素也不会有任何改变,这也就满足了Set中元素不重复的特性。
TreeSet比HashSet有所改进,是一个有序的集合(默认使用红黑树完成排序)。这里排序是通过元素比较为依据,不是插入的顺序。
LinkedHashSet在HashSet的基础上又形成了双向链表,可以记住插入的顺序
3、HashMap、HashTable、TreeMap、LinkedHashMap
(1)、HashMap原理
实际上是一个数组和单向链表的结合体,通过key值得到一个hashcode,计算出作为数组的下标,能够快速定位到数组槽位。
如果多个不同的key值计算出了相同的hashcode,就会有多个value定位到数组的同一个位置,这就是所谓的冲突。这种情况下,这多个value会组成链表,访问效率就会降低。因此为了提高效率,key值计算出来的hashcode要尽量的分散。
下面是一个HashMap的数据结构图:
上面提到了根据key值计算hashcode,这个计算过程叫做Hash算法,一般通用的做法是key值对数组的长度取模,当然还有很多其他精妙的算法,不赘述。
当HashMap中的元素越来越多的时候,冲突的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,比较消耗性能。
(2)、HashMap特性
HashMap键必须唯一,值可以重复,没有顺序;键值可以为null,null键值只有一个,放到table[0]链表里面;
HashMap其实就是一个Entry数组,Entry对象中包含了键和值,其中next也是一个Entry对象,它就是用来处理hash冲突的,形成一个链表,既然是数组,就有扩容的问题。
对hashmap进行equal操作,判断的原则是key、value都分别相等才行;
HashMap的key必须唯一,如果key相等,put的时候,会把旧值覆盖掉;
HashMap的put方法:每次put的时候,会把新的键值对插入到单链表的头结点位置。
// 将“key-value”添加到HashMap中 public V put(K key, V value) { // 若“key为null”,则将该键值对添加到table[0]中。 if (key == null) return putForNullKey(value); // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。 int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出! if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 若“该key”对应的键值对不存在,则将“key-value”添加到table中 modCount++; //将key-value添加到table[i]处 addEntry(hash, key, value, i);//插入到单链表的头结点位置 return null; }
hashmap是通过hash值定位数组的,定位方法为hash&(size-1),这里size为数组大小,为了能够充分利用空间,这里的size须为偶数,实际hashmap中,数组的大小定义为capacity为大于initialCapacity的最小的2的n次幂;
当Hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后根据hash值重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能;
在resize的过程中,Entity<K,V>在数组中的下标会发生变化,原来在一个链表中的元素,调整后不一定在一个链表中(因为hash值重新计算了);在同一个链表中的元素次序会反过来,因为链表插入是从头部插入的,早插入的会逐渐移动到末尾,这是为了避免尾部遍历(tail traversing)。
HashMap的扩容动作:
// 重新调整HashMap的大小,newCapacity是调整后的容量 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; //如果就容量已经达到了最大值,则不能再扩容,直接返回 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中, // 然后,将“新HashMap”赋值给“旧HashMap”。 Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); } // 将HashMap中的全部元素都添加到newTable中 void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; //每一个单向链表 if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity);//计算新的下标,每一个成员都可能计算出与原来不同的下标i e.next = newTable[i];//注意这三句,把新的e放到链表的前面 newTable[i] = e; e = next; } while (e != null); } } }
HashMap的get方法
// 获取key对应的value public V get(Object key) { if (key == null) return getForNullKey(); // 获取key的hash值 int hash = hash(key.hashCode()); // 在“该hash值对应的链表”上查找“键值等于key”的元素 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k; //判断key是否相同,用==或者equals if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } //没找到则返回null return null; }
(3)、HashMap和HashTable的区别
HashMap是非synchronized,而Hashtable使用synchronized来保证线程安全,多个线程可以共享一个Hashtable,但在线程竞争激烈的情况下HashTable的效率非常低下;而如果没有正确的同步的话,多个线程是不能共享HashMap的;
另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别;
HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行
(4)、TreeMap在HashMap的基础上有所改进,通过比较键值来实现排序,是一个有序的键值对。
(5)、LinkedHashMap在HashSet的基础上又形成了双向链表,用来记住插入的顺序。
另外,LinkedHashMap可以按照访问顺序,对映射条目进行调整,使得每次被访问的条目都会自动移动到双向链表的尾部,如果要使用这种功能,请使用LinkedHashMap<K,V>(initialCapacity,loadFactor,true)
4、包装器synchronizedMap 和 synchronizedList、synchronizedSet
这三个包装器,将非线程安全的Map和List转换为线程·安全的。
Map m = Collections.synchronizedMap(new HashMap());
不过用这种方法只能实现有条件地线程安全――所有单个的操作(put和get)都是线程安全的,但是多个操作组成的操作序列却可能导致数据争用,而引发多线程故障。
Map m = Collections.synchronizedMap(new HashMap()); List l = Collections.synchronizedList(new ArrayList()); // put-if-absent idiom -- contains a race condition // may require external synchronization,下面这两句在多线程操作中并不同步 if (!map.containsKey(key)) map.put(key, value); // ad-hoc iteration -- contains race conditions // may require external synchronization这种size的操作的结果可能在遍历过程中发生变化 for (int i=0; i<list.size(); i++) { doSomething(list.get(i)); } // normal iteration -- can throw ConcurrentModificationException // may require external synchronization for (Iterator i=list.iterator(); i.hasNext(); ) { doSomething(i.next()); }
5、ConcurrentHashMap
Hashtable 和 synchronizedMap 所采取的获得同步的简单方法有两个主要的不足。首先,这种方法对于可伸缩性是一种障碍,因为一次只能有一个线程可以访问 hash 表,严重影响效率。同时,这样仍不足以提供真正的线程安全性,许多公用的混合操作仍然需要额外的同步。虽然诸如 get() 和 put() 之类的简单操作可以在不需要额外同步的情况下安全地完成,但还是有一些公用的操作序列,例如迭代或者 put-if-absent(空则放入),需要外部的同步,以避免数据争用。
Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。数据结构 ConcurrentHashMap的目标是实现支持高并发、高吞吐量的线程安全的HashMap。一个ConcurrentHashMap由多个segment组成,每一个segment都包含了一个HashEntry数组的hashtable, 每一个segment包含了对自己的hashtable的操作,比如get,put,replace等操作,这些操作发生的时候,对自己的hashtable进行锁定。由于每一个segment写操作只锁定自己的hashtable,所以可能存在多个线程同时写的情况,性能无疑好于只有一个hashtable锁定的情况。
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。
参考:
http://blog.csdn.net/liulin_good/article/details/6213815
http://www.cnblogs.com/ITtangtang/p/3948555.html
http://www.cnblogs.com/ITtangtang/p/3948406.html
http://www.cnblogs.com/ITtangtang/p/3948538.html
http://qifuguang.me/2015/09/10/[Java%E5%B9%B6%E5%8F%91%E5%8C%85%E5%AD%A6%E4%B9%A0%E5%85%AB]%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90ConcurrentHashMap
https://www.ibm.com/developerworks/cn/java/j-jtp07233/
《java核心技术 卷I 基础知识》