ArrayList与Linked的源码解析以及可能出现的面试问题

63 阅读6分钟

一. ArrayList相关

构造方法源码

    List list = new ArrayList<>();
​
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //存储具体数据的数组
    transient Object[] elementData; 
    
    private static final Object[] EMPTY_ELEMENTDATA = {};
    //无参构造器
    public ArrayList() { 
        //无参构造器默认是一个空的数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    //有参构造器
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) { 
            // 如果是构造参数设置为0,与使用无参构造器是一样的初始化一个空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else { 
            // 小于0抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
        }
    }
​

1.1 面试有可能会问的问题

  • ArrayList底层是用的什么结构存储数据,无参构造器初始化大小是多少?有参构造器呢?

    • 1.底层是用的一个Object类型的数组。
    • 2.当使用无参构造器底层的数组是一个空的,只用调用add()方法才会分配大小。
    • 3.有参构造器是根据设置的大小给Object类型的数组分配一个固定的内存空间。

2.add()方法源码

    list.add("测试添加数据"); 
    //实际存储数据的Object类型数组
    transient Object[] elementData; 
    //modCount是AbstractList类的属性
    protected transient int modCount = 0;
    private int size;
    //调用的添加接口方法
    boolean add(E e); 
    //接口方法的实现
    public boolean add(E e) { 
        //每次调用加一
        modCount++;
        //e表示的需要添加的数据,elementData存储数据的数组,size实际存储的数据个数
        add(e, elementData, size); 
        //只要是不抛出异常固定返回true
        return true;
    }
    //实际的添加数据方法  e表示的需要添加的数据,elementData存储数据的数组,s实际存储的数据个数
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length) 
            // 如果是实际存储的数个数 == 数组最大长度 ,那么需要扩容
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
    
    //扩容方法1
    private Object[] grow() {
        return grow(size + 1);
    }
    //扩容方法2
    private Object[] grow(int minCapacity) {
        // newCapacity(int i); 
        return elementData = Arrays.copyOf(elementData,newCapacity(minCapacity));
    }
​
    //数组扩容具体方法
    private int newCapacity(int minCapacity) {
        // overflow-conscious code 
        //获取就数组的最大长度
        int oldCapacity = elementData.length; 
        //新的数组长度 = 旧数组长度 + (旧数组长度 / 2)
        //结论:新数组长度是就数组的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //新数组长度 - (实际存储的元素个数+1);实际存储的元素个数是指定在上一次添加成功后记录的数量,+1是本次需要的长度 
        if (newCapacity - minCapacity <= 0) { 
            //如果结果小于等于0 
            // private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)  
                //如果存储元素的数组是空的
                // private static final int DEFAULT_CAPACITY = 10; 
                // 返回 10或者minCapacity中大的一个
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow 
                // 这个为什么会小于0?如果int类型最大值就会出现负数
                throw new OutOfMemoryError(); 
            // 如果 newCapacity 的结果是负数,且minCapacity大于0。
            // 为什么会有负数,如果int类型最大值就会出现负数
            return minCapacity;
        }
        //    int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
        return (newCapacity - MAX_ARRAY_SIZE <= 0) ? newCapacity: hugeCapacity(minCapacity);
    }
​
    // 返回最大容量的逻辑
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE)
            ? Integer.MAX_VALUE
            : MAX_ARRAY_SIZE;
    }

2.1 面试可能会遇到的问题

  1. ArrayList的扩容机制以及什么时候触发

    • 扩容机制:扩容的大小是原容量的1.5倍,公式:原数组长度+(原数组长度 / 2)= 新数组长度

    • 触发时机:

      • 使用无参构造器初始化化ArrayList,调用add()方法;
      • 当Object存储的数据量等于数组的容量;
  2. ArrayList的最大容量是多少?

    • 理论的最大容量是int类型最大值减8
  3. ArrayList是线程安全的吗?

    • 非线程安全,整个源码完全没有出现锁相关代码。
  4. 为什么查询快,新增/删除慢?

    • 底层是数组结构,且分配的是一块连续的内存空间。
    • 因为存在索引,所以查询效率高。
    • 新增删除慢是相对的,如果数据量大,那么向中间新增或者删除数据会出现数据移动的问题,所以效率低。
  5. 默认的数组大小是多少

    • 如果初始化的ArrayList没有指定大小,那么在调用add()方法的时候设置默认大小是10。(懒加载)
  6. 如果需要在多线程的情况使用ArrayList,怎么办?

    • 首先有vector,它的基本实现和ArryList基本一致,但是许多方法用了sync修饰。所以存在效率问题(基本不用)。

    • 其次,List list = Collections.synchronizedList(list);将list转换为线程安全的在使用

    • 其他Collections常用方法:

      • Collections.sort(List list);对集合排序,从小到大。
      • Collections.reverse(List list);集合内容反转,1234变为4321
      • Collections.max(list);获取集合的最大值

