【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进行元素增加或删除,导致不一。

目录
相关文章
|
6天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
11天前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
1天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
9 2
|
7天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
26 4
|
8天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
41 4
|
21天前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
45 5
|
21天前
|
安全 Java 程序员
深入Java集合框架:解密List的Fail-Fast与Fail-Safe机制
本文介绍了 Java 中 List 的遍历和删除操作,重点讨论了快速失败(fail-fast)和安全失败(fail-safe)机制。通过普通 for 循环、迭代器和 foreach 循环的对比,详细解释了各种方法的优缺点及适用场景,特别是在多线程环境下的表现。最后推荐了适合高并发场景的 fail-safe 容器,如 CopyOnWriteArrayList 和 ConcurrentHashMap。
48 5
|
19天前
|
Java 程序员 编译器
Java|如何正确地在遍历 List 时删除元素
从源码分析如何正确地在遍历 List 时删除元素。为什么有的写法会导致异常,而另一些不会。
16 3
|
19天前
|
Java 程序员
Java|List.subList 踩坑小记
不应该仅凭印象和猜测,就开始使用一个方法,至少花一分钟认真读完它的官方注释文档。
18 1
|
20天前
|
存储 Java
[Java]面试官:你对异常处理了解多少,例如,finally中可以有return吗?
本文介绍了Java中`try...catch...finally`语句的使用细节及返回值问题,并探讨了JDK1.7引入的`try...with...resources`新特性,强调了异常处理机制及资源自动关闭的优势。
18 1

热门文章

最新文章