[核心工具] Collections

来源:互联网 发布:大众软件2014电子版 编辑:程序博客网 时间:2024/06/01 07:38
核心类关系图:
    首先说明,这里说道的Collections是 java.lang 中相对早期就有实现的那些基础核心类,而不包括current包下的工具类。current包下的工具类会在另一篇中讲解。



List
    List中最常用的有三个:ArrayList, Vector,LinkedList。
  • ArrayList, Vector
    基于数组实现,允许null元素。而ArrayList和Vector的区别在于,Vector所有方法都是线程同步的,而ArrayList则没有因此也是线程不安全的。创建一个同步的List还可以这样 List list = Collections.synchronizedList(new LinkedList(...));。
    Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。
    由于基于数组实现,导致以下几个特点:
        0:size,isEmpty,get,set方法运行时间为常数。
        1:Add的过程中需要校验数组大小,当数组不够用则需要扩容。扩容过程中涉及到数组复制,影响性能。添加一个元素需要O(n)的时间
        2:优秀的随机访问,直接通过下标完成。
        3:随机或者删除需要大量的移动数组元素,性能较差。
        4:有一个容量参数可以用于指定初始大小(每次默认扩展到原来的1.5倍)。
  • LinkedList
    基于循环双向链表实现的,LinkedList本身即使为空也包含一个链表的Header节点。
    由于基于链表实现,导致以下几个特点:
        0:size需要遍历列表。
        1:优秀的随机插入删除性能
        2:不需要预先分配连续内存空间,也不需要容量调整
        3:随机访问需要从Header节点逐一往后找,性能很差。
        4:for(int I=0; I<linkedLis.size(); I++) 这样的操作非常慢,因为每次实际上都是一个随机访问。需要用forEach或者迭代器进行循环操作。
  • Stack 类
     Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。

     RandomAccess接口。由于List有基于数组和基于链表的两种实现方法,而链表不支持随机访问。因此Java提供了RandomAccess接口,基于数组实现的都实现了该接口,而基于链表实现的都没有实现该接口。因此 RandomAccess 才适合用 for(int&nbsp;I=0;&nbsp;I&lt;linkedLis.size();&nbsp;I++) 这样的操作,否则最好用forEach或者迭代器。


Map
     请注意,Map没有继承Collection接口,Map提供key到value的映射。线程不安全。
    一个Map中不能包含相同的key,每个key只能映射一个value。
    Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。
    AbstractMap抽象类覆盖了equals()和hashCode()方法以确保两个相等键值对返回相同的哈希码。 
  • HashMap类
     HashMap是非同步的,并且允许null,即null value和null key。HashMap基于数组 + 链表的形式来。其中数组中是Entry类,包含了:key, value, next, hash四个值。
    在不冲突的情况下,直接从Key的Hash定位到数组中的位置,而在冲突的情况下,会在对应位置上的链表上增加元素。Hash定位是通过:hashCode() + hash() 方法来实现的。
    当向HashMap中添加元素的时候:
        首先计算元素的hashcode值,然后用这个(元素的hashcode)%(HashMap集合的大小)+1计算出这个元素的存储位置,
        如果这个位置位空,就将元素添加进去;如果不为空,则用equals方法比较元素是否相等,相等就不添加,否则找一个空位添加。
     
    java.lnag.Object中对hashCode的约定:
        1) 在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话(不是所有字段, 而是equals用到的字段),则对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。
        2) 如果两个对象根据equals(Object o)方法是相等的,则调用这两个对象的hashCode方法必须产生相同的整数结果。
        3) 如果两个对象根据equals(Object o)方法是不相等的,则调用这两个对象中任一个对象的hashCode方法,不要求产生不同的整数结果。但如果能不同,则可能提高散列表的性能。

    从这个实现可以看出,有以下几个特点:
        1:hash算法和hashCode算法影响性能。HashMap的hash方法是基于位操作的,而Object的默认hashCode方法是Native方法。注意,hashCode是可以重载的,因此需要注意实现性能。
        2:当 hash算法和hashCode算法很合理,而容量也足够大的时候,Value很难产生冲突,那么约等于是随机访问。但是,如果出现大量的冲突,那么则退化成几个链表。
        3:既然是数组,那么就有容量参数。后面会单独分析这个过程。
  • TreeMap
    实现了SortedMap接口,它用来保持键的有序顺序。添加到SortedMap实现类的元素必须实现Comparable接口。TreeMap类是它的唯一一份实现。
    线程不安全。
    TreeMap是基于红黑树实现的,这是一种平衡查找树。
    TreeMap提供了:subMap, headMap, tailMap方法,用于返回部分子树。其返回的子树还是有序的。
    由于是基于树实现的,有如下的特性:
        1:插入删除过程可能涉及到树的再平衡,所以可能造成性能影响,此外查找也不能直接进行下标访问。因此这些过程是一般是慢于基于数组的实现。
        2:由于红黑树算法,最坏的情况下也能以o(log n)时间内做到查找删除等。
  • 其他早期Map类型
     Hashtable:任何非空(non-null)的对象都可作为key或者value。基于数组实现。和HashMap不同之处在于,Hashtable是同步的。
     WeakHashMap:一种改进的HashMap,它对key实行“弱引用”,如果一个key没有被除了WeakHashMap之外的其他方所引用,那么在内存不足的时候该key可以被GC回收(弱引用的特点)。因为值可能被回收,不能存放重要数据,因此特别适合做缓存。
    LinkedHashMap一种改进的HashMap,为key增加了一个额外链表,用于维护插入或者读取顺序(而SortedMap维护的是比较顺序),两个顺序只能取其一。LinkedHashMap.Entry在HashMap.Enty基础上增加了before和after属性用于构成链表。如果需要保持的是读取顺序,那么每次 get 就会修改链表,将当前取得的元素移到队尾。
     EnumMap:使用EnumMap时,Key必须指定为枚举类型且不能为 null。它的key为枚举元素,value自定义。在工作中我们也可以用其他的Map来实现我们关于枚举的需求,但是为什么要用这个EnumMap呢?但是使用EnumMap会更加高效:它只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值。这使得EnumMap的效率非常高。
    注:迭代器和LinkedHashMap在get过程中的错误
    集合在使用迭代器过程中禁止对元素内容进行修改,而一般认为get()方法是只读的。但是对于LinkedHashMap,其为了维护读取顺序,需要修改链表,所以在迭代器中也会报错。


