JAVA Collection集合全解

一、Collection接口

        Collection是最基本的集合接口,JDK 不提供此接口的任何直接实现:它提供更具体的子接口(如 Set 和 List)实现。此接口通
常用来传递
Collection,并在需要最大普遍性的地方操作这些Collection
常用的子接口有:
List,Queue,Set
不论Collection的实际类型如何,它都支持一个iterator()的方法,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:

Iterator it = collection.iterator(); // 获得一个迭代子
while(it.hasNext()) {
   Object obj = it.next(); // 得到下一个元素
}

下面分别来看看Collection的这些子接口

1.List接口(按插入顺序,允许有相同元素

│ List
│├LinkedList
│├ArrayList
│└Vector

List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。和下面要提到的Set不同,List允许有相同的元素
实现List接口的常用类有LinkedList,ArrayList,Vector和Stack。

①、LinkedList类
List接口的链接列表实现。

注意,此实现不是同步的。如果多个线程同时访问一个链接列表,而其中至少一个线程从结构上修改了该列表,则它必须保持外部同步。这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedList方法来“包装”该列表。最好在创建时完成这一操作,以防止对列表进行意外的不同步访问,如下所示:

List list = Collections.synchronizedList(new LinkedList(...));

LinkedList使用链表实现:

//节点定义
private static class Node<E> {
    E item;
    //指向下一个节点的指针
    Node<E> next;
    //指向前一个节点的指针
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

/**
插入操作
在链表末尾插入元素
*/
void linkLast(E e) {
    final Node<E> l = last;
    //让新加入的节点成为链表最后节点
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}


②、ArrayList类
List接口的大小可变数组的实现。
每个 ArrayList 实例都有一个 容量 。指用来存储列表元素的数组的大小。随着向ArrayList中不断添加元素,其容量也自动增长。如下是ArrayList容量增加的方法:

private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        .......
        elementData = Arrays.copyOf(elementData, newCapacity);
}

可以看到如果要容量不够用的话,新容量 = 旧容量+旧容量/2
并用Arrays.cpoyOf根据新容量生成一个新的数组
所以在添加大量元素前,应用程序可以先使用ensureCapacity操作来增加ArrayList实例的容量。这可以减少递增式再分配的数量。

注意,此实现不是同步的。如果多个线程同时访问一个链接列表,而其中至少一个线程从结构上修改了该列表,则它必须保持外部同步。这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedList方法来“包装”该列表。最好在创建时完成这一操作,以防止对列表进行意外的不同步访问,如下所示:

List list = Collections.synchronizedList(new ArrayList(...));

③、Vector类

Vector类与ArrayList类似,List接口的大小可变数组的实现。但是,Vector扩容行为和ArrayList有区别

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        ...
        elementData = Arrays.copyOf(elementData, newCapacity);
}
可以看到如果初始化Vector时定义了capacityIncrement,如下面这样定义的话,每次vector需要扩容就增加1000个容量

Vector vector = new Vector(10,1000);

如果没有定义capacityIncrement的话,vector默认扩容行为为:新容量 = 旧容量+旧容量
Vector每个方法是同步的。但复合操作还需要额外同步来保证线程安全
考虑下面的代码片段,它迭代一个 Vector 中的元素。尽管 Vector 的所有方法都是同步的,但是在多线程的环境中不做额外的同步就使用这段代码仍然是不安全的
Vector v = new Vector();
//contains race conditions -- may require external synchronization
for (int i=0; i<v.size(); i++) {
    doSomething(v.get(i));
}


