【Java面试】List知识点总结

简介: 【Java面试】List知识点总结

1.ArrayList与LinkedList区别

| | ArrayList| LinkedList |
| :---: | :----: | :---: |
| 数据结构| Object数组| 双向链表|
| 线程安全| 否| 否|
| add时间复杂度| O(n)| O(1)|
| delete时间复杂度 | O(n)| O(1)|
| get时间复杂度| O(1)| O(n)|
| 快速访问| 支持| 不支持|
| 存储空间|默认空余一些空间|保存数据&前后指针|

RandomAccess
首先抛出一个问题,让我们带着问题,去了解真相
1.数据结构是否有随机访问功能,是如何实现的?
2.数据结构是否有随机访问功能,取决于该接口?
3.该接口实现了具体的随机访问功能?

然而,事实好像并未如此,查看JDK源码,可知此接口中什么都没有定义和实现。

package java.util;

public interface RandomAccess {
}

由此可知,RandomAccess 并非具体实现,而只是一个标识而已,标识实现了这个接口的类,具有随机访问的功能。

2.ArrayList 扩容机制

2.1 ArrayList 重要变量定义

    //默认的容量大小
    private static final int DEFAULT_CAPACITY = 10;
    //默认的空对象数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //真正存储数据的对象数组
    transient Object[] elementData; // non-private to simplify nested class access
    //arraylist的大小,并非是elementData大小
    private int size;
    //默认arraylist最大的容量,为int最大值减去8
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

2.2 ArrayList 初始化

无参初始化

    //初始化为空数组
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

含参(指定容量大小)初始化-

    //initialCapacity想要初始化的大小
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //如果initialCapacity大于0,则直接以期望的大小,创建数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //如果initialCapacity等于0,则直接空数组赋值
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //如果initialCapacity小于0,则抛出初始化异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