Set
    Set是一种不包含重复的元素的Collection, 确切地说,是不包含e1.equals(e2)的元素对。Set最多有一个null元素。很明显,Set的构造函数有一个约束条件,传入的Collection参数不能包含重复的元素。
    请注意:必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。
    Set接口有四个常见实现 HashSet、TreeSet、EnumSet、LinkedHashSet,都是基于 Map作为最终实现方式的, 分别对应 HashMap、TreeMap、EnumMap、LindexHashSet. 
    这两个 Set 都不是同步的, 所以可以通过 Set<String> synset = Collections.synchronizedSet(set); 来获取同步的 Set. 
  • Set 不重复实现原理
     以HashSet为例,在HashSet中,基本的操作都是有HashMap底层实现的,因为HashSet底层是用HashMap存储数据的。
public class HashSet<E>  extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {    
    static final long serialVersionUID = -5024744406713321676L;    
    private transient HashMap<E,Object> map;    // 底层使用HashMap来保存HashSet中所有元素。    
    private static final Object PRESENT = new Object();   // 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。    

    public HashSet() {    
        map = new HashMap<E,Object>();    //默认的无参构造器,构造一个空的HashSet。
        // 实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。   
    }    
  
    public HashSet(Collection<? extends E> c) {    
        map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));  // 构造一个包含指定collection中的元素的新set。  
        // 实际底层使用默认的加载因子0.75和足以包含指定, collection中所有元素的初始容量来创建一个HashMap。
        addAll(c); 
    }    

    public HashSet(int initialCapacity, float loadFactor) {    
        map = new HashMap<E,Object>(initialCapacity, loadFactor); // 以指定的initialCapacity和loadFactor构造一个空的HashSet。  
    }    

    public HashSet(int initialCapacity) {    
        map = new HashMap<E,Object>(initialCapacity);    // 以指定的initialCapacity构造一个空的HashSet。  
        // 实际底层以相应的参数及加载因子loadFactor为0.75构造一个空的HashMap。
    }    

    // 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。  
    // 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。 
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {    
        map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);    
    }    

    public Iterator<E> iterator() {    
        return map.keySet().iterator(); // 返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的。  
    }    

    // ……. 一下省略,size, isEmpty, contains, add, remove, clear 等等都是直接调用的HashMap的方法。
     既然是利用 HashMap 来实现的, 那么为什么还需要 HashSet?
     Set使用Map实现其功能只是一种方式罢了,CopyOnWriteArraySet可以用ArrayList实现. 实际使用中, Set 和 Map 确实不是一个场景, 利用 HashMap来模拟HashSet 的功能, 的确是可以的, 但是这样用的多了, 不如提供一个封装.Set 单独存在是有必要的, 毕竟是有特定用途的, 而 HashSet 只是恰好用 HashMap 来作为实现而已.