2.Set接口(某些实现不按插入顺序排序,不允许有相同元素

│ Set
│├HashSet
│├LinkedHashSet
│└TreeSetSet是一种不包含重复的元素的Collection,Set最多有一个null元素。并且Set元素排放不按插入顺序排序。比如Set接口的实现HashSet,排放顺序是根据元素的Hash值。

①、HashSet

此类由哈希表(实际上是一个HashMap实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。

public HashSet() {
    map = new HashMap<>();
}

HashSet 的内部采用 HashMap来实现。由于 Map 需要 key 和 value,所以所有 key 的都有一个默认 value。

注意,此实现不是同步的。如果多个线程同时访问一个HashSet,而其中至少一个线程修改了该 set,那么它必须保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用Collections.synchronizedSet方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:

Set s = Collections.synchronizedSet(new HashSet(...));
②、LinkedHashSet

此类由一个LinkedHashMap支持。具有可预知迭代顺序的Set接口的哈希表和链接列表实现。此实现与HashSet的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,即按照将元素插入到set中的顺序(插入顺序)进行迭代。

注意,此实现不是同步的。如果多个线程同时访问一个LinkedHashSet,而其中至少一个线程修改了该 set,那么它必须保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用Collections.synchronizedSet方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:

Set s = Collections.synchronizedSet(new LinkedHashSet(...));

③、TreeSet

基于TreeMapNavigableSet实现。使用元素的自然顺序对元素进行排序,或者根据创建set时提供的Comparator进行排序,具体取决于使用的构造方法。

注意,此实现不是同步的。如果多个线程同时访问一个TreeSet,而其中至少一个线程修改了该 set,那么它必须保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用Collections.synchronizedSet方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:

SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

二、Map接口

Map
├Hashtable
├HashMap
└WeakHashMap

        Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个 value。Map接口提供3种集合的视图,Map的内容可以被当作一组key集合一组value集合或者一组key-value映射

①、Hashtable类

        Hashtable实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。
Hashtable通过initial capacity和load factor两个参数调整性能。通常缺省的load factor 0.75较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。
  如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。
  Hashtable是同步的。

②、HashMap类

HashMap和Hashtable类似,不同之处在于HashMap是非同步的并且允许null,即null value和null key,但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap 的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低

再看一篇文章了解一下HashMap的源码:

HashMap源码剖析

说道HashMap和Hashtable顺便说一下为什么覆写一个对象的equeals方法的时候,最好同样覆写hashCode方法。保证equals相等的对象hashCode方法也相等:

为什么覆写equals的时候一定要覆写hashCode?

③、WeakHashMap类

WeakHashMap是一种改进的HashMap,它对key实行“弱引用”,如果一个key不再被外部所引用,那么该key可以被GC回收

三、总结

        1、如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。
        2、如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。
        3、要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。
   4、尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。
        5、
Vector是同步的。这个类中的一些方法保证了Vector中的对象是线程安全的。而ArrayList则是异步的,因此ArrayList中的对象并 不是线程安全的。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合那么使用ArrayList是一个很好的选择,这样可以避免由于同步带 来的不必要的性能开销。
        6、数据增长,从内部实现机制来讲ArrayList和Vector都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,如果元素的数目 超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。
        7、使用模式,在ArrayList和Vector中,从一个指定的位置(通过索引)查找数据或是在集合的末尾增加、移除一个元素所花费的时间是一样的,这个时间我们用 O(1)表示。但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长:O(n-i),其中n代表集合中元素的个数,i代表元素增加或移除 元素的索引位置。为什么会这样呢?以为在进行上述操作的时候集合中第i和第i个元素之后的所有元素都要执行位移的操作。这一切意味着什么呢?
这意味着,你只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用Vector或ArrayList都可以。如果是其他操作,你最好选择其他 的集合操作类。比如,LinkList集合类在增加或移除集合中任何位置的元素所花费的时间都是一样的?O(1),但它在索引一个元素的使用缺比较慢 -O(i),其中i是索引的位置.使用ArrayList也很容易,因为你可以简单的使用索引来代替创建iterator对象的操作。LinkList也 会为每个插入的元素创建对象,所有你要明白它也会带来额外的开销。
最后,在《Practical Java》一书中Peter Haggar建议使用一个简单的数组(Array)来代替Vector或ArrayList。尤其是对于执行效率要求高的程序更应如此。因为使用数组 (Array)避免了同步、额外的方法调用和不必要的重新分配空间的操作。


参考:java源码、JDK文档、Java集合类详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值