含参(指定已有list数据)初始化

    public ArrayList(Collection<? extends E> c) {
        //将已知list转为array,直接赋值数组
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            //规避已知的list不是对象数组,则强转为对象数组赋值
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

2.3 ArrayList 扩容

想要知道如何真正去实现扩容,我们需要抽丝剥茧,从最初的add看起

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

每次add时,都会去调用ensureCapacityInternal进行容量扩容检测,传入的期望的最小容量是当前arraylist大小+1

    private void ensureCapacityInternal(int minCapacity) {
       //如果当前对象数组就是空的,则最小的扩容大小应该是DEFAULT_CAPACITY,即10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        //如果想要扩容的大小,大于当前对象数组的实际大小,则调用grow进行扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

如果想要扩容的大小,大于当前对象数组的实际大小,则调用grow进行扩容

    private void grow(int minCapacity) {
        // 得到现有数组的大小,将现有数组的大小除以2,然后加上当前大小,也就是现有数组大小的1.5倍
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
       //如果扩容1.5倍之后的容量,依然达不到用户期望的大小,则直接用期望大小赋值
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
       //如果扩容的大小,大于了MAX_ARRAY_SIZE ,则调用hugeCapacity
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 将元素进行copy
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

由上诉代码分析可知,扩容首先默认扩容为当前容量的1.5倍,但是如果扩展之后的容量依然达不到期望的大小,则会直接以期望的大小去扩容。
当然了,在真正扩容之前,还需判断一下,目前得到的最终大小是否超过了arraylist的限制MAX_ARRAY_SIZE ,如果超过了,则直接赋值为Integer.MAX_VALUE,否则为MAX_ARRAY_SIZE 。

 private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

3.ArrayList 与 Vector区别和联系

老规矩,先上源码,通过源码对比一下,ArrayList 和 Vector源码类的定义

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{***}

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{***}

由上可知,ArrayList和Vector都是list实现,而且实现的接口和拥有的功能几乎一致。
接下来,我们对比一下,具体功能的实现是否一致,以add举例
Vector add方法实现

  public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

ArrayListadd方法实现

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

对比可知,实现代码几乎一致,但是有一个细微区别,Vector 的方法上都加了synchronized 关键字,由此可知,Vector 是线程安全,ArrayList是非线程安全,其他功能和接口完全一致。

4.ArrayList ConcurrentModificationException异常

 final void checkForComodification() {
            if (ArrayList.this.modCount != this.expectedModCount) {
                throw new ConcurrentModificationException();
            }
        }

我们看到在ArrayList的内部类Itr中(实现了Iterator接口),有一个检测方法,iterator遍历过程中,add、remove、next方法都会先调用这个函数,检测ArrayList.this.modCount != this.expectedModCount,问题来了,modCount是做什么的?
我们依然是通过源码来探究原因,看一下这个遍历在哪里调用了?

 public E remove(int var1) {
        this.rangeCheck(var1);
        ++this.modCount;
        Object var2 = this.elementData(var1);
        int var3 = this.size - var1 - 1;
        if (var3 > 0) {
            System.arraycopy(this.elementData, var1 + 1, this.elementData, var1, var3);
        }

        this.elementData[--this.size] = null;
        return var2;
    }

    private void ensureExplicitCapacity(int var1) {
        ++this.modCount;
        if (var1 - this.elementData.length > 0) {
            this.grow(var1);
        }

    }
  public boolean add(E var1) {
        this.ensureCapacityInternal(this.size + 1);
        this.elementData[this.size++] = var1;
        return true;
    }

我们看到在ArrayList的add、remove方法中,都对这个遍历进行了++操作,那立意就显而易见了,这个变量是为了记录ArrayList内部数组操作次数的。
那么为什么会报出这个异常呢?expectedModCount又是怎么来的,我们看到expectedModCount实际上是迭代器里面的一个变量,在通过迭代器进行list的操作时,expectedModCount = modCount,

private class Itr implements Iterator<E> {
        int cursor;
        int lastRet = -1;
        int expectedModCount;

        Itr() {
            this.expectedModCount = ArrayList.this.modCount;
        }

        public boolean hasNext() {
            return this.cursor != ArrayList.this.size;
        }

        public E next() {
            this.checkForComodification();
            int var1 = this.cursor;
            if (var1 >= ArrayList.this.size) {
                throw new NoSuchElementException();
            } else {
                Object[] var2 = ArrayList.this.elementData;
                if (var1 >= var2.length) {
                    throw new ConcurrentModificationException();
                } else {
                    this.cursor = var1 + 1;
                    return var2[this.lastRet = var1];
                }
            }
        }

        public void remove() {
            if (this.lastRet < 0) {
                throw new IllegalStateException();
            } else {
                this.checkForComodification();

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

        public void forEachRemaining(Consumer<? super E> var1) {
            Objects.requireNonNull(var1);
            int var2 = ArrayList.this.size;
            int var3 = this.cursor;
            if (var3 < var2) {
                Object[] var4 = ArrayList.this.elementData;
                if (var3 >= var4.length) {
                    throw new ConcurrentModificationException();
                } else {
                    while(var3 != var2 && ArrayList.this.modCount == this.expectedModCount) {
                        var1.accept(var4[var3++]);
                    }

                    this.cursor = var3;
                    this.lastRet = var3 - 1;
                    this.checkForComodification();
                }
            }
        }

        final void checkForComodification() {
            if (ArrayList.this.modCount != this.expectedModCount) {
                throw new ConcurrentModificationException();
            }
        }
    }

通过上面迭代器的源码可以验证我们上面的猜想。那么这个异常怎么出现的,就显而易见了,一般出现在,我们通过Iterator遍历list、map的时候,而同时又通过list、map的remove、add操作去操作数组,导致ArrayList.this.modCount != this.expectedModCount

总结

1.ArrayList、LinkedList、Vendor的区别和联系,主要从底层数据结构实现不同、add:remove:get相应的时间复杂度不一样、是否线程安全。
2.ArrayList如果不指定大小,默认初始容量为0,第一次添加元素时,初始容量开始为初始化为10,之后每次添加元素,进行扩容检测机制(如果size+1,大于当前数组大小,则进行扩容,首先扩容为1.5倍,如果依然不能满足,则扩充为Int最大值-8),ArrayList每次扩容,都会进行数组copy。
3.ConcurrentModificationException发生是因为我们在使用迭代器遍历List的同时,还使用了List相应的remove、add进行元素增加或删除,导致不一。

目录
相关文章
|
29天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1月前
|
存储 设计模式 SQL
[Java]知识点
本文涵盖Java编程中的多个知识点,包括静态与动态代理、基本数据类型转换、设计模式、异常处理、类加载、序列化、ORM框架、IPv4地址分类、编译与解释等。文章详细介绍了每个知识点的原理和使用方法,并提供了相关示例和注意事项。
39 16
[Java]知识点
|
23天前
|
网络协议 Java 物联网
Java网络编程知识点
Java网络编程知识点
39 13
|
1月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
52 4
|
2月前
|
Android开发
Android面试高频知识点(1) 图解Android事件分发机制
Android面试高频知识点(1) 图解Android事件分发机制
|
2月前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
|
2月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
2月前
|
消息中间件 Android开发 索引
Android面试高频知识点(4) 详解Activity的启动流程
Android面试高频知识点(4) 详解Activity的启动流程
28 3
|
2月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
26 2
|
2月前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
58 1