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 基础知识》

 

posted @ 2015-11-21 00:52  mingziday  阅读(470)  评论(0编辑  收藏  举报