• 容器之路 List解析


    容器之路 List解析

    1.类图

    这里我们主要介绍两个类,分别是ArrayListLinkedList,这两个类都是List的实现类,下面是简要的类图。

    2.ArrayList

    ArrayList是我们非常常用的list实现,这个类的底层实际上是使用可以变化长度数组来保存数据,一些操作都是非常简单的,我们主要需要注意的就是可以变化长度这一点的实现方式。

    transient Object[] elementData;

    transient关键字表示elementData不会被序列化,但是这不表示ArrayList无法序列化,ArrayList使用了自定义的方法来进行序列化,这种做法可能是为了防止大量空的数据被序列化

    2.1 初始化

    当我们初始化一个ArrayList的时候,实际上就是初始化了一个数组。

    public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
    

    2.2 add方法

    在数组中增加元素是十分简单的,这个方法主要需要注意的就是边界检查,判断是否需要动态的增加数组的长度。

    类中有变量size用来保存数组中元素的个数,当我们增加一个元素的时候,size的大小需要增加。我们需要判定,增加后的大小是否超出。

    这里有两种情况,第一种,数组初始化后第一次插入数据,那么比较默认大小10和传入参数的大小,选择较大的一个。第二种,不是第一插入数据,选择传入参数。

    有了确切的应该可以容纳的元素个数后,我们判断目前数组的长度是否大于这个值。如果不大于,那么我们就需要调用方法grow来增加数组的长度。

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    private static int calculateCapacity(Object[] elementData, intminCapacity) {
        //第一次插入数据
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
        grow(minCapacity);
        
    }
    

    如果数组长度需要增加,那么,我们会让数组的长度变为原来的1.5倍,然后将原来的数据复制到新数组中。

    private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    

    2.3 remove方法

    移除方法有两个,一个是移除某个index上的值,一个是移除object

    移除对象的操作我们一般先将移除对象之后的所有元素向前移动,然后将最后一个元素置为null,并将size减1。

    另外需要说明的一点是,我们移除一个对象,只会移除list中第一个对象。

    2.4 序列化问题

    我们在上面提到ArrayList没有使用默认的序列化方法,而是自己实现了序列化方法,下面我们给出方法。

    如果一个类需要序列化,那么我们需要让这个类实现接口Serializable
    如果我们不想使用默认的序列化方法,我们可以自己实现下面两个方法。然后,将字段使用transient修饰。

        private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException{
            // Write out element count, and any hidden stuff
            int expectedModCount = modCount;
            s.defaultWriteObject();
    
            // Write out size as capacity for behavioural compatibility with clone()
            s.writeInt(size);
    
            // Write out all elements in the proper order.
            for (int i=0; i<size; i++) {
                s.writeObject(elementData[i]);
            }
    
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
        }
    
        /**
         * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
         * deserialize it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            elementData = EMPTY_ELEMENTDATA;
    
            // Read in size, and any hidden stuff
            s.defaultReadObject();
    
            // Read in capacity
            s.readInt(); // ignored
    
            if (size > 0) {
                // be like clone(), allocate array based upon size not capacity
                int capacity = calculateCapacity(elementData, size);
                SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
                ensureCapacityInternal(size);
    
                Object[] a = elementData;
                // Read in all elements in the proper order.
                for (int i=0; i<size; i++) {
                    a[i] = s.readObject();
                }
            }
        }
    

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

    LinkedList即实现了List接口,也实现了Deque接口,实际上Deque接口就是double queue,双端队列。

    使用链表的形式,我们可以很方便的移除或者新增一个元素,但是如果我们需要访问某个元素,只能依次遍历。同时,我们也可以使用LinkedList来实现栈或者队列的功能。

    LinkedList中有两个指针,分别指向第一个元素和最后一个元素。

    transient Node<E> first;
    
    transient Node<E> last;
    

    3.1.增加与删除元素

    类中提供了多种方式可以在链表的头部和尾部增加或删除结点,同时也可以在某个index处增加或删除结点。

    增加或删除元素时,我们需要注意指向前驱节点以及后继结点的指针的处理。

    我们仍需注意的一点时头结点和尾结点的处理。

    下图给出的是通用的链表中增加或删除元素的图示,相信大家对这些内容都是十分熟悉的,再次不再赘述。

    LinkedList的方便之处在于我们灵活的在首尾增删节点。但是如果我们需要的单纯是一个队列或者栈,我们应该选用ArrayDeque,它效率更高。LinkedList相较ArrayList增删结点等操作需要更为复杂的处理。

    4.关于遍历

    ArrayListLinkedList中有两种迭代器,分别是IteratorListIterator

    Iterator在遍历过程中不能动态增加list中的元素,但是ListIterator可以增加或修改元素。另外ListIterator不仅可以向后遍历,同时也可以向前遍历,能够灵活的移动,更加方便。

    Iterator是一个更为通用的迭代器类,所有集合对象都可以使用。

    另外需要注意,在使用迭代器遍历的过程中,不能够直接调用集合的增加修改删除方法,这样会抛出异常,当需要修改时,可以考虑使用迭代器自带的方法进行操作。

    public class IteratorTest {
        public static void main(String[] args) {
            String[] strings = {"aaa", "bbbb", "ccc"};
            ArrayList<String> list = new ArrayList<>(Arrays.asList(strings));
    
            Iterator iterator = list.iterator();
            while (iterator.hasNext()) {
                String tmp = (String) iterator.next();
                System.out.println(tmp);
                //满足条件remove当前结点
                if ("aaa".equals(tmp)) {
                    iterator.remove();
                }
            }
    
            Stream.of(list).forEach(System.out::println);
    
            ListIterator listIterator = list.listIterator();
            while (listIterator.hasNext()) {
                String tmp = (String) listIterator.next();
                System.out.println(tmp);
    
                if ("bbbb".equals(tmp)) {
                    //满足条件,修改结点的值
                    (listIterator).set("change");
                    //并且增加结点,在当前结点后面增加
                    (listIterator).add("dddd");
                }
            }
    
            Stream.of(list).forEach(System.out::println);
        }
    }
    
    //输出结果
    aaa
    bbbb
    ccc
    [bbbb, ccc]
    bbbb
    ccc
    [change, dddd, ccc]
    
  • 相关阅读:
    <contextparam>与<initparam>的区别与作用
    Eclipse中的条件断点
    通过ContentResolver调用系统URI实现的通讯录示例
    Spring中ApplicationContext加载机制和配置初始化
    Java常见笔试、面试题目深度剖析
    加载/WEBINF/applicationContext.xml的解决方法
    Spring ClassPathXmlApplicationContext和FileSystemXmlApplicationContext
    改变MyEclipse默认编码方式
    怎么把100多个EXCEL文件合并成一个
    1.Python简介
  • 原文地址:https://www.cnblogs.com/qmlingxin/p/9779644.html
Copyright © 2020-2023  润新知