二. LinkedList相关

1.构造方法源码

    List<Integer> objects = new LinkedList<>(); 
    // 无参构造器
    public LinkedList() {}
    // 有参构造器
    public LinkedList(Collection<? extends E> c) { 
        //调用无参构造器
        this();
        //调用addAll()方法
        addAll(c);
    }

1.1 面试可能会问的问题

基本上没啥问的,如果聊到这就说一下有参构造器传入参数是集合类型,底层就是调用了addAll()方法。

2.add()方法源码

    objects.add(1);
    
    transient Node<E> last;
    transient Node<E> first;
    transient int size = 0; 
    //modCount属性在AbstractList类中
    protected transient int modCount = 0;
    //e是新添加的数据对象
    boolean add(E e); 
    //添加方法
    public boolean add(E e) {
        //具体方法调用
        linkLast(e);
        return true;
    }
    //具体方法调用
    void linkLast(E e) { 
        // 末尾节点 
        final Node<E> l = last; 
        // 创建一个新的节点容纳新数据
        final Node<E> newNode = new Node<>(l, e, null); 
        // 新节点赋值给全局变量的末尾节点
        // 注意:前面以已经把上次的末尾节点保存到局部变量中l
        last = newNode;
        if (l == null)  
            //如果上一次的末尾节点是null ,那么当前创建的节点不光是末尾节点,同时也是头节点
            first = newNode;
        else
            // 如果上一次末尾节点不null,那就把上一次默认节点对象中的next属性指向新创建的节点对象
            l.next = newNode;
        size++;
        modCount++;
    }
​
    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.面试中可能出现的问题:

  1. LinkedList中add()方法的执行流程?

    • 首先关键对象是Node,关键属性是Node last和Node first.
    • 如果是首次添加first和last都是null,所以第一次添加会把传入对象封装为一个Node对象,然后first和last都指向它。
    • 如果是第二次以上添加,先找到原来链表的末尾节点,也就是last赋值给局部变量,然后把当前对象也封装唯一个Node对象,然后把新Node对象赋值全局last对象(这就形成了头节点不变,但尾节点是最新添加的数据)。接着把原last的局部对象中的next属性指向新创建对象(这样就把原末尾节点和当前新节点关联上)。
  2. LinkedList和ArrayList的区别?

    • 底层组成结构不同,LinkedList底层是Node对象组成双向链表,ArrayList底层是Object对象数组。
    • 理论的最大容量不同:ArrayList的最大容量是int类型的最大值减8,LinkedList理论上是没有限制大小(但是具体使用收到内存和相同资源限制)。
    • ArrayList底层是数组,有索引,查询和遍历效率高。删除和新增效率低(尾部删除和新增不存在),因为涉及到数据移动的问题。
    • LinkedList新增删除效率高,因为新增和删除只要修改Node的结构指向,所以效率高。

3.addAll()方法源码

    objects.addAll(list); 
    transient int size = 0;
    // 参数类型必须是Collection的实现
    boolean addAll(Collection<? extends E> c);
​
    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
    
    // index表示指定插入位置 c表示需要添加的集合
    public boolean addAll(int index, Collection<? extends E> c) { 
        // 校验index,有异常会抛出数组下标越界
        checkPositionIndex(index);
        // 传入参数集合对象转为对象数组
        Object[] a = c.toArray(); 
        // 获取数组最大长度
        int numNew = a.length;
        if (numNew == 0) 
            //如果传入空数组直接返回false
            return false;
        //pred表示为当前需要传入数据的上一个节点(原来的末尾节点) 
        //succ表示下一个节点
        Node<E> pred, succ;
        if (index == size) { 
            //如果数据插入的位置 = 实际的数据量,表示是插在末尾
            succ = null;
            pred = last;
        } else { 
            // 这个是在中间插入
            succ = node(index);
            pred = succ.prev;
        }
​
        for (Object o : a) { 
            // 循环操作,与add()方法基本一致
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }
​
        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }
​
        size += numNew;
        modCount++;
        return true;
    }