第二章 集合 - Java源码笔记

1 ArrayList源码解析和设计思路

1.1 整体结构

ArrayList就是一个数组结构。

  • 允许 put null 值,会自动扩容;
  • size、isEmpty、get、set、add 等方法时间复杂度都是 O (1);
  • 是非线程安全的,多线程情况下,推荐使用线程安全类:Collections#synchronizedList;
  • 增强 for 循环,或者使用迭代器迭代过程中,如果数组大小被改变,会快速失败,抛出异常。

1.2 初始化

三个方法:无参数直接初始化、指定大小初始化、指定初始数据初始化。

// 无参数直接初始化
 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
 // 指定大小初始化
 public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
 public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
  • 无参数直接初始化,默认大小是空数组,而不是10. 10是第一扩容的值。

1.3 新增和扩容实现

  • 扩容后是原来的1.5倍;
  • 扩容最大为 Integer.MAX_VALUE;
  • 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。
  • 扩容本质,新建一个新数组,然后把老数组复制过去。比较耗性能,所以编码时,尽量指定数据的大小,避免频繁扩容。

1.4 删除

删除某个index下标的元素,后面的元素会往前移动一位。

1.5 迭代器

int cursor;// 迭代过程中,下一个元素的位置,默认从 0 开始。
int lastRet = -1; // 新增场景:表示上一次迭代过程中,索引的位置;删除场景:为 -1。
int expectedModCount = modCount;// expectedModCount 表示迭代过程中,期望的版本号;modCount 表示数组实际的版本号。
public boolean hasNext() {
            return cursor != size;
        }
 public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

2 LinkedList 源码解析

2.1 整体结构

在这里插入图片描述
Node的代码

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

2.2 追加,删除

可以从头尾追加,有可以从头尾删除

2.3 节点查询

如果index在前半部分,从头开始查,否则从后开始查,这样提高效率。size >> 1 表示右移1位,即相当于处于2.

Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

2.4 迭代器

实现ListIterator接口,支持双向迭代,即可以从头也可以从尾开始迭代。

3 List的面试题

3.1 说说对ArrayList和LinkedList的理解

可以先回答总体架构,然后从某个细节突破。比如ArrayList底层是数组,其API都是对底层数组的封装等等。

3.2 扩容类问题

1. ArrayList 无参数构造器构造,现在 add 一个值进去,此时数组的大小是多少,下一次扩容前最大可用大小是多少?
此时数组大小是1,下一次扩容前最大可用大小是10. 第一次扩容时默认值是10.
2. 如果我连续往 list 里面新增值,增加到第 11 个的时候,数组的大小是多少?
oldCapacity + (oldCapacity>> 1),下一次扩容是1.5倍。即从10变成15.
3. 数组初始化,被加入一个值后,如果我使用 addAll 方法,一下子加入 15 个值,那么最终数组的大小是多少?
初始化后,加入一个值,此时默认扩容是10,再一下子加入15个值,先扩容1.5倍,即15,还是不能满足,此时有个策略:

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

直接扩容到16个。
4. 现在我有一个很大的数组需要拷贝,原数组大小是 5k,请问如何快速拷贝?
5k是比较大的值,数组初始化时指定容量为5k。如果初始化不指定大小,那么就会频繁的扩容,有大量的拷贝,影响性能。
5. 为什么说扩容会消耗性能?
底层使用System.arraycopy,把原数组拷贝到新数组上,所以消耗性能。
6. 源码扩容过程有什么值得借鉴的地方?

  • 自动扩容,使用者不用关心底层数据结构的变化。扩容是按照1.5倍增长,前期增长比较慢,后期增长比较大,大部分工作使用的数据大小不大,有利用节省性能,后期增长大,有利于快速扩容。
  • 扩容过程,注意数组溢出,不小于0,不大于Integer最大值。

3.3 删除类问题

1. 有一个 ArrayList,数据是 2、3、3、3、4,中间有三个 3,现在我通过 for (int i=0;i<list.size ();i++) 的方式,想把值是 3 的元素删除,请问可以删除干净么?最终删除的结果是什么,为什么?删除代码如下:

List<String> list = new ArrayList<String>() {{
            add("2");
            add("3");
            add("3");
            add("3");
            add("4");
        }};
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i).equals("3")) {
                list.remove(i);
            }
        }

不能。在这里插入图片描述
2. 还是上面的 ArrayList 数组,我们通过增强 for 循环进行删除,可以么?
不可以,会报错。因为增强 for 循环过程其实调用的就是迭代器的 next () 方法,当你调用 list#remove () 方法进行删除时,modCount 的值会 +1,而这时候迭代器中的 expectedModCount 的值却没有变,导致在迭代器下次执行 next () 方法时,expectedModCount != modCount 就会报 ConcurrentModificationException 的错误。
参看:为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作
3. 还是上面的数组,如果删除时使用 Iterator.remove () 方法可以删除么,为什么?
可以的,因为 Iterator.remove () 方法在执行的过程中,会把最新的 modCount 赋值给 expectedModCount,这样在下次循环过程中,modCount 和 expectedModCount 两者就会相等。
4. 以上三个问题对于 LinkedList 也是同样的结果么?
是的,虽然 LinkedList 底层结构是双向链表,但对于上述三个问题,结果和 ArrayList 是一致的。

3.4 对比类问题

1. ArrayList 和 LinkedList 有何不同?
两者的底层实现逻辑不同,ArrayList是数组,而LinkedList是双向链表,围绕底层而实现的api也不同,bla,bla等等。
2. ArrayList 和 LinkedList 应用场景有何不同
ArrayList适合快速查找,不用频繁新增和删除的场景,而LinkedList适合频繁新增和删除,很少查询的场景。
3. ArrayList 和 LinkedList 两者有没有最大容量
前者有最大integer个,后者理论上无线大,但实际大小用的是int size, 所以也只有integer个。
4. ArrayList 和 LinkedList 是如何对 null 值进行处理的
两者都运行null值的新增和删除,不过ArrayList删除null值是从头开始删。
5. ArrayList 和 LinedList 是线程安全的么,为什么?
不是。在多线程环境中,两者新增和删除等操作都不是同步的。
6. 如何解决线程安全问题?
Collections中有线程安全的list,Collections.synchronizedList(),或者用CopyOnWriteArrayList 。

3.5 其它类型题目

1. 描述下双向链表么?
略。
2. 描述下双向链表的新增和删除
略。

4 HashMap 源码解析

发布了97 篇原创文章 · 获赞 3 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_39530821/article/details/105347974