第七章:深入集合Collection

7.1 集合框架与ArrayList

如图1所示,是Java常见的一些集合框架的类。最上面是Collection,它有两个子接口,List和Set。这些List有一些类来实现这个借口,比如说AbstractList,在AbstractList下面还有Vector Stack,还有ArrayList等。但是对于set这个接口来说,下面也实现了很多类。最右端是Map这个借口,实现它的有HashMap等等。

1523867566600983.jpg

图1

1.ArrayList

List借口的可变数组实现。实现了所有可选列表操作,并允许包括null在内的所有元素,它是非线程安全。它的底层使用的数据结构是数组,适合查找修改,弱于删减

例:ArrayList实现分析

//用指定的元素替代此列表中指定位置上的元素//并返回以前位于该位置上的元素public E set(int index, E element){       RangeCheck(index);//检查index是否合法             E oldValue = (E) elementData[index];       elementData[index] = element;       return oldValue;}//将指定的元素添加到此列表的尾部//直接添加速度快public boolean add(E e){       ensureCapacity(size+1);//确保容量满足       element[size++] = e;       return true;}

第四行是用来检查index是否符合范围要求。下面这一行取出index值保存在oldValue中,将下标为index的值设置为新的,然后把旧的元素值返回。

下面是增加元素的方法。首先确保还有一个额外容量,然后把值赋值给最后一个元素,返回true代表增加成功了。

//将指定的元素插入此列表中的指定位置。//如果当前位置有元素,则右移当前位于该位置上的元素//以及后续所有元素(即将索引加1)//涉及数组拷贝,插入速度不及add方法public void add(int index, E element){       if(index>size)||index <0){              throw new IndexOutOfBoundsException("Index:"+index+",              Size:"+size);              }       //如果数组长度不足,进行扩容       ensureCapacity(size+1);       System.arraycopy(elementData, index,elementData,index+1,size-index);       elementData[index] = element;       size++;}

首先判断下标index是否大于尺寸,或者index小于0,这就说明数组长度不足,需要对数组进行扩容,这个时候就抛出一个错误来。接下来确定数组的容量是能够容纳的。arraycopy就是把列表中若干值进行拷贝,也就是说所有元素往后拷贝1位。然后把值复制给空的那个地方。这样的操作代价还是很大的,所以性能并不好。

//数组扩容,1.5倍方式扩容。涉及数组拷贝,速度慢public void ensureCapacity(int minCapacity){       modCount++;       int oldCapacity = elementData.length;       if(minCapacity > oldCapacity){              int newCapacity = (oldCapacity*3)/2+1;              if(newCapacity < minCapacity)                     newCapacity = minCapacity;       elementData = Arrays.copyOf(elementData, newCapacity);       }}

    获取旧容量大小给oldcapacity。如果指定最小的容量比原有还要大,那么先扩大1.5,如果扩充后这个长度比需要的最小长度还要小,那么直接设置为mincapacity的大小。然后把原来列表的数据直接复制给新的数组。

7.2 LinkedList

LinkedList是List借口的链接列表实现,它可以实现所有的可选列表操作,并且允许所有元素(包括null),它实现Deque借口,为add、poll提供FIFO队列操作以及其他堆栈和双端队列操作。该实现是非线程安全的。当我们在编程序时,如果用到这个链表,并且是多线程去访问这个链表的时候,需要自己加XX(没听清),LinkedList不做同步与互斥来控制,需要你自己写程序来解决这个问题。它适合增减,弱于查改

例:LinkedList数据结构

private transient Entry<E> header = new Entry<E>(null,null,null);private static class Entry<E>{       E element;       Entry<E> next;       Entry<E> previous;       /***       *构造方法:目标对象paramE将被防止在paramEntry1之前,       *paramEntry2之后       */       Entry(E paramE, Entry<E>paramEntry1,Entry<E>paramEntry2){              this.element = paramE;              this.next = paramEntry1;              this.previous = paramEntry2;       }}

