java集合入门

来源:互联网 发布:小猪收银系统源码 编辑:程序博客网 时间:2024/06/05 15:58

基础

List

        可重复,取出的顺序与添加的顺序一样。

        ArrayList:采用变长数组实现,内部使用数组存储添加的元素,如果元素的个数超过数组的长度,则自动扩容。

        LinkedList:内部采用双向链表实现。可以使用它模拟队列、栈等数据结构。

Set

        不可重复,无序(取出的顺序与添加的顺序不一样)

        HashSet:内部使用HashMap实现。其内部元素的排列顺序以hashCode为准。

        TreeSet:自己指定元素的排列顺序。

Map

        存储的是键值对。

        HashMap:内部以散列链表实现。

HashMap

        内部的数据结构为数组+链表,各个hash值相同的key的item以链表形式存储,而所有的链表的头节点又都存储在一个数组中。以put为例,如下:

    @Override public V put(K key, V value) {        if (key == null) {            return putValueForNullKey(value);        }        int hash = Collections.secondaryHash(key);//获取key的hashCode,当然内部进行了别的处理。处理主要目的是使key的hash值能分散的均匀些        HashMapEntry<K, V>[] tab = table;        int index = hash & (tab.length - 1);        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {//取出该链表的表头tab[index],然后通过next遍历该链表            if (e.hash == hash && key.equals(e.key)) {//如果有相同的key存在,就直接替换掉原来的value,并将旧value返回。                preModify(e);                V oldValue = e.value;                e.value = value;                return oldValue;//返回旧值            }        }        // 没有key的节点,那就新建一个,并添加到节点中。        modCount++;        if (size++ > threshold) {            tab = doubleCapacity();            index = hash & (tab.length - 1);        }        addNewEntry(key, value, hash, index);        return null;    }

        最后一步的addNewEntry是将新的key,value,hash构建成HashMapEntry对象,然后插入到该链表的表头。

        对于HashMapEntry,它表示链表的节点。对于链表的节点来说,它至少有一个指向于下一个节点的指针(这也是为什么需要将key,value重新封装成HashMapEntry对象的原因),并有存储当前节点值的数值域。其成员变量如下:

        final K key;        V value;        final int hash;        HashMapEntry<K, V> next;
        这是一个很常规的链表节点的数据结构。

        由于使用了数组存储链表的头节点,所以当item过多时就需要进行扩容,否则会导致链表越来越长,造成在查找时对性能的消耗(链表的查找需要从头节点一个个的遍历比对,不如数组方便)。扩容的方法就是doubleCapacity。如下:

private HashMapEntry<K, V>[] doubleCapacity() {        HashMapEntry<K, V>[] oldTable = table;        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) {//达到最大容量,无法再进行扩容。其值为1<<30。            return oldTable;        }        int newCapacity = oldCapacity * 2;        HashMapEntry<K, V>[] newTable = makeTable(newCapacity);//新建一个容量为原来2倍的HashMapEntry数组        if (size == 0) {//原来Map中没有元素,直接返回            return newTable;        }        for (int j = 0; j < oldCapacity; j++) {            /*             * Rehash the bucket using the minimum number of field writes.             * This is the most subtle and delicate code in the class.             */            HashMapEntry<K, V> e = oldTable[j];            if (e == null) {                continue;            }            int highBit = e.hash & oldCapacity;            HashMapEntry<K, V> broken = null;            newTable[j | highBit] = e;//将旧数组中的表头移到新数组中            for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {//依次移动该链表中的每一个节点。                int nextHighBit = n.hash & oldCapacity;                if (nextHighBit != highBit) {                    if (broken == null)                        newTable[j | nextHighBit] = n;                    else                        broken.next = n;                    broken = e;                    highBit = nextHighBit;                }            }            if (broken != null)                broken.next = null;        }        return newTable;    }

        由于数组进行了扩容,所以旧数组中的节点的位置需要重新计算。相当于重构了整个map。

缺点

        1,无论该map中是否存有数据,都必须有一个数组,对内存有一定的浪费。

        2,需新建一个对象,用于表示链表中的节点。

        3,不支持基本数据类型,使用int作为key时,必须转换成Integer,也是一种浪费。

        4,扩容时必须重构map,还是浪费。

优点

        使用数组+链表结构,一方面利用了数组的查找方便的特性,另一方面又避免了数组对内存的高要求——分配的空间必须是连续的。

        对于大批量数据存储时,使用HashMap是一个不错的选择,最好能预估数据的大小,并且指定一个初始容量。这种方法可以避免了HashMap的多次扩容,减少性能的消耗。

ArrayDeque

        它是一个变长数组,内部含有指向数据首、尾元素的指针,类似于队列——先进先出。

        offer方法最终会执行到如下方法:

    public void addLast(E e) {        if (e == null)            throw new NullPointerException("e == null");        elements[tail] = e;        if ( (tail = (tail + 1) & (elements.length - 1)) == head)            doubleCapacity();    }
        从这里很容易看出,offer是将新元素添加到数组的末尾,并根据需要判断是否需要扩容。
        而且由判断条件可以知道,ArrayDeque实现的是循环队列。之所以使用tail+1与数组长度-1取&,是为了让tail能循环着取数组中的下标,从而达到循环队列的目的。

        再看一下pop方法:

    public E removeFirst() {        E x = pollFirst();        if (x == null)            throw new NoSuchElementException();        return x;    }    public E pollFirst() {        int h = head;        @SuppressWarnings("unchecked") E result = (E) elements[h];        // Element is null if deque empty        if (result == null)            return null;        elements[h] = null;     // Must null out slot        head = (h + 1) & (elements.length - 1);        return result;    }

        其最终会走到这两个方法,可以看出它只是从head中取一个元素,然后判断是否为null,如果是null就扔个异常。这是很简单粗暴的,它没有在取元素时判断当前数组中有没有元素,但这种方式我喜欢——简单直接,自己用的时候自己考虑这种事。

        而且也节约一个数组空间——使用数组实现循环队列时,一般需要留一下位置不用,这样才能判断出当前数组是没有元素还是已经满了。

        当然,ArrayDeque也不需要判断数组是不是满了,因为它会进行扩容。

        无论是从offer还是pop中都可以看出,ArrayDeque不接收null元素

0 0
原创粉丝点击