ArrayList结构原理介绍

目录

1、介绍

1.1、原理

1.2、性能

1.有序性能分析

1. 插入操作

2. 删除操作

3. 查找操作

2.无序性能分析

1. 插入操作

2. 删除操作

3. 查找操作

1.3、总结

2、扩容原理:

3、遍历

3.1、并发异常

3.2、CopyOnWrite容器

3.2.1、扩容

3.2.2、缺点

3.2.3、示例:

4、add

5、remove

5.1.remove(int index)

5.2.remove(Object o)

1.fastRemove

6、Fail-Fast机制

7、内存溢出

7.1、堆内存溢出 OutOfMemoryError

7.2、栈内存溢出 StackOverflowError


常见的数据结构:

1、介绍

1.1、原理

    ArrayList 是基于动态数组实现的,底层的数据存储在一个连续的内存块中。这使得大多数操作,包括访问元素和查询操作,都能够快速执行。

  • 随机访问ArrayList 可以使用索引来直接访问数组中的元素。查询操作的时间复杂度为 O(1),这是最佳的时间复杂度。
  • 简单的计算: 当你执行查询时,例如通过 myList.get(index),它只需直接访问该索引位置的数据。

当删除元素时,ArrayList 需要进行以下操作:

  • 查找到要删除的元素: 首先,若要根据值删除,可能需要先搜索元素(时间复杂度为 O(n)),如果是根据索引删除,则直接找到索引(O(1))。
  • 移动元素: 当元素被删除后,所有后续元素需要向前移动,以填补被删除元素留下的空缺。这使得删除操作的时间复杂度变为 O(n),因为在最坏情况下,可能需要移动整个数组部分。

1.2、minCapacity 

     minCapacity 通常是指在需要扩容时,容器所需的最小容量。它确保在添加元素时不会因容量不足而抛出 IndexOutOfBoundsException

1.3、构造器

1.3.1.无参的构造方法

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

        可以看到无参构造其实是为elementData赋值了一个默认的空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。也就是说,使用无参构造函数初始化 ArrayList 后,它当时的数组容量为 0。

在使用add方法的时候:

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

    transient Object[] elementData; 

    protected transient int modCount = 0;

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;



    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }


    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }


 /**
 * 要分配的最大数组大小
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * ArrayList 扩容的核心方法。
 * 
 * @param minCapacity 所需的最小容量
 */
private void grow(int minCapacity) {
    // 旧容量
    int oldCapacity = elementData.length;
    // 新容量设置为旧容量的1.5倍,通过位运算实现(oldCapacity / 2)
    // 位运算比整除运算更高效
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 检查新容量是否满足最小需要的容量
    // 如果新容量小于最小需要容量,则将新容量设为最小需要容量
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    
    // 检查新容量是否超过最大数组大小
    // 如果新容量大于 MAX_ARRAY_SIZE,调用 hugeCapacity 方法
    // 如果 minCapacity 超过最大容量,则新容量设为 Integer.MAX_VALUE
    // 否则,新容量设为 MAX_ARRAY_SIZE
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        newCapacity = hugeCapacity(minCapacity);
    }
    
    // 最后使用 Arrays.copyOf 扩容,将旧数组复制到新数组中
    // 此时新数组的大小为新容量
    elementData = Arrays.copyOf(elementData, newCapacity);
}



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

这里使用无参构造初始化了ArrayList,当后面调用add()进行添加操作时,将会给数组分配默认的初始容量为DEFAULT_CAPACITY = 10。

int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)! 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数

>>1 右移一位相当于除 2,右移 n 位相当于除以 2 的 n 次方。

简单介绍下:

    transient 关键字用于标记某个字段不应该被序列化。当一个对象被序列化时,它的所有字段都会被保存到字节流中。但是,如果某些字段标记为 transient,则这些字段在序列化过程中会被忽略,而不会被写入到字节流中。

