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

目录
相关文章
|
2月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
57 2
|
12天前
|
Web App开发 前端开发 Linux
「offer来了」浅谈前端面试中开发环境常考知识点
该文章归纳了前端开发环境中常见的面试知识点,特别是围绕Git的使用进行了详细介绍,包括Git的基本概念、常用命令以及在团队协作中的最佳实践,同时还涉及了Chrome调试工具和Linux命令行的基础操作。
「offer来了」浅谈前端面试中开发环境常考知识点
|
14天前
|
消息中间件 Android开发 索引
Android面试高频知识点(4) 详解Activity的启动流程
讲解Activity的启动流程了,Activity的启动流程相对复杂一下,涉及到了Activity中的生命周期方法,涉及到了Android体系的CS模式,涉及到了Android中进程通讯Binder机制等等, 首先介绍一下Activity,这里引用一下Android guide中对Activity的介绍:
27 4
|
12天前
|
存储 移动开发 前端开发
「offer来了」面试中必考的15个html知识点
该文章汇总了前端面试中常见的15个HTML知识点,涵盖了从HTML文档的规范书写、doctype声明的作用到新兴的HTML5标签应用及移动端viewport设置等内容,旨在帮助求职者更好地准备相关技术面试。
「offer来了」面试中必考的15个html知识点
|
2天前
|
JSON 安全 前端开发
第二次面试总结 - 宏汉科技 - Java后端开发
本文是作者对宏汉科技Java后端开发岗位的第二次面试总结,面试结果不理想,主要原因是Java基础知识掌握不牢固,文章详细列出了面试中被问到的技术问题及答案,包括字符串相关函数、抽象类与接口的区别、Java创建线程池的方式、回调函数、函数式接口、反射以及Java中的集合等。
9 0
|
12天前
|
Web App开发 前端开发 JavaScript
「offer来了」1张思维导图,6大知识板块,带你梳理面试中CSS的知识点!
该文章通过一张思维导图和六大知识板块系统梳理了前端面试中涉及的CSS核心知识点,包括CSS框架、基础样式问题、布局技巧、动画处理、浏览器兼容性及性能优化等方面的内容。
|
2月前
|
Java
用JAVA架建List集合为树形结构的代码方法
这段代码定义了一个表示树形结构的 `Node` 类和一个用于构建树形结构的 `TreeController`。`Node` 类包含基本属性如 `id`、`pid`、`name` 和 `type`,以及子节点列表 `children`。`TreeController` 包含初始化节点列表并将其转换为树形结构的方法。通过过滤和分组操作实现树形结构的构建。详情可见:[代码示例链接1](http://www.zidongmutanji.com/zsjx/43551.html),[代码效果参考链接2](https://www.257342.com/sitemap/post.html)。
32 5
|
1月前
|
Java API 开发者
代码小妙招:用Java轻松获取List交集数据
在Java中获取两个 `List`的交集可以通过 `retainAll`方法和Java 8引入的流操作来实现。使用 `retainAll`方法更为直接,但会修改原始 `List`的内容。而使用流则提供了不修改原始 `List`、更为灵活的处理方式。开发者可以根据具体的需求和场景,选择最适合的方法来实现。了解和掌握这些方法,能够帮助开发者在实际开发中更高效地处理集合相关的问题。
39 1
|
2月前
|
存储 Java API
【Java高手必备】揭秘!如何优雅地对List进行排序?掌握这几种技巧,让你的代码瞬间高大上!
【8月更文挑战第23天】本文深入探讨了Java中对List集合进行排序的各种方法,包括使用Collections.sort()、自定义Comparator以及Java 8的Stream API。通过示例代码展示了不同情况下如何选择合适的方法:从简单的整数排序到自定义类对象的排序,再到利用Comparator指定特殊排序规则,最后介绍了Stream API在排序操作中的简洁应用。理解这些技术的区别与应用场景有助于提高编程效率。
24 4
|
2月前
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。