首页 > 编程语言 >Java中的List集合:从ArrayList到CopyOnWriteArrayList

Java中的List集合:从ArrayList到CopyOnWriteArrayList

时间:2024-11-23 16:04:11浏览次数:10  
标签:Java ArrayList List System 线程 数组 println out

目录

1. List接口简介

在Java集合框架中,List接口是最常用的接口之一。List表示一个有序的集合,允许重复元素,并提供了基于索引的操作。

主要的List方法包括:

  • add(E e): 添加元素到列表末尾
  • add(int index, E element): 在指定位置插入元素
  • get(int index): 获取指定位置的元素
  • remove(int index): 移除指定位置的元素
  • set(int index, E element): 替换指定位置的元素
  • size(): 返回列表的大小

2. ArrayList

ArrayList是List接口最常用的实现之一,它的底层是基于动态数组实现。

2.1 基本特性

  • 允许null元素
  • 有序集合
  • 非同步(线程不安全)
  • 随机访问效率高
  • 插入和删除操作可能较慢(特别是在列表中间)

2.2 内部实现

ArrayList内部使用一个对象数组来存储元素。当数组容量不足时,会进行扩容操作。

2.3 扩容机制

当ArrayList的大小超过其当前容量时,会触发扩容操作:

  1. 创建一个新的更大的数组(通常是原容量的1.5倍)
  2. 将原数组中的元素复制到新数组
  3. 将新数组赋值给elementData字段
/**
 * 增加数组的容量,以确保它可以容纳至少minCapacity个元素
 * 此方法在内部用于管理动态数组的大小,当数组的当前容量不足以容纳新元素时调用
 * 
 * @param minCapacity 数组需要能够容纳的最小容量
 * @return 扩容后的数组对象
 */
private Object[] grow(int minCapacity) {
    // 获取当前数组的容量
    int oldCapacity = elementData.length;
    // 检查当前数组是否已经初始化或者不是默认的空数组
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 计算并获取新的数组容量,确保它既可以满足最小容量要求,又符合首选的扩容策略
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, //最小增长量
                oldCapacity >> 1           /* 优先增长量*/);
        // 使用新的容量复制当前数组,并返回扩容后的数组
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        // 如果当前数组未初始化或等于默认的空数组,使用默认容量或minCapacity中较大的一个来初始化数组
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

/**
 * 计算基于旧长度和增长需求的新数组长度。
 * 本方法旨在确定一个满足最小增长要求的新长度,同时考虑一个优先增长量。
 * 在特殊情况下(例如,当首选长度超过最大数组长度限制时),将使用不同的计算策略。
 *
 * @param oldLength 原始数组长度,必须是非负数。
 * @param minGrowth 最小增长量,也就是我现在至少还要增加的容量,必须是正数,表示所需增长的最小长度。
 * @param prefGrowth 优先增长量,表示期望的增长量,为原容量的一半。
 * @return 根据参数计算的新数组长度。
 */
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
    // 前提条件检查因内联而省略
    // 应断言 oldLength 非负
    // 应断言 minGrowth 为正数

    // 计算首选长度,可能溢出
    int prefLength = oldLength + Math.max(minGrowth, prefGrowth);

    // 如果首选长度在合理范围内,直接返回  SOFT_MAX_ARRAY_LENGTH是数组最大长度的限制
    if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
        return prefLength;
    } else {
        // 将冷代码放在单独的方法中处理
        return hugeLength(oldLength, minGrowth);
    }
}

2.4 性能考虑

  • 随机访问: O(1)
  • 插入/删除(末尾): 平均O(1)
  • 插入/删除(中间): O(n)
  • 查找: O(n)

2.5 代码示例

import java.util.ArrayList;
import java.util.List;

public class ArrayListExample {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<>();

        // 添加元素
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Orange");
        
        // 在指定位置插入元素
        fruits.add(1, "Mango");
        
        System.out.println("Fruits 列表: " + fruits);//Fruits 列表: [Apple, Mango, Banana, Orange]
        
        // 获取元素
        String secondFruit = fruits.get(1);
        System.out.println("第二个: " + secondFruit);//第二个: Mango
        
        // 修改元素
        fruits.set(2, "Grape");
        
        // 删除元素
        fruits.remove("Apple");
        
        System.out.println("更新后的列表: " + fruits);//更新后的列表: [Mango, Grape, Orange]
        
        // 遍历列表
        for (String fruit : fruits) {
            System.out.println(fruit);//Mango Grape Orange
        }
        
        // 检查是否包含某元素
        boolean containsBanana = fruits.contains("Banana");
        System.out.println("有没有Banana? " + containsBanana);//有没有Banana? false
        