目的:

  • 内存效率ArrayList 中的 elementData 数组用于存储列表中的元素。当整个 ArrayList 被序列化时,如果 elementData 因为不需要序列化,序列化效率会提高,同时也能减少得到 ArrayList 对象的存储空间。

  • 使用场景: 在某些情况下,可能不需要持久存储整个集合的状态。例如,你可能只希望保存元数据(例如 size 等),而不关心实际的内容。在这种情况下,标记 elementData 为 transient 是合理的。

  • 灵活性与稳定性: 当反序列化一个 ArrayList 时,elementData 被重置为适用相应的数组(通常为一个新的数组),因此即使在对象的序列化和反序列化过程中丢失了 elementData 的具体数据,也不影响 ArrayList 的基本功能。

对上述代码进行简单解释:

添加第一个元素:

        当你尝试添加第一个元素时,elementData.length 为 0,因为 ArrayList 还没有任何元素。此时,ensureCapacityInternal() 方法被调用,minCapacity 被设置为 10(这是 ArrayList 默认的初始容量)。

注意:此处因为没有元素,调用的是无参构造器,所以初始化容量为10。

        由于 minCapacity - elementData.length > 0 成立(10 - 0 > 0),因此调用 grow(minCapacity) 方法来扩容。

添加第二个元素:

        当添加第二个元素时,minCapacity 被计算为 2。此时,elementData.length 已经扩容为 10(由于添加了第一个元素)。
此时 minCapacity - elementData.length > 0 不成立(2 - 10 > 0),所以不会调用 grow(minCapacity) 方法,ArrayList 继续使用现有的容量。


添加第三到第十个元素:

        添加第三、第四、直到第十个元素时,elementData.length 仍然为 10,因此同样不会调用 grow(minCapacity) 方法,ArrayList 保持现有容量。


添加第十一个元素:

        当添加第十一个元素时,minCapacity 被计算为 11。这时,minCapacity 大于 elementData.length(11 > 10),因此 ensureExplicitCapacity 会进入 grow(minCapacity) 方法,触发扩容。

1.3.2.有参,根据传入的数值大小,创建指定长度的数组

3.通过传入Collection元素列表进行生成

Collections.synchronizedList(new ArrayList<>())

1.2、性能

ArrayList的插入速度一定会比LinkedList的慢吗?

当插入有序的时候,ArrayList的效率较高。

示例:

public class LinkedListTest {
    public static void main(String[] args) {
        //LinkedList
        long l1 = System.currentTimeMillis();
        LinkedList<String> linkedList = new LinkedList<>();
        //Random random = new Random(1000);
        for (int i =0;i<1000000;i++){
            linkedList.add(i+"");
        }
        long l2 = System.currentTimeMillis();
        System.out.println("LinkedList耗时"+(l2-l1));

        //ArrayList
        long l3 = System.currentTimeMillis();
        ArrayList<String> arrayList = new ArrayList<>();
        for (int i =0;i<1000000;i++){
            arrayList.add(i+"");
        }
        long l4 = System.currentTimeMillis();
        System.out.println("ArrayList耗时"+(l4-l3));
    }
}

输出:
LinkedList耗时205
ArrayList耗时179

1.有序性能分析

1. 插入操作

        头部插入:若要在 ArrayList 的头部插入元素,由于它是基于数组实现的,需要将后续所有元素依次往后移动一个位置,时间复杂度为 O(n),其中 n 是数组中元素的数量。即便数据有序,这个移动操作也不可避免,性能欠佳。

        尾部插入:若数组容量足够,在尾部插入元素的时间复杂度为 O(1),因为只需在数组末尾添加元素即可。当数组容量不足时,需要进行扩容操作,扩容操作的时间复杂度为 O(n),不过由于扩容是偶尔发生的,平均下来尾部插入的时间复杂度仍接近 O(1)。

        中间插入:在有序数据的中间插入元素,同样需要将插入位置之后的元素依次往后移动,时间复杂度为 O(n)。

2. 删除操作

        头部删除:删除头部元素时,需要将后续所有元素依次往前移动一个位置,时间复杂度为 O(n)。

        尾部删除:删除尾部元素的时间复杂度为 O(1),因为只需将数组的大小减 1 即可。

        中间删除:删除中间元素时,需要将删除位置之后的元素依次往前移动,时间复杂度为 O(n)。