HashMap重要性质和优化总结
由于HashMap用的非常多,而且相对List来说结构复杂不少,同时常见的HashSet又是基于HashMap来实现的,因此需要单独讲HashMap拿出来分析。
DEFAULT_INITIAL_CAPACITY = 16;map的初始大小, 默认为16, 必须是2的幂次MAXIMUM_CAPACITY = 1 << 30;最大容量,如果指定的容量大于最大容量,将使用此值 1<<30, 必须是2的幂次.DEFAULT_LOAD_FACTOR = 0.75f;负载因子threshold;map是否扩容的决定性因素, 每次 resize 的大小都是增加 (capacity * load factor)bucket数组中最小存储单元,在源码中为Entry链表
HashMap创建到put流程基本介绍
     hashMap由其名字可以知道,它使用的是哈希算法来管理存储其中的对象的,具体是用数组和链表两种数据结构管理的。
1: 初始化
程序将利用initialCapacity计算一个新的capacity,capacity大小为大于初始容易值的最小的2的整数次幂的值(如初始容量为15,则capacity为16.初始为3,则capacity为4),

threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];

如果参数均未指定,则使用默认值初始化
2: put如果一个对象hash到同一个bucket,则会形成一个链表,链表查询是线性的。在对象放入map后,会检查map大小。如果map的size大于或等于threshold(capacity * load factor),注意不是在size大于capacity时扩容,则会以map两倍容量扩容(此步骤设计到重新申请空间和计算hash值,性能消耗比较大)
     调整 Map 实现的大小
     在哈希术语中,内部数组中的每个位置称作“存储桶”(bucket),而可用的存储桶数(即内部数组的大小)称作容量 (capacity)。为使 Map 对象有效地处理任意数目的项,Map 实现可以调整自身的大小。
     但调整大小的开销很大。调整大小需要将所有元素重新插入到新数组中,这是因为不同的数组大小意味着对象现在映射到不同的索引值。先前冲突的键可能不再冲突,而先前不冲突的其他键现在可能冲突。这显然表明,如果将 Map 调整得足够大,则可以减少甚至不再需要重新调整大小,这很有可能显著提高速度。

     使用负载因子
     基于哈希的 Map 使用一个额外参数并粗略计算存储桶的密度。Map 在调整大小之前,使用名为“负载因子”的参数指示 Map 将承担的“负载”量,即它的负载程度。负载因子、项数(Map 大小)与容量之间的关系简单明了:
如果(负载因子)x(容量)>(Map 大小),则调整 Map 大小
    例如,如果默认负载因子为 0.75,默认容量为 11,则 11 x 0.75 = 8.25,该值向下取整为 8 个元素。因此,如果将第 8 个项添加到此 Map,则该 Map 将自身的大小调整为一个更大的值。相反,要计算避免调整大小所需的初始容量,用将要添加的项数除以负载因子,并向上取整. 例如,对于负载因子为 0.75 的 100 个项,应将容量设置为 100/0.75 = 133.33,并将结果向上取整为 134.
     1.4 版后的某些 Map(如 HashMap 和 LinkedHashMap,而非 Hashtable 或 IdentityHashMap)使用需要 2 的幂容量的哈希函数,但下一个最高 2 的幂容量由这些 Map 计算,因此您不必亲自计算。
     负载因子本身是空间和时间之间的调整折衷。较小的负载因子将占用更多的空间,但将降低冲突的可能性,从而将加快访问和更新的速度。使用大于 0.75 的负载因子可能是不明智的,而使用大于 1.0 的负载因子肯定是不明智的,这是因为这必定会引发一次冲突使用小于 0.50 的负载因子好处并不大,而且将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。


参考:
http://www.tuicool.com/articles/NN7ZzqJ
http://ifeve.com/why-is-there-not-concurrent-arraylist-in-java-util-concurrent-package/
《Java程序性能优化》 葛一鸣 
原创粉丝点击