• Java提高——常见Java集合实现细节(3)


    Map和List

    map的values方法

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

    HashMap的values方法的源码:

    public Collection<V> values() {
       //获取values实例变量
       Collection<V> vs = values;
       //如果vs == null,将返回new values()对象
       return (vs != null ? vs : (values = new Values()));
    }

    private final class Values extends AbstractCollection<V> {
        public Iterator<V> iterator() {
            return newValueIterator();
        }
        public int size() {
            return size;
        }
        public boolean contains(Object o) {
            return containsValue(o);
        }
        public void clear() {
            HashMap.this.clear();
        }
    }
    Iterator<V> newValueIterator()   {
        return new ValueIterator();
    }
    private final class ValueIterator extends HashIterator<V> {
        public V next() {
            return nextEntry().value;
        }
    }
    综上HashMap的values()方法表面上返回了一个Values对象,但这个对象不能添加元素。它的主要功能是用于遍历HashMap里的所有value。而遍历主要依赖于HashIterator的nextEntry()方法实现。每个Entry都持有一个引用变量指向下一个Entry.

    TreeMap的 values方法的源码:

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

    class Values extends AbstractCollection<V> {
        public Iterator<V> iterator() {
        //以TreeMap中最小节点创建一个ValueIterator
         return new ValueIterator(getFirstEntry());
        }
    
        public int size() {
         //调用外部类的size()实例方法的返回值作为返回值
         return TreeMap.this.size();
        }
    
        public boolean contains(Object o) {
         //调用外部类的containsValue(o)实例方法的返回值作为返回值
         return TreeMap.this.containsValue(o);
        }
    
        public boolean remove(Object o) {
        //从TreeMap中最小的节点开始搜索,不断搜索下一个节点
        for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e)) {
                if (valEquals(e.getValue(), o)) {//如果找到指定节点
                    deleteEntry(e);//执行删除
                    return true;
                }
            }
            return false;
        }
    
        public void clear() {
         //调用外部类的clear()实例方法来清空该集合
         TreeMap.this.clear();
        }
    }
    

    getFirstEntry:获取TreeMap底层“红黑树”最左边的“叶子节点”,也就是“红黑树”中最小的节点,即TreeMap中第一个节点。

    final Entry<K,V> getFirstEntry() {
        Entry<K,V> p = root;
        if (p != null)
        //不断搜索左子节点,直到p成为最左子树的叶子节点
        while (p.left != null)
                p = p.left;
        return p;
    }

    successor:获取TreeMap中指定Entry(t)的下一个节点,也就是红黑树中大于t节点的最小节点。

    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
       //如果其右子树存在,搜索右子树中最小的节点(也就是右子树最左的叶子节点)
       else if (t.right != null) {
           //先获取其右子节点
         Entry<K,V> p = t.right;
         //不断搜索左子节点,直到找到最左的叶子节点
         while (p.left != null)
                p = p.left;
            return p;
       //如果右子树不存在
        } else {
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
        //只要父节点存在,且ch是父节点的右节点
        //表明ch大于其父节点,循环一直继续    
        //直到父节点为null,或者ch变成父节点的子节点——此时父节点大于被搜索的节点
         while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }
    归纳:不管是HashMap还是TreeMap,他们的values()方法都可以返回其所有value组成的Collection集合——通常理解,这个Collection集合应该是一个List集合,因为map的多个value允许重复。但这两个map对象values()方法返回的是一个不存储元素的Collection集合。当程序遍历Collection集合时,实际上就是遍历Map对象的value

    Map和List的关系

    从底层上看,Set和Map很相似;从用法上看,Map和List也有很大的相似。

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

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

    可以说List相当于所有key都是int类型的Map。Map和List只是用法上有些许相似之处,在底层实现上并没有太大的相似之处。、

    ArrayList和LinkedList

    List的实现主要有三个类:ArrayList,Vector,LinkedList


    其中Vector有一个子类Stack,这个Stack子类仅在Vector父类的基础上增加了5个方法,这5个方法将Vector扩展成一个Stack,本质上,Stack依然是一个Vector。

    Stack源码:

    public class Stack<E> extends Vector<E> {
        /**
         * 无参构造器
         */
        public Stack() {
        }
    
        /**
         * 实现向栈定添加元素的方法
         */
        public E push(E item) {
            addElement(item);//调用父类的方法来添加元素
    
            return item;
        }
    
        /**
         * 实现出栈的方法(位于栈顶的方法将被弹出栈)
         */
        public synchronized E pop() {
            E       obj;
            int     len = size();
    
            obj = peek();
            removeElementAt(len - 1);
    
            return obj;
        }
    
        /**
         * 取出最后一个元素,但不会弹出栈
         */
        public synchronized E peek() {
            int     len = size();
        //如果不包含任何元素,直接抛出异常
            if (len == 0)
                throw new EmptyStackException();
            return elementAt(len - 1);
        }
       //集合不包含任何元素就是空栈
        public boolean empty() {
            return size() == 0;
        }
    
        public synchronized int search(Object o) {
         //获取o在集合中的位置
         int i = lastIndexOf(o);
    
            if (i >= 0) {
           //用集合长度减去o在集合中的位置,就得到该元素到栈顶的距离。
           return size() - i;
            }
            return -1;
        }
    
        private static final long serialVersionUID = 1224463164541339165L;
    }
    

    从源码可以看出Stack新增的5个方法中有3个使用了synchronized修饰——那些需要操作集合元素的方法都被添加了synchronized修饰,也就是说Stack是一个线程安全的类,这也是为了让Stack和Vector保持一致——Vector也是一个线程线圈类。

    如今不在推荐使用Stack类,而是使用Deque实现类。在无需保证线程安全的情况下,完全可以使用ArrayDeque代替Stack。

    Deque接口代表双端队列这种数据结构,即具有队列先进先出的性质,也具有栈的性质。即是队列也是栈。

    ArrayList和ArrayDeque底层都是基于Java数组实现的,只是提供的方法不同而已。

    Vector和ArrayList

    Vector和ArrayList都实现了List接口,底层都是基于Java数组存储集合元素。

    ArrayList源码:

    //采用elementData数组保存集合元素
    private transient Object[] elementData;

    Vector源码:

    //采用elementData元素保存集合
    protected Object[] elementData;

    ArrayList使用了transient修饰,保证了系统序列化ArrayList对象不会直接序列化elementData数组,而是通过writeObject和readObject实现定制序列化。从序列化的角度看,ArrayList的实现比Vector安全。

    除此之外,Vector其实就是ArrayList的线程安全版,ArrayList和Vector大部分实现方法都相同,只是Vector方法增加了synchronized修饰。

    下面看看两者的一些源码:

    ArrayList的add方法:

    public void add(int index, E element) {
        rangeCheckForAdd(index);
      //保证ArrayList底层数组可以保存所有集合元素
        ensureCapacityInternal(size + 1);  // Increments modCount!!
      //将elementData数组中index位置之后的所有元素向后移动一位   
      //也就是将elementData数组中的index位置空出来   
       System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
      //将新元素放进elementData数组的index位置
      elementData[index] = element;
        size++;
    }
    
    //如果添加位置大于集合长度或小于0,抛出异常
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    

    Vector的add方法:

    public void add(int index, E element) {
        insertElementAt(element, index);
    }
    public synchronized void insertElementAt(E obj, int index) {
        modCount++;//增加集合的修改次数
       //如果添加位置大于集合长度抛出异常
       if (index > elementCount) {
            throw new ArrayIndexOutOfBoundsException(index
                                                     + " > " + elementCount);
        }
       //保证ArrayList底层数组可以保存所有集合
       ensureCapacityHelper(elementCount + 1);
    
     //将elementData数组中index位置之后的所有元素向后移动一位   
     //也就是将elementData数组中的index位置空出来  
    
     System.arraycopy(elementData, index, elementData, index + 1, elementCount - index); elementData[index] = obj;//将新元素放入elementData数组的index位置 elementCount++;}

    发现只是Vector的方法多了synchronized方法修饰。

    ArrayList序列化实现比Vector序列化实现更加安全,因此Vector基本被ArrayList代替。Vector的唯一好处就是他是线程安全的。但是ArrayList也可以被包装成线程安全的。

    ArrayList和LinkedList实现差异

    List代表一种线性表的数据结构,

    ArrayList则是一种顺序存储的线性表,底层采用数组保存每个集合元素,

    LinkedList则是一种链式存储的线性表,本质是一个双向链表,但是它不仅实现了List接口还实现了Deque接口,也就是说LinkedList既可以当成双向链表来使用,也可以当成队列来使用,还可以当成栈来使用。(Deque代表双端队列,同时具有队列和栈的特性)。

    从上可知:ArrayList底层采用数组保存集合元素,则ArrayList插入时需要完成以下两件事:

    1)底层数组长度大于集合元素个数

    2)插入位置之后的元素“整体搬家”向后一格

    当删除ArrayList集合中指定位置元素时,程序也要进行“整体搬家”,而且还要将被删除的数组元素赋为null。

    public E remove(int index) {
       //如果index大于或等于size,抛出异常
       rangeCheck(index);
    
        modCount++;
       //保证index索引处的元素
       E oldValue = elementData(index);
       //计算需要“整体搬家”的元素个数
        int numMoved = size - index - 1;
        //当numMoved大于0时,开始搬家
       if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // 释放被删除的元素,以免GC回收该元素
        return oldValue;
    }

    对于ArrayList而言,添加、删除都需要”整体搬家“,因此性能十分差

    public E get(int index) {
        rangeCheck(index);
        checkForComodification();
        return ArrayList.this.elementData(offset + index);
    }
    E elementData(int index) {
        return (E) elementData[index];
    }
    当时获取元素的性能和数组几乎相同,非常快。

    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;
        }
    }

    在指定位置插入新节点:

    public void add(int index, E element) {
        checkPositionIndex(index);
       //如果index == size直接在header之前插入新节点
        if (index == size)
            linkLast(element);
        else//否则在index索引处的节点之前插入新节点
            linkBefore(element, node(index));
    }
    
    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++;
    }
    在指定节点(succ)前添加一个新的节点
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
       //创建一个新节点,新节点的下一个节点指向succ,上一个节点指向succ的上一个节点
      final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
       //让succ的上一个节点向后指向新节点
       succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }
    获取指定索引处的节点
    Node<E> node(int index) {
        // assert isElementIndex(index);
        //如果index<size/2
        if (index < (size >> 1)) {
            Node<E> x = first;//从链表的头部开始搜索
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {//如果index>size/2
            Node<E> x = last;//从链表的尾部开始搜索
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

    LinkedList的get方法只是对上面的node方法进行封装:

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    

    无论如何LinkedList为了获取指定索引处的元素都是比较麻烦的,系统开销也会比较大。

    但是简单的插入操作就比较简单,只要修改了几个节点里的prev,next引用的值就可以了。

    删除节点也必须先通过node方法找到索引处的节点,然后修改前一个节点的next引用和后一个节点的prev引用:

    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
    
    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;//先保存x节点的元素
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
       //将被删除的元素的两个引用、元素都赋值为null,以便垃圾回收
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
    
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
    
        x.item = null;
        size--;
        modCount++;
        return element;
    }
    ArrayList和LinkedList性能分析

    ArrayList性能总体上优于LinkedList。

    当程序需要通过get(int index)方法获取List集合指定索引处的元素时,ArrayList性能大大优于LinkedList,因为ArrayList底层是数组来保存集合元素,所以调用get方法获取指定索引处的元素时,底层实际上调用elementData[index]来返回该元素,因此性能非常好。

    当程序调用add(int index,Object obj)向List集合中添加元素时,ArrayList必须对底层数组进行“整体搬家”。如果数组不够长,还要重新创建一个长度为原来1.5倍的数组,再由垃圾回收机制回收原来的数组,因此系统开销比较大;对于LinkedList而言,它的主要系统开销集中在node(int index )方法上,必须一个一个的搜索过去,直到找到index元素,再在此元素之前插入新元素。即便如此,执行该方法时LinkedList性能依然优于ArrayList

    Iterator迭代器

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

    List和Set在实现Iterator的差异——>导致删除集合元素的不同表现

  • 相关阅读:
    react native
    快速幂模板
    Java异常归纳
    Java环境变量配置
    过滤器
    cookie和session页面随机数和防止重复提交
    javabean&el&jstl
    servlet&jsp
    Tomcat和Servlet入门
    网络编程
  • 原文地址:https://www.cnblogs.com/huangzhe1515023110/p/9276103.html
Copyright © 2020-2023  润新知