3. 查找操作

        按索引查找:由于 ArrayList 支持随机访问,按索引查找元素的时间复杂度为 O(1),可以直接通过数组下标访问对应元素。

        按值查找:若要按值查找元素,需要遍历整个数组,时间复杂度为 O(n)。但如果数据是有序的,可以使用二分查找,将时间复杂度降低到 O(logn)。

2.无序性能分析

1. 插入操作

        头部插入:和有序数据一样,在头部插入元素需要将后续元素依次往后移动,时间复杂度为 O(n)。

        尾部插入:数组容量足够时,尾部插入时间复杂度为 O(1);容量不足时,扩容操作平均时间复杂度接近 O(1)。

        中间插入:在中间插入元素,需要移动插入位置之后的元素,时间复杂度为 O(n)。

2. 删除操作

        头部删除:删除头部元素需要将后续元素依次往前移动,时间复杂度为 O(n)。

        尾部删除:尾部删除时间复杂度为 O(1)。

        中间删除:删除中间元素需要移动删除位置之后的元素,时间复杂度为 O(n)。

3. 查找操作

        按索引查找:同样支持随机访问,按索引查找元素的时间复杂度为 O(1)。

        按值查找:由于数据无序,只能通过遍历数组来查找元素,时间复杂度为 O(n)。

1.3、总结

        以下结论适用于通常情况哦,也有个别极端例子比较特殊,当数据量远远大的时候,linkedlist和arraylist的插入数据性能将会越来越接近,且在有序情况下,Arraylist的数据比linkedlist能稍好点。

        对于随机访问(通过索引来获取元素),ArrayList的性能通常优于LinkedList,因为它可以直接通过索引访问数组中的元素。相比之下,LinkedList需要从头节点或尾节点开始遍历。


        对于元素的插入和删除操作,LinkedList通常优于ArrayList,尤其是在列表中间位置进行操作时。LinkedList的插入和删除操作只需改变相邻节点的引用,而不需要移动其他元素。而ArrayList在插入或删除元素时可能需要移动大量元素。


2、扩容原理:

        ArrayList是使用数组作为底层数据结构来实现List的。当ArrayList需要扩容时,会创建一个新的数组来存储元素,并将旧数组中的元素复制到新数组中。ArrayList的扩容策略如下:

1、当ArrayList的元素数量超过了其数组的长度时,就会触发扩容操作。
扩容时,ArrayList会创建一个新的容量更大的数组,通常是原数组容量的1.5倍(可以通过修改源码进行调整)。
2、ArrayList会将旧数组中的元素按顺序复制到新的数组中。
3、将新数组设置为ArrayList的底层数组,完成扩容操作。


通过这种扩容策略,ArrayList能够在元素数量变多时,保持较好的性能。因为扩容操作的时间复杂度为O(n),其中n为元素数量。

public class ArrayList<E> implements List<E> {
    private static final int DEFAULT_CAPACITY = 10;
    private Object[] elementData;
    private int size;

    public ArrayList() {
        this.elementData = new Object[DEFAULT_CAPACITY];
        this.size = 0;
    }

    public void add(E e) {
        ensureCapacity(size + 1);
        elementData[size++] = e;
    }

    private void ensureCapacity(int minCapacity) {
        if (minCapacity > elementData.length) {
            int newCapacity = elementData.length + (elementData.length >> 1);
            if (newCapacity < minCapacity)
                newCapacity = minCapacity;
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    }

    // 其他方法省略...
}

示例:

public class ArrayListResizeDemo {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>(5);

        // 添加6个元素,触发扩容
        for (int i = 1; i <= 6; i++) {
            list.add(i);
        }

        System.out.println("List size: " + list.size()); // 输出:6
        System.out.println("List capacity: " + getArrayListCapacity(list)); // 输出:7
    }

    // 获取ArrayList的容量
    private static int getArrayListCapacity(ArrayList<?> list) {
        try {
            java.lang.reflect.Field capacityField = ArrayList.class.getDeclaredField("elementData");
            capacityField.setAccessible(true);
            return ((Object[]) capacityField.get(list)).length;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
            return -1;
        }
    }
}