header是链表的头,三个空在后面有指。这个结点中包含三个元素,element是值,next是当前节点的下一个节点是什么,previous是前一根节点是什么。后面的构造方法指定了这三个元素的内容。

//根据序号获取Entry对象private Entry<E> entry(int paramint){       if((paramint<0)||(paramint>=this.size)){              throw new indexOutOfBoundsException("Index:"+paramint+",              size:"+this.size);       }       Entry localEntry = this.header;       int i;       //最多遍历size/2个元素       if(paramint<this.size>>1){              for(i = 0; i<=paramint;i++)                     localEntry = localEntry.next;       }else{              for(i=this.size;i>paramint;i--)                     localEntry = localEntry.previous;       }       return localEntry;}

这个方法是用来根据给定序号获取这个节点的值。首先判断序号是否越界,若越界则抛出异常类,否则继续进行。然后先设置链接到链表的头部,也就是从头部开始进行遍历。然后开始判断所给定的序号比列表一半的长度小还是长度达。若比长度右移一位(this.size>>1代表右移一位,实际上也就代表着长度除以2,这是我们在学二进制的时候就学过的)小,那么就从表头开始,一步一步往下找,直到找到我们需要的序号的元素,然后输出值。若大于,则同理。

/****要添加的元素:paramE*目标对象:paramEntryEntry*特点:插入速度快*/private Entry<E> addBefore(E paramE, Entry<E> paramEntry){       //要添加的对象,设置其previous和next       Entry localEntry = new Entry(paramE, paramEntry, paramEntry.previous)             localEntry.previous.next=localEntry;       localEntry.next.previous = localEntry;             this.size+=1;       this.modCount+=1;       return localEntry;}

实例化一个新的节点,这个时候我们必须指定这个结点放在哪里。

//指定位置添加元素,需要先找到index的元素,然后添加public void add(int index, E element){       addBefore(element,(index==size?header:entry(index)));}//队首添加元素public void addFirst(E paramE){       addBefore(paramE,this.header.next);}//队尾添加元素public void addLast(E paramE){       addBefore(paramE, this.header);} public E remove(){       return removeFirst();}//删除第一个public E remove(int index){       return remove(entry(index));}//删除第index个元素public E removeFirst(){       return remove(header.next);}public E removeLast(){       return remove(header.previous);}//删除最后一个

List的适用范围

(1)ArrayList适用于对于数据查询修改大于数据增加删除的场合;

(2)LinkedList适用于对于数据增删大于数据查询的场合。

7.3 HashMap与HashTable

1.HashMap(哈希映射)

它是基于哈希表的Map借口的实现,提供所有可选的映射操作,并允许使用null值和null键。它是非线性安全的。它不保证映射的顺序,特别是不保证顺序恒久不变的。HashMap的数据结构如图2所示。

1523867648129678.jpg

图2

代码描述版:

transient Entry[] table; static class Entry<K,V> implements Map.Entry<K,V>{       final K key;       V value;       Entry<K,V> next;       final int hash;       //......剩余代码}

第一行就是设置一个数组,类型是Entry。第二行说明这个Entry。后面说明是Entry是一个列表等等。

总结来说,HashMap底层就是一个数组结构,数组中的每一项又是一个链表,先建一个的时候就会初始化这么一个数组,Entry就是数组中的元素,那么每个Map Entry就是一个K value对,它只有一个向下的指针来指示我们下一个元素,这就构成了链表。

例:如何向HashMap中添加元素

public V put(K key, V value){       if(key==null)//null键视为相同的键              return putForNullKey(value);//根据key的keycode重新计算hash值       int hash = hash(key.hashCode());//搜索指定hash值在对应table中索引       int i = indexFor(hash,table.length);//如果i索引处的Entry不为null,通过循环不断遍历e元素的下一个元素       for(Entry<K,V>e=table[i];e!=null;e=e.next){              Object k;//key重复出现则更新其value              if(e.hash ==hash&&((k=e.key)==key||key.equals(k))){                     V oldValue = e.value;                     e.value =value;                     e.recordAccess(this);                     return oldValue;              }       }       modCount++;       addEntry(hash,key,value,i);       return null;}

例:hash函数,加入高位计算,防止地位不变,高位变化时,造成hash冲突

代码:

static int hash(int h){       h^=(h>>>20)^(h>>>12);       return h^(h>>>7)^(h>>>4);} void addEntry(int hash, K key, V value, int bucketIndex){       //获取指定bucketIndex索引处的Entry       Entry<K,V>e=table[bucketIndex];       //将新创建的Entry放入bucketIndex索引处,并让新的Entry指向原来的Entry       table[bucketIndex] = new Entry<K,V>(hash,key,value,e);       //如果Map中的key-value对的数量超过了极限       if(size++>=threshold)              //把table对象的长度扩充到原来的2倍              resize(2*table.length);}//根据hash值查找对应的table位置static int indexFor(int h, int length){       return h&(length-1);}

例:HashMap的get操作

public V get(Object key){       if(key==null)              return getForNullKey();       int hash =hash(key.hashCode());       for(Entry<K,V>e = table[indexFor(hash,table.length)];       e!=null;       e=e.next){              Object k;              if(e.hash==hash&&((k=e.key)==key||key.equals(k)))                     return e.value;       }       return null;}

2.HashTable(哈希表)

    HashTable和HashMap采用相同的存储机制,二者的实现基本一致。但是HashTable不允许NULL存在。HashTable是线程安全的,内部的方法基本都是synchronized。它的迭代器具有强一致性。

7.4 TreeMap与LinkedHashMap

1.TreeMap

它实际上是Map接口的树实现,不允许null值存在,非线性安全,键值有序。它的数据结构实际是采用了红黑二叉树,是一个自平衡二叉查找树,在进行插入和删除操作时,通过特定的操作能够保持二叉查找树的平衡,从而获得较高的查找性能。它的优点是做查找、插入和删除操作的时间是O(logn)。

如图3所示,Entry是红黑树的结点,它包含了红黑树的6个基本组成成分:key、value、left(左孩子)、right(右孩子)、parent、color。Entry节点根据key进行排序,本身entry节点包含的内容为value。红黑树排序时,根据entry中的key进行排序,entry中的key比较大小是根据比较器comparator来进行判断的。

1523867700555969.jpg

图3

TreeMap的优点:

(1)空间利用率高:HashMap的数组大小必须为2的n次方;TreeMap中树的每一个节点就代表了一个元素。

(2)性能稳定:Hash碰撞会导致HashMap查询开销提高;HashMap扩容时会rehash,开销高;TreeMap的操作均能在O(logn)内完成。

2.LinkedHashMap

Map接口的哈希表和链接列表实现,提供所有可选的映射操作,并允许使用null值和null键,非线程安全,具有可预知的迭代顺序。

例:LinkedHashMap实现分析

如图4所示。

1523867722709780.jpg

图4

HashMap第一级是数组,第二级是单向链表;而这个第二级结构是一个双向链表。

3.Map的适用范围

(1)HashMap适用于一般的键值映射需求;

(2)HashTable适用于有多线程并发的场合;

(3)TreeMap适用于要按照键排序的迭代场合;

(4)LInkedHashMap适用于特殊顺序的迭代场合(如LRU算法)。

7.5 HashSet

1.HashSet

它实现Set借口,由哈希表支持,允许使用null元素。非线程安全,不保证set的迭代顺序,特别是不保证该顺序恒久不变。

例:HashSet的实现分析

1523867741571214.jpg

图5

2.Set的特点

(1)HashSet通过HashMap实现;

(2)TreeSet通过TreeMap实现;

(3)LinkedHashSet通过LinkedHashMap实现;

(4)Set类与Map类拥有近似的使用特性。