        // 获取列表大小
        int size = fruits.size();
        System.out.println("列表大小: " + size);//列表大小: 3
    }
}

3. LinkedList

LinkedList是基于双向链表实现的List,同时也实现了Deque接口。
在这里插入图片描述### 3.1 特性

  • 允许null元素
  • 有序集合
  • 非同步(线程不安全)
  • 插入和删除操作效率高(特别是在列表两端)
  • 随机访问效率较低

3.2 内部实现

LinkedList使用双向链表存储元素,每个节点包含前驱节点、后继节点和元素值。
在这里插入图片描述

3.3 性能考虑

  • 随机访问: O(n)
  • 插入/删除(两端): O(1)
  • 插入/删除(中间): O(n)
  • 查找: O(n)

3.4 代码示例

import java.util.LinkedList;
import java.util.List;

public class LinkedListExample {
    public static void main(String[] args) {
        LinkedList<String> animals = new LinkedList<>();
        
        // 添加元素
        animals.add("Lion");
        animals.add("Tiger");
        animals.add("Bear");
        
        // 在列表开头添加元素
        animals.addFirst("Elephant");
        
        // 在列表末尾添加元素
        animals.addLast("Giraffe");
        
        System.out.println("链表: " + animals);//链表: [Elephant, Lion, Tiger, Bear, Giraffe]
        
        // 获取第一个和最后一个元素
        String firstAnimal = animals.getFirst();
        String lastAnimal = animals.getLast();
        System.out.println("第一个: " + firstAnimal);//第一个: Elephant
        System.out.println("最后一个: " + lastAnimal);//最后一个: Giraffe
        
        // 删除第一个和最后一个元素
        animals.removeFirst();
        animals.removeLast();
        
        System.out.println("更新后的链表: " + animals);//更新后的链表: [Lion, Tiger, Bear]
        
        // 使用索引访问元素
        String secondAnimal = animals.get(1);
        System.out.println("第二个: " + secondAnimal);//第二个: Tiger
        
        // 修改元素
        animals.set(1, "Leopard");
        
        // 遍历列表
        for (String animal : animals) {
            System.out.println(animal);//Lion Leopard  Bear
        }
        
        // 检查是否包含某元素
        boolean containsTiger = animals.contains("Tiger");
        System.out.println("链表是否存在Tiger? " + containsTiger);//链表是否存在Tiger? false
        
        // 获取列表大小
        int size = animals.size();
        System.out.println("链表大小: " + size);//链表大小: 3
    }
}

4. Vector

Vector是一个古老的同步List实现,现在已经不推荐使用。

4.1 特性

  • 同步(线程安全)
  • 允许null元素
  • 有序集合
  • 基于动态数组实现

4.2 与ArrayList的比较

  1. 同步性: Vector是同步的,ArrayList不是。
  2. 性能: 在单线程环境下,ArrayList通常优于Vector。
  3. 扩容: Vector默认扩容为原来的2倍,ArrayList是1.5倍。

4.3 代码示例

import java.util.Vector;

public class VectorExample {
    public static void main(String[] args) {
        Vector<String> vector = new Vector<>();
        
        // 添加元素
        vector.add("One");
        vector.add("Two");
        vector.add("Three");
        
        System.out.println("Vector: " + vector);//Vector: [One, Two, Three]
        
        // 在指定位置插入元素
        vector.add(1, "Four");

        System.out.println("Vector: " + vector);//Vector: [One, Four, Two, Three]
        // 获取元素
        String element = vector.get(2);
        System.out.println("第三个元素: " + element);//第三个元素: Two
        
        // 修改元素
        vector.set(0, "Zero");
        
        // 删除元素
        vector.remove("Three");
        
        System.out.println("更新后Vector: " + vector);//更新后Vector: [Zero, Four, Two]
        
        // 获取向量大小
        int size = vector.size();
        System.out.println("Vector 大小: " + size);//
        
        // 检查是否包含某元素
        boolean containsTwo = vector.contains("Two");//Vector 大小: 3
        System.out.println("是否包含 'Two'? " + containsTwo);//是否包含 'Two'? true
    }
}

5. Collections.synchronizedList

Collections.synchronizedList是一个同步List包装器,可以将任何List转换为线程安全的List。

在这里插入图片描述

根据list是否实现RandomAccess接口,决定使用哪种线程安全的实现,总之返回一个线程安全的List

5.1 特性

  • 同步(线程安全)
  • 包装了一个普通的List实现
  • 所有操作都是同步的

5.2 实现原理

synchronizedList通过在每个方法上添加同步块来实现线程安全。

在这里插入图片描述

5.3 使用场景

当需要在多线程环境中使用ArrayList或LinkedList时,可以使用synchronizedList。

5.4 代码示例

import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class SynchronizedListExample {

    public static void main(String[] args) {
        // 创建一个非线程安全的ArrayList
        List<String> unsafeList = new ArrayList<>();
//
//        // 使用Collections.synchronizedList将其转换为线程安全的List
//        List<String> safeList = Collections.synchronizedList(unsafeList);

        // 创建一个线程池,模拟多线程环境
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        // 提交多个任务,每个任务向safeList中添加元素
        for (int i = 0; i < 100; i++) {
            final int index = i;
            executorService.submit(() -> {
                String item = "Item " + index;
                unsafeList.add(item);
//                System.out.println(Thread.currentThread().getName() + " added: " + item);
            });
        }

        // 关闭线程池,等待所有任务完成
        executorService.shutdown();
        try {
            //等待所有任务完成,超时则关闭线程池
            if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
        }

        // 输出最终的列表内容,验证线程安全性
        System.out.println("Final unsafeList size: " + unsafeList.size());
//        for (String item : unsafeList) {
//            System.out.println(item);
//        }
    }
}

首先来看不安全List的结果:多次测试也会发现,最后的集合数量是存在问题的,方便显示我只输出了集合最后数量

在这里插入图片描述

接下来使用安全的List进行测试(注意把循环体、输出的list修改为safeList),无论执行多少次,最后的结果都是100

在这里插入图片描述

6. CopyOnWriteArrayList

CopyOnWriteArrayList是一个线程安全的List实现,专门设计用于处理读多写少的并发场景。

CopyOnWrite一般就是写时复制:当要对某个对象进行修改时,先对该对象进行复制一份,在副本上进行修改,修改完毕后再将原来的对象指向副本完成更新,因此要注意当对象较大时会耗费较大的空间

6.1 特性

  • 线程安全
  • 适用于读多写少的场景
  • 写操作开销大(复制整个数组)
  • 读操作不需要加锁,性能高

6.2 实现原理

  • 读操作不加锁,直接返回数组元素(如果有在修改,那就先读取原来的数组)。
  • 写操作会创建一个新的数组副本,在副本上进行修改,然后用新数组替换旧数组。

这里看看add方法来源码
在这里插入图片描述

6.3 使用场景

  • 读操作远多于写操作的并发场景
  • 监听器列表
  • 需要频繁遍历的线程安全列表

6.4 代码示例

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        
        // 添加元素
        list.add("Red");
        list.add("Green");
        list.add("Blue");
        
        System.out.println("初始 list: " + list);
        
        // 创建一个读线程
        Runnable reader = () -> {
            for (String color : list) {
                System.out.println(Thread.currentThread().getName() + " 正在读: " + color);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        
        // 创建一个写线程
        Runnable writer = () -> {
            list.add("Yellow");
            System.out.println(Thread.currentThread().getName() + " 写入 Yellow");
            list.remove("Green");
            System.out.println(Thread.currentThread().getName() + " 删除 Green");
        };
        
        // 启动多个读线程和一个写线程
        new Thread(reader, "Reader1").start();
        new Thread(reader, "Reader2").start();
        new Thread(writer, "Writer").start();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("最后的 list: " + list);
    }
}

输出结果:

这里能够看到:writer线程写入yellow并且删除green后,两个reader线程都仍然读取到了green并且都没有读取到插入的yellow,正是因为两个读线程读取的快照是原数组,而修改的对象是原对象的副本,再看最后的list发现,green被删除了也插入了yellow,正是修改完成后重新指向了新数组的结果。

7. 性能比较

下面是各种List实现在不同操作上的性能比较:

操作ArrayListLinkedListVectorCopyOnWriteArrayList
随机访问O(1)O(n)O(1)O(1)
插入/删除(开头)O(n)O(1)O(n)O(n)
插入/删除(结尾)O(1)*O(1)O(1)*O(n)
插入/删除(中间)O(n)O(n)O(n)O(n)
搜索O(n)O(n)O(n)O(n)

*注: 当需要扩容时为O(n)

8. 选择合适的List实现

  1. ArrayList: 适用于随机访问频繁、插入删除操作较少的场景。
  2. LinkedList: 适用于频繁在两端插入删除,但很少随机访问的场景。
  3. Vector: 不推荐使用,已被ArrayList和Collections.synchronizedList取代。
  4. Collections.synchronizedList: 需要线程安全且读写操作都比较频繁的场景。
  5. CopyOnWriteArrayList: 读多写少的并发场景,如监听器列表。

9. 总结

Java提供了多种List实现,每种实现都有其特定的用途和性能特征。在选择使用哪种List实现时,需要考虑以下因素:

  1. 是否需要线程安全
  2. 读写操作的频率
  3. 随机访问的需求
  4. 插入和删除操作的位置和频率
  5. 内存使用和性能要求

如果想以此对比Map家族可参考:
链接: Java中的Map集合

希望本文章能对各位有所帮助!

标签:Java,ArrayList,List,System,线程,数组,println,out
From: https://blog.csdn.net/2303_76892351/article/details/143993731

相关文章

  • 利用Java爬虫获取商品评论:技术与实践
    在电子商务的激烈竞争中,商品评论作为消费者购买决策的重要参考,对于商家来说具有不可估量的价值。Java作为一种强大的编程语言,其丰富的库支持使得爬虫技术成为获取这些数据的有效手段。本文将详细介绍如何使用Java进行商品评论的爬取,并提供相应的代码示例。Java爬虫基础Java......
  • 用Java爬虫“偷窥”商品评论:一场代码与网页的“谍战”
    在这个数字化的时代,商品评论就像是隐藏在网页深处的秘密情报,对于我们这些“情报分析师”来说,获取这些情报就是一场刺激的“谍战”。而Java,就是我们手中的瑞士军刀。今天,就让我们用Java来“偷窥”那些商品评论,看看它们背后隐藏的秘密。Java爬虫:不是007,但胜似007Java爬虫,听起......
  • JAVA八股与代码实践----解决循环依赖
    1、三级缓存1.1、一级缓存一级缓存:singletonObjects功能:存储已完全初始化完成的单例Bean。作用:解决完全初始化后的Bean的复用问题:一级缓存是最终的Bean存储地,用于存储所有已完成初始化的单例Bean。任何时候需要一个单例Bean,都会优先从一级缓存中获取,避免重......
  • Java 题目集 4 - 6 总结
    一、前言在Java编程学习的漫长道路上,题目集4-6犹如一座座充满挑战与机遇的山峰,促使我们不断攀登,拓展知识边界,提升编程技能与思维深度。这一系列题目集犹如一场全方位的能力试炼,全面检验了我们在多个关键领域的知识掌握程度与实践应用能力。从知识点的覆盖范围来看,题目集4......
  • 基于Java+SpringBoot+Mysql在线简单拍卖竞价拍卖竞拍系统功能设计与实现三
    一、前言介绍:免费学习:猿来入此1.1项目摘要主要源于互联网技术的快速发展和电子商务的普及。随着网络技术的不断进步,人们越来越依赖于互联网进行购物、交易和沟通。电子商务的兴起为在线拍卖提供了广阔的市场和便利的条件。在线拍卖系统通过搭建一个虚拟的拍卖平台,将传统的拍卖......
  • 基于Java+SpringBoot+Mysql在线简单拍卖竞价拍卖竞拍系统功能设计与实现四
    一、前言介绍:免费学习:猿来入此1.1项目摘要主要源于互联网技术的快速发展和电子商务的普及。随着网络技术的不断进步,人们越来越依赖于互联网进行购物、交易和沟通。电子商务的兴起为在线拍卖提供了广阔的市场和便利的条件。在线拍卖系统通过搭建一个虚拟的拍卖平台,将传统的拍卖......
  • idea javadoc
    生成类的Javadoc将光标放在类名上。按下 Option+Enter(⌥+Enter)。选择 Generate。选择 JavaDocComment。生成方法的Javadoc将光标放在方法名上。按下 Option+Enter(⌥+Enter)。选择 Generate。选择 JavaDocComment。快捷键总结生成类的Javadoc:Opt......
  • 软件测试报告-布里斯托网址/曼彻斯特网址的自动化测试Testsuite.side和用户故事UserSt
    软件测试报告-布里斯托网址/曼彻斯特网址的自动化测试Testsuite.side和用户故事UserStory和java_code曼切斯特/java+junit+seleniumIDE+side软件测试实现/布里斯托bristol.ac.uk的LinkGraphAdequacy 也就是userstory软件测试#testsuites.side 执行日志log先演示......
  • 第十章JavaScript的应用
    10.1JavaScript概述10.1.1JavaScript简介Jovusoripl是一种基于对象(Ohjet)和事件驱(FrentDriven)并具有安全性能的脚木语育,能够与HTML(超文本标记滔言)、Jara港言二起在Web页面中与Web客户交互,它无须经过先将数据传给服务器端(Sever).再传回来的过程,而直接可以由客户......
  • 数据结构-8.Java. 七大排序算法(下篇)
    本篇博客给大家带来的是排序的知识点,由于时间有限,分两天来写,下篇主要实现最后一种排序算法:归并排序。同时把中篇剩下的快排非递归实现补上.文章专栏:Java-数据结构若有问题评论区见欢迎大家点赞评论收藏分享如果你不知道分享给谁,那就分享给薯条.你们......