输出:
List size: 6
List capacity: 7

        当你在代码中创建一个 ArrayList 并添加元素时,ArrayList 的容量和大小在追加元素时会发生变化。对于你提到的情况,让我们分析一下为什么 ArrayList 的容量不是 10,而是 7。

1. ArrayList的初始容量及扩容机制

  • 当你创建 ArrayList<Integer> list = new ArrayList<>(5); 时,ArrayList 被初始化为大小 0,并有一个初始容量为 5 的 elementData 数组。
  • ArrayList 的大小(size)是指当前实际上存储的元素数量,而容量(capacity)是指 elementData 数组可以容纳的最大元素数量。

2. 添加元素时的扩容机制

  • 当你添加第 6 个元素时,由于当前容量不足以容纳新元素,ArrayList 会触发扩容。
  • 扩容时,ArrayList 通常会将数组的容量增加到原来的 1.5 倍(有时为 1.5 倍,但这并不是固定的实现,具体实现依赖于 Java 的版本和 ArrayList 的实现细节)。

3. 为什么容量是 7?

  • 初始容量是 5。
  • 当你尝试添加第 6 个元素时,ArrayList 需要扩容。
  • 通常情况下,ArrayList 将会将容量扩增到原始容量的 1.5 倍,因此新的容量会是:
    • 原始容量 = 5
    • 新容量 = ceil(5 * 1.5) = ceil(7.5) = 8(在你的情况中,我们看到它变成了 7,而不是 8)

因为具体的实现可能在不同版本中稍有不同(如,这取决于 JVM 和内部的扩容策略),但扩容的影响是显而易见的。

        在此特定情况下,扩容后的新数组容量被设置为7。如果超过8(或者更常见的10)时会更高,因为重设所有的容量扩大至一定的值,这取决于实现中定义的策略。

4. 关键点

  • ArrayList 的初始容量和扩容策略是由其实现决定的。
  • 扩容过程会影响容量和性能,特别是在频繁增添元素时。

总结

        ArrayList 初始设定的容量为 5,经过扩容后显示的容量为 7,表明了它的 ArrayList 实现中的特定扩容逻辑和策略。为了优化和避免频繁的扩容,可以在创建 ArrayList 时合理估计所需的容量。

        如果多个线程同时访问和修改 ArrayList,可能会导致数据不一致。如果需要线程安全的集合,可以考虑使用 Vector 或 Collections.synchronizedList()和CopyOnWriteArrayList


3、遍历

3.1、并发异常

迭代ArrayList时做add或remove操作会发生什么?会抛出 java.util.ConcurrentModificationException

示例:

import java.util.ArrayList;
import java.util.Iterator;

public class ConcurrentModificationExample {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");
        
        // 通过迭代器遍历 ArrayList
        Iterator<String> iterator = list.iterator();
        
        while (iterator.hasNext()) {
            String fruit = iterator.next();
            System.out.println("Current fruit: " + fruit);
            // 在迭代期间对集合进行修改
            if (fruit.equals("Banana")) {
                list.add("Orange"); // 结构性修改
            }
        }
    }
}


输出:
Current fruit: Apple
Current fruit: Banana
Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1012)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:901)
    ...

1、解决方法
1.对JAVA集合进行遍历删除时务必要用迭代器
2.使用CopyOnWriteArrayList

2、总结
对于ArrayList,在使用Iterator遍历时,不能使用list.add()、list.remove()等改变list的操作,只能用it.remove()
原因:

        ArrayList不是线程安全的,需在单线程环境下使用,如果在遍历时还有别的线程做增删操作,必然会有问题,如数组下标越界
ArrayList#Iterator设计的是不能在迭代时有别的线程对list修改,此种修改对当前迭代器是可能存在问题的,所以增加了对modCount的校验
但当前迭代器可以remove,因为它自己删除就不是并发修改了,迭代器remove会重置expectedModCount,并将cursor往前一位。
        

        CopyOnWriteArrayList在使用Iterator遍历时,可以用list.add(),list.remove()等改变list的操作,但不支持it.remove()。
因为CopyOnWriteArrayList的Iterator实现类COWIterator会在创建时复制一份list的副本,之后迭代的是副本,**所以期间怎么对list.add(),list.remove()都没事,list.remove()操作的是原始的list,但不支持it.remove()**。
        Iterator中的本身就是副本,删除副本中的元素没意义,如果去删除原始list,在并发环境下此时list可能和创建迭代器时的副本已经完全不同了。

3.2、CopyOnWrite容器

        CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

         CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy(Arrays.copyOf),复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

3.2.1、扩容

  • 以数组实现。节约空间,但数组有容量限制。超出限制时会增加50%容量,用System.arraycopy()复制到新的数组,因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。

        发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

        读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。        

public E get(int index) {
    return get(getArray(), index);
}

3.2.2、缺点


1.内存占用问题,频繁的fullgc会占用性能。

2.数据一致性问题。

3.2.2.1、内存占用问题

        因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
  针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
  

3.2.2.2、数据一致性问题

        CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,即使可以查询到,建议不要使用CopyOnWrite容器。

3.2.3、示例:

在线用户列表

        在社交媒体或在线游戏中,在线用户的列表可能会被频繁访问,而用户的加入和离开相对少。使用 CopyOnWriteArrayList 可以有效避免并发问题。

import java.util.concurrent.CopyOnWriteArrayList;

class OnlineUserManager {
    private CopyOnWriteArrayList<String> onlineUsers = new CopyOnWriteArrayList<>();

    // 用户加入
    public void userJoined(String username) {
        onlineUsers.add(username);
        System.out.println(username + " has joined.");
    }

    // 用户离开
    public void userLeft(String username) {
        onlineUsers.remove(username);
        System.out.println(username + " has left.");
    }

    // 打印当前在线用户
    public void printOnlineUsers() {
        System.out.println("Current online users: " + onlineUsers);
    }
}

public class OnlineUserManagerDemo {
    public static void main(String[] args) {
        OnlineUserManager userManager = new OnlineUserManager();

        // 启动线程模拟用户登录
        Thread loginThread = new Thread(() -> {
            userManager.userJoined("Alice");
            userManager.userJoined("Bob");
        });

        // 启动线程模拟退出用户
        Thread logoutThread = new Thread(() -> {
            userManager.userLeft("Alice");
        });

        loginThread.start();
        logoutThread.start();

        try {
            loginThread.join();
            logoutThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        userManager.printOnlineUsers();
    }
}

4、add

第一种是直接插入列表尾部,另一种是插入某个位置。

        如果是直接插入尾部的话,那么只需调用 ensureCapacityInternal 方法做容量检测。如果空间足够,那么就插入,空间不够就扩容后插入。

示例:

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

        如果是插入的是某个位置,那么就需要将 index 之后的所有元素后移以为,之后再将元素插入至 index 处。

示例:


// 插入某个位置
public void add(int index, E element) {
    rangeCheckForAdd(index);
 
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}


5、remove

ArrayList 的删除方法有两个,分别是:

  • 删除某个位置的元素:remove(int index)
  • 删除某个具体的元素:remove(Object o)

5.1.remove(int index)

第一个删除方法:删除某个位置的元素

// 删除某个位置的元素
public E remove(int index) {
    rangeCheck(index);
 
    modCount++;
    E oldValue = elementData(index);
 
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
 
    return oldValue;
}

        首先做参数范围检查,接着将 index 位置后的所有元素都往前挪一位,最后减少列表大小。

删除某个特定的元素。

5.2.remove(Object o)

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

1.fastRemove

1.作用:

方法跳过边界检查,不返回删除值。

        这里会有一个疑问,那就是为什么不直接复用 remove(int index) 方法,而要新写一个方法呢?答案在 fastRemove 方法的注释中已经写了,就是为了跳过边界检查,提高效率。

/*
 * 用私有的方法 fastRemove 方法跳过边界检查,不返回删除值。
 */
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

        首先,遍历列表的所有元素,找到需要删除的元素索引,最后调用 fastRemove 方法删除该元素。

示例:

import java.util.ArrayList;

public class NormalRemoveExample {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        try {
            // 正常删除索引为 2 的元素,会进行边界检查并返回删除的值
            int removedValue = list.remove(2);
            System.out.println("Removed value: " + removedValue);
            System.out.println("Updated list: " + list);

            // 尝试删除一个越界的索引,会抛出 IndexOutOfBoundsException
            list.remove(10);
        } catch (IndexOutOfBoundsException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}

        在上述代码中,当调用 list.remove(2) 时,Java 会先检查索引 2 是否在列表的合法范围内,然后删除该索引对应的元素并返回它。

        而当尝试调用 list.remove(10) 时,由于索引越界,会抛出 IndexOutOfBoundsException 异常。

2、特点:

1.跳过边界检查

        在 fastRemove 方法中,没有对传入的 index 进行边界检查。这意味着如果调用者传入一个非法的索引(比如超出数组长度的索引),程序可能会抛出 ArrayIndexOutOfBoundsException 异常或者产生其他未定义的行为。

        这种做法的好处是可以节省边界检查的时间开销,提高方法的执行效率,但同时也要求调用者自己确保传入的索引是合法的。

2.不返回删除值

  fastRemove 方法的返回类型是 void,它只负责从数组中移除指定索引的元素,而不会返回被删除的元素。

        这样可以避免为了保存和返回删除值而进行的额外操作,进一步提高性能。


6、Fail-Fast机制

1.原理

     ArrayList 在其内部使用一个称为 modCount 的计数器来跟踪集合的修改次数。当创建迭代器时,该计数器的当前值会被记录下来。在迭代过程中,若在迭代器的生命周期内发现 modCount 的值发生变化(即发生了结构性修改),则迭代器会抛出 ConcurrentModificationException


7、内存溢出

7.1、堆内存溢出 OutOfMemoryError

1.从jvm的角度看发生的情况是:

1、动态扩展的栈内存无法满足内存分配。

2、建立新的线程没有足够内存创建栈。

2.从编码角度看发生的情况是:

1、内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

2、集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

3、代码中存在死循环或循环产生过多重复的对象实体;

4、使用的第三方软件中的BUG;

5、启动参数内存值设定的过小;

3.解决方案:

1、检查代码中是否存在一次性取出大量数据

2、检查循环体、递归调用中是否有大量导致gc无法回收的对象

3、-Xms -Xmx 配置最大最小堆内存大小,默认 -Xms256m -Xmx512m

示例:

/** * 堆内存溢出 */

private static void OutOfMemoryErrorExample() {
        List list = new ArrayList<>();
        String str = "";
        while (true) {
            str += new Date().toString();
            list.add(str);
        }
}

7.2、栈内存溢出 StackOverflowError

1.从jvm的角度看发生的情况是:

方法执行时申请不到新的空间存储(局部变量表, 操作数栈 , 动态链接 , 方法出口信息)。

2.从编码角度看发生的情况是: 一般出现在递归和循环依赖调用的代码块中。

3.解决方案:

1、检查递归和循环依赖调用的代码块,尽可能严谨。

2、-Xss 通过这个参数配置默认的jvm栈大小,这个标识即可以通过项目的配置也可以通过命令行来指定,默认 -Xss1m 或者 -Xss0.5m。

示例:

private static void StackOverflowErrorExample(int index) {
        if (index != 0) {
            StackOverflowErrorExample(++index);
        }
}

总结

        一般来说,方法在调用时发生的内存不足 会抛StackOverflowError ,发生在方法执行过程中的内存不足会抛 OutOfMemoryError、StackOverflowError(方法调用层次太深,内存不够新建栈帧),OutOfMemoryError(线程太多,内存不够新建线程),模拟内存溢出的时候可以设置jvm的启动参数,设置小点的内存量,让它尽快达到内存溢出的效果。

关于更多集合的介绍可以参考以下文章:

1、java集合的介绍

2、list 内存溢出_java内存溢出的情况

3、list的扩容机制

4、CopyOnWriteArrayList的使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值