1、set和Map的关联之处

  Set代表无序,不可重复的集合,Map代表由多个key-value对组成的集合,Map集合类似于传统的关联数组。表面上相似性很少,实际上Map和Set之间莫大关联,可以说Map是Set的扩展。

java 集合性能contains java集合实现原理_ci

Map是一个关联数组,但是如果将value看成key的附属,捆绑在一起。

将Set拓展成Map:

java 集合性能contains java集合实现原理_结点_02

View Code

 

2、HashMap和HashSet

  前面将一个Set集合拓展成了Map集合,由于这个Set采用了HashSet作为实现类,HashSet会使用Hash算法保存集合中每个SimpleEntry,因此扩展出的Map本质是一个HashMap。

  HashSet和HashMap相似之处多。HashSet,系统采用Hash算法决定元素的存储位置,这样保证快速存取集合元素,对于HashMap,将value当成key的附属,根据Hash算法决定key的存储位置,也保证快速存取集合key,value紧随key存储。

  集合号称存储的是Java对象,实际上存储的是java对象的引用变量。

 

  HashMap:

    放入HashMap的代码片段如下:

java 集合性能contains java集合实现原理_结点_02

View Code

HashMap比ArrayList负责一点:采用一种所谓的Hash算法决定每个元素的存储位置。

HashMap类的put(K key, V value)方法的源码:

public V put(K key, V value){
        if( key == null){
            return putForNullKey(value);
        }
        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;
            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;
    }

Map.Entry就是一个key-value对。

addEntry(hash, key, value, i)方法:

public void addEntry(int hash, K key, V value, int bucketIndex){
        Entry<K, V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
        if(size++ >= threshould){
            resize(2 * table.length);
        }
    }

size:该变量保存了该HashMap中所包含的key-value对的数量

threshould:包含了HashMap所能容纳的key-value对的极限,值等于HashMap的容量乘以负载因子(load factor)

 

上面程序可以知道,当size++ >= threshould时,HashMap会扩充HashMap的容量,每一次增大一倍。

上面使用的table就是一个普通数组,这个数组的长度就是HashMap的容量。

构造:

HashMap():构建一个初始容量为16,负载因子为0.75的HashMap
HashMap(int initialCapacity):构建一个初始容量为initialCapacity,负载因子为0.75的HashMap
HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个HashMap

一个构造的源码:

public HashMap(int initialCapacity, float loadFactor){
        if(initialCapacity < 0){
            throw new IllegalArgumentException("Illegal initial capacity initialCapacity");
        }
        if(initialCapacity > MAXIMUM_CAPACITY){
            initialCapacity = MAXIMUM_CAPACITY;
        }
        if(loadFactor <=0 || Float.isNaN(loadFactor)){
            throw new IllegalArgumentException("Illegal load factor:" + loadFactor);
        }
        int capacity = 1;
        while(capacity < initialCapacity){
            capacity <<= 1; //左移动
        }
        this.loadFactory = loadFactor;
        threshould = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }

上面代码包含一个简洁的代码实现:找出大于initialCapacity的、最小的2的n次方值,并将其作为HashMap的实际容量(由capacity变量保存)。例如给的initialCapacity为10,那么HashMap的实际容量为16.

可以看出,table的实质是一个数组,长度为capacity的数组。

小技巧:通俗来说HashMap的实际容量比initialCapacity大一点,除非指定的initialCapacity参数恰好是2的n次方,所以我们构建HashMap的时候将initialCapacity参数指定为2的n次方可以节省系统开支。

对于HashMap及其子类而言,采用Hash算法决定集合中元素的存储位置。初始化HashMap的时候,系统会创建一个长度为capacity的Entry数组。这个数组里可以存储的元素的位置被称为“桶(bucket)“,每个bucket都具有指定索引,系统根据索引快速访问bucket里的元素。

  无论何时,HashMap的每个”桶“只存储一个元素(一个Entry)。如果Entry对象包含引用变量指向下一个Entry,可能出现一个Entry链。

get(Object):

public V get(Object key){
        if(key == null){
            return getFullKey();
        }
        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;
    }

当创建HashMap时,有一个默认的负载因子(load factor),默认值是0.75.这是时间和空间成本上的一种折衷:增大负载因子可以减少Hash表所占用的内存,但会增加查询数据的事件开销;较小负载因子会提高数据查询的性能,但会降低Hash表所占用的内存空间。

 

HashSet而言,它是基于HashMap实现的。源码:

public class HashSet1<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable{
    private transient HashMap<E, Object> map;
    private static final Object PRESENT = new Object();
    ...
    public HashSet(){
        map = new HashMap<E, Object>();
    }
    public HashSet(int initialCapacity){
        map = new HashMap<E, Object>(initialCapacity);
    }
    HashSet(int initialCapacity, float loadFactory, boolean dumy){
        map = new LinkedHashMap<E, Object>(initialCapacity, loadFactor);
    }
    public Iterator<E> iterator(){
        return map.keySet().iterator();
    }
    public int size(){
        return map.size();
    }
    public boolean isEmpty(){
        return map.isEmpty();
    }
    public boolean contains(Object o){
        return map.containsKey(o);
    }
    public boolean add(E e){
        return map.put(e, PRESENT) == null;
    }
    public boolean remove(Object o){
        return map.remove(o) == PRESENT;
    }
    public void clear(){
        map.clear();
    }
    ...
}

使用HashSet的小例子:

java 集合性能contains java集合实现原理_结点_02

View Code

上面输出的时false,这是因为HashSet判断两个对象标准除了要通过equals()比较返回true之外还需要两个对象的hashCode()相等。所以将某个类的对象当成HashMap的key,试图将这个类的对象放入HashSet中保存时,重写该类的equals(Object obj)方法和hashCode()方法很重要,而且这两个方法的返回值必须一致。

 

3、TreeSet和TreeMap
类似于前面的HashMap和HashSet的关系,HashSet底层依赖于HashMap实现。TreeSet底层采用一个NavigableMap来保存TreeSet集合的元素。实际上由于NavigableMap只是一个接口,因此底层仍然时使用TreeMap来包含Set集合中的所有元素。

TreeSet部分源码:

java 集合性能contains java集合实现原理_结点_02

View Code

TreeSet底层容器就是TreeMap

 

对于TreeMap而言,它采用一种被称为”红黑树“的排序二叉树来保存,Map中每一个Entry都被当成”红黑树”的一个节点对待。

java 集合性能contains java集合实现原理_Code_06

简单介绍红黑树特性:

  

java 集合性能contains java集合实现原理_结点_07

就这张图吧,因为我对它不是很了解,左边小,右边大,二叉树。

然后我们的TreeMap结构就像前面看HashMap提到的Entry链。

TreeMap是有序的,有点类似插入排序,当然二叉排序树算法不是简单的插入排序。TreeMap底层采用红黑树保存集合中的Entry,意味着TreeMap添加、取出元素性能都比HashMap低。存取元素都需要循环找到合适的位置,比较耗内存。但TreeMap、TreeSet相比HashMap、HashSet的优势在于它们按key根据指定顺序规则保持有序状态。

TreeMap的put方法,实现将Entry放入TreeMap的Entry链,并保证Entry有序:

java 集合性能contains java集合实现原理_结点_02

View Code

TreeMap的get方法:

java 集合性能contains java集合实现原理_结点_02

View Code

当TreeMap里面的comparator != null,即表明该TreeMap采用了定制排序。定制排序的时候使用getEntryUsingComparator(key)得到Entry:

java 集合性能contains java集合实现原理_结点_02

View Code

通过源码分析不难看出,TreeMap本质上就是一棵“红黑树”,而TreeMap的每个Entry就是该红黑树的一个节点。

 

Map和List

  Map集合是一个关联数组,它包含两组值:一组是所有key组成的集合,因为Map集合的key不允许重复,而且Map不会保存key加入的顺序,因此这些key可以组成一个Set集合;另外一组是value组成的集合,因为Map集合的value完全可以重复,而且Map可以根据key来获取对应的value,所以这些value可以组成一个List集合

 例子:

java 集合性能contains java集合实现原理_结点_02

View Code

执行结果:

java 集合性能contains java集合实现原理_结点_12

说明Map的values()并不是一个List集合,是各自集合的Values内部类。

看HashMap和TreeMap的values代码源码:

public Collection<V> values(){
        Collection<V> vs = values;
        return (vs != null ? vs : (values = new Values()) );
    }
    public Collection<V> values(){
        Collection<V> vs = values;
        return (vs != null ? vs : (values = new Values()));
    }

values的返回值主要体现在各自的内部类上。

HashMap的内部类以及其它关键代码:

java 集合性能contains java集合实现原理_结点_02

View Code

TreeMap的values类:

java 集合性能contains java集合实现原理_结点_02

View Code

TreeMap中对于红黑树的获取上一个节点和下一个节点:

java 集合性能contains java集合实现原理_结点_02

View Code

为了保证Map的一致性,遍历values还是用的iterator:

java 集合性能contains java集合实现原理_结点_02

View Code

归纳:不管是HashMap还是TreeMap,它们的values()方法都可以返回其所有value组成的Collection集合---展昭这个理解,这个Collection集合应该是一个List集合,因为Map的多个value允许重复,但是HashMap、TreeMap的values()方法的实现更巧妙,这两个Map对象values()返回的是一个不存储元素的Collection集合,当集合遍历Collection集合时,实际上就是遍历Map对象的value.

 

Map和List的关系从底层实现上看,Set和Map很相似,但是从用法上看,Map和List也有很大的相似之处:

  Map接口提供了get(K key)方法允许Map对象根据key来取得value

  List接口提供了get(int index)方法允许List对象根据元素索引来取得value

java 集合性能contains java集合实现原理_结点_17

队列:

  参照排队:先进先出

栈:

  参照子弹夹:先进后出

链表:

  线性表的链式存储表示的特点是用一组任意的存储单元存储线性表数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素 与其直接后继数据元素 之间的逻辑关系,对数据元素 来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。由这两部分信息组成一个"结点"(如概述旁的图所示),表示线性表中一个数据元素。线性表的链式存储表示,有一个缺点就是要找一个数,必须要从头开始找起,十分麻烦。

  由分别表示,,…,的N 个结点依次相链构成的链表,称为线性表的链式存储表示,由于此类链表的每个结点中只包含一个指针域,故又称单链表或线性链表。

 

循环链表是与单链表一样,是一种链式的存储结构,所不同的是,循环链表的最后一个结点的指针是指向该循环链表的第一个结点或者表头结点,从而构成一个环形的链。

 

双向链表其实是单链表的改进。

当我们对单链表进行操作时,有时你要对某个结点的直接前驱进行操作时,又必须从表头开始查找。这是由单链表结点的结构所限制的。因为单链表每个结点只有一个存储直接后继结点地址的链域,那么能不能定义一个既有存储直接后继结点地址的链域,又有存储直接前驱结点地址的链域的这样一个双链域结点结构呢?这就是双向链表

在双向链表中,结点除含有数据域外,还有两个链域,一个存储直接后继结点地址,一般称之为右链域;一个存储直接前驱结点地址,一般称之为左链域。

 

3.3ArrayList和LinkedList

  List集合的实现类中,主要有3个实现类:ArrayList、Vector和LinkedList。其中Vector还有一个Stack子类,这个Stack子类增加了5个方法,将一个Vector扩展成了Stack。

java 集合性能contains java集合实现原理_结点_02

View Code

实际上即时当程序中需要栈这种数据结构时,Java也不再推荐使用Stack类,而是推荐使用Deque实现类。从JDK1.6开始,java提供了一个Deque接口,并为该接口提供一个ArrayDeque实现类。在无需保证线程安全的情况下,程序完全可以使用ArrayDeeue类来替代Stack类。

Deque接口代表双端队列这种数据结构。双端队列已经不再是简单的队列了,它既具有队列的性质先进先出(FIFO),也具有栈的性质(FILO),感觉就是在Stack类进行拓展,删掉了一些同步synchronized。

3.3.1 Vector和ArrayList的区别

  两者本质没有什么太大不同,都实现了List接口,而且底层都是基于java数组来存储集合元素。

在ArrayList集合类源码中可以看到下面一行:
private transient Object[] elementData;

在Vector集合类的源码中也可以看到类似的一行:
protected Object[] elemetData;

ArrayList使用transient修饰了elementDate数组。这保证系统序列化ArrayList对象时不会直接序列化elementDate数组,而是通过ArrayList提供的writeObject、readObject方法来实现定制序列化;但对于Vector而言,它没有使用transient修饰elementDate数组,而且Vectr只提供了一个writeObject方法,并未完全实现定制序列化。

ArrayList的序列化实现比Vector安全。

其他地方没有太大区别,一点细节差距和同步差距罢了。

两个的增加元素方法以及扩充容量方法:

java 集合性能contains java集合实现原理_结点_02

View Code

看出来,默认ArrayList一次增加0.5倍,Vector一次增加1倍

Vector多了一个选择,如果指定了capacityIncrement的值,并且不等于null的话,每次扩充都扩充这个长度。

Vector是一个非常古老的集合在JDK1.0就开始存在了,所以存在大量方法。

 

3.3.2 ArrayList和LinkedList的差异

  List代表一种线性表的数据结构,ArrayList是一种顺序存储的线性表。ArrayList底层采用数组来保存每个集合元素,LinkedList则是一种链式存储的线性表。其本质上就是一个双向链表,但它不仅实现了List接口,还实现了Deque接口。也就是说LinkedList既可以当成双向链表是哟哪个,也可以当成队列使用,还可以当成栈来使用。

  ArrayList底层采取一个elementData数组保存所有的额集合元素,因此ArrayList插入元素时需要完成下面两件事:

    保证ArrayList封装的数组长度大于集合元素的个数

    将插入位置之后的所有数组元素“整体搬家”,向后移动一“格”

ArrayList的增删慢,查询快,源码:

public E remove(int index){
    RangeCheck(index);
    modCount++;
    E oldValue = (E)elementData[index];
    int numMoved = size - index -1;
    if(numMoved > 0){
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    }
    elementData[--size] = null;
    return oldValue;
}
public E get(int index){
    RangeCheck(index);
    return (E) elementData[index];
}

LinkedList本质上就是一个双向链表,因此它使用如下内部类来保存每个集合元素:

private static class Entry<E>{
    E element;
    Entry<E> next;
    Entry<E> previous;
    Entry(E element, Entry<E> next, Entry<E> previous){
        this.element = element;
        this.next = next;
        this.previous = previous;
    }
}

添加删除查找元素的代码:

java 集合性能contains java集合实现原理_结点_02

java 集合性能contains java集合实现原理_java 集合性能contains_21

public void add(int index, E element){
    addBefore(element, (index == size ? header : entry(index)));
}
//entry(int index):搜索指定索引处的元素
//addBefore(E element, Entry ref):在ref节点之前插入element新节点
private Entry<E> entry(int index){
    if(index < 0 || index >= size){
        throw new IndexOutBoundsException("Index:" + index + ",Size:" + size);
    }
    Entry<E> e = header;
    if(index < (size >> 1)){
        for(int i = 0; i <= index; i++){
            e = e.next;
        }
    }else{
        for(int i = size; i > index; i--){
            e = e.previous;
        }
    }
    return e;
}
public E get(int index){
    return entry(index).element;
}
private Entry<E> addBefore(E e, Entry<E> entry){
    Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
    newEntry.previous.next = newEntry;
    newEntry.next.previous = newEntry;
    size++;    
    modCount++;
    return newEntry;
}
public E remove(int index){
    return remove(entry(index));
}
private E remove(Entry<E> e){
    if(e == header){
        throw new NoSuchElementException();
    }
    E result = e.element;
    e.previous.next = e.next;
    e.next.previous = e.previous;
    //将被删除的两个引用、元素都赋为null,以便垃圾回收
    e.next = e.previous = null;
    e.element = null;
    size--;
    modCount;
    return result;
}

View Code

对于ArrayList直接使用下表就能取出指定位置的元素;但对于LinkedList就比较麻烦了,LinkedList必须一个一个元素地搜索,知道找到第index个元素。

单纯的添加某个节点,LinkedList的性能会非常好,如果在指定索引处添加节点,LinkedList必须先找到指定所引处的节点--搜索过程的开销不小,因此LinkedList的add(int index,E element)党法的诶性能并不是特别好。

 

ArrayList和LinkedList的性能分析和适用场景。

  程序需要get(int index)方法获取List集合指定索引处的元素时,ArrayList性能大大优于LinkedList。

  程序调用add(int index,Object obj)向List集合中添加元素时,ArrayList必须对底层数组元素进行“整体搬家”。如果添加元素导致长度草果底层数组长度,ArrayList必须建立一个长度1.5倍的数组,再垃圾回收回收原数组,因此系统开销大,而LinkedList开销主要集中在entry(int index)方法上,该方法一个一个搜索过去,即使如此,LinkedList的性能依然高于ArrayList

  程序使用remove(int iondex)删除与增加类似。

  当程序把LinkedList当成双端队列、栈使用,LinkedList总是具有较好的性能表现

 

Iterator迭代器

  Iterator是一个迭代器接口,它专门用于迭代各种Collection集合,包括Set集合和List集合

public class Test1 <V>{
    enum Gender{
        MALE, FEMALF;
    }

    public static void main(String[] args){
        HashSet<String> hashSet = new HashSet<String>();
        System.out.println("HashSet的Iterator:" + hashSet.iterator());
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<String>();
        System.out.println("LinkedHashSet的Iterator:" + linkedHashSet.iterator());
        TreeSet<String> treeSet = new TreeSet<String>();
        System.out.println("TreeSet的Iterator:" + treeSet.iterator());
        EnumSet<Gender> enumSet = EnumSet.allOf(Gender.class);
        System.out.println("EnumSet的Iterator:" + enumSet.iterator());
        ArrayList<String> arrayList = new ArrayList<String>();
        System.out.println("ArrayList的Iterator:" + arrayList.iterator());
        Vector<String> vector = new Vector<String>();
        System.out.println("Vector的Iterator:" + vector.iterator());
        LinkedList<String> linkedList = new LinkedList<String>();
        System.out.println("LinkedList的Iterator:" + linkedList.iterator());
        ArrayDeque<String> arrayDeque = new ArrayDeque<String>();
        System.out.print("ArrayDeque的Iterator:" + arrayDeque.iterator());

    }


}

运行发现,Iterator在各个集合有各自的内部实现类:

java 集合性能contains java集合实现原理_Code_22

 

上面结果看出来,EnumSet集合的Iterator瑟吉欧RegularEnumSet的内部类之外,其他的Set集合对应的Iteratoe都是它对应的Map类的内部类KeyIteratoe。

ArrayList和Vector的实现基本相同,只是线程安全的区别,因此他两的对应的Iterator是相同的,即AbstractList的内部类Itr。...

通过上面介绍不难发现,对于Iterator迭代器而言,它只是一个接口。java要求各个集合都提供一个iterator()方法,该方法可以返回一个Iterator用来遍历该集合中元素,至于返回的Iterator类型不关心,这是典型的"迭代器模式"。

所谓的迭代器模式指的是,系统为遍历多种数据列表、集合、容器提供一个标准的迭代器接口,这些数据列表、集合、容器就可面向相同的迭代器接口编程,通过相同的迭代器接口访问不同的数据列表、集合、容器里的数据。不同的数据列表、集合、容器如何实现这个迭代器接口则交给给数据列表、集合、容器自己完成。

 

迭代器删除指定元素

  由于Iterator迭代器只负责对各种集合所包含的元素进行迭代,它自己并没有保留集合元素,因此使用Iterator进行迭代时,通常不应该删除集合元素,否则将引发ConcurrentModificationException异常。当然,java允许通过Iterator提供的remove()方法删除刚刚迭代的集合。

  但实际在某些特殊情况下,可以在使用Iterator迭代集合时直接删除集合中某个元素。

public class ArrayListRemove {
    public static void main(String[] args){
        ArrayList<String> list = new ArrayList<String>();
        list.add("111");
        list.add("222");
        list.add("333");
        for(Iterator<String> it = list.iterator(); it.hasNext(); ){
            String ele = it.next();
            if(ele.equals("222")){
                list.remove(ele);
            }
            if(ele.equals("333")){
                list.remove(ele);
            }
            /**
             * 上面不报错,下面报错
             */
            if(ele.equals("111")){
                list.remove(ele);
            }
        }
    }
}

 

可以发现在上面的程序中删除最后一个元素或者删除倒数第二个元素都不会报错,但是如果删除的是其他的元素,就会抛出:ConcurrentModificationException异常。

总结:

  对于ArrayList、Vector、LinkedList等List集合而言,当使用Iterator遍历它们时,如果正在遍历倒数第2个集合元素或者更后,使用List集合的remove()方法删除集合的任意一个元素并不会引发ConcurrentModificationException异常,正在遍历时删除其他元素就会引发异常。

这是因为:

List集合对应的Iterator的实现类(Itr)的hasNext()方法:

public boolean hasNext(){
  return cursor != size();  
}

对于Itr遍历器而言,它判断是否还有下一个元素的标准很简单:下一步将访问的元素的索引不等于集合的大小,就会返回true。当到倒数第二个元素时,下一个即将访问的元素索引为size()-1.此时删除集合的任意一个元素,将导致集合size()变成size() -1 ,这导致hasNext()方法返回false。也就是说,遍历提前技术,Iterator不会访问下一个元素,随意不会报错。

所以对于迭代过程中的删除,只要你掌握规律是可以运作的。

 

Set元素的话判断的是比List少1,也就是说只有最后一个元素的时候删除不会出现问题。