Java List集合

1. List接口

        List接口继承自Collection接口,是单列集合的一个重要分支;将实现了List接口的对象称为List集合。List接口的特点可简单总结为“有序可重复”。

        List接口不但继承了Collection接口中的全部方法,而且增加了一些操作集合的特有方法:

2. ArrayList

2.1 ArrayList概述

        ArrayList是List接口的一个实现类,是程序中最常见的一种集合。在ArrayList内部封装了一个长度可变的数组对象,当存入的元素超过数组长度时,ArrayList会在内存空间中创建一个更大的数组来存储这些元素。

        ArrayList的主要特点如下:

        1、动态大小:ArrayList的大小是可以动态增长和缩小的。与普通的数组相比,ArrayList不需要指定初始大小,并且可以根据需要自动调整容量。

        2、随机访问:ArrayList通过索引来访问集合中的元素。由于ArrayList使用了数组实现,因此可以通过索引以常量时间复杂度(O(1))来获取元素。这使得ArrayList非常适合需要频繁随机访问元素的场景。

        3、允许重复元素:ArrayList可以包含重复的元素。这意味着可以多次添加相同的元素到ArrayList中。

        4、支持动态修改:ArrayList提供了一系列方法来修改集合中的元素,如添加、删除、插入和替换等操作。通过这些方法,可以方便地对集合进行修改。

        5、迭代和遍历:ArrayList实现了Iterable接口,因此可以使用迭代器(Iterator)或者增强型for循环来遍历集合中的元素。

2.2 ArrayList常用方法

        除了从Collection接口间接继承的方法外,ArrayList中还包含以下常用方法。

        1、void add(int index, E element):将给定的元素插入到指定位置,原位置及后续元素都顺序向后移动。

        2、E remove(int index):删除给定位置的元素,并将被删除的元素返回。

        3、E get(int index):获取集合中指定下标对应的元素,下标从0开始。

        4、E set(int index, E element):将给定的元素存入给定位置,并将原位置的元素返回。

2.3 ArrayList方法示例

        编写代码,测试ArrayList的使用。代码示意如下:

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

public class ArrayListDemo1 {
    public static void main(String[] args) {
        // 使用泛型ArrayList集合
        ArrayList<String> mylist = new ArrayList<>();
        // 向集合中添加3个元素
        mylist.add("one");
        mylist.add("two");
        mylist.add("three");
        System.out.println("此列表元素包括:"+mylist);
        System.out.println("------");
        // E get(int i) 返回集合中指定下标的元素
        String s = mylist.get(2);
        System.out.println("索引号为2的元素是:"+s);
        System.out.println("------");
        // E set(int index,E e)将给定位置的元素替换成
        // 新元素,并返回被替换的元素
        String old = mylist.set(2, "3");
        System.out.println("被替换的元素:"+old);
        System.out.println("此列表元素包括:"+mylist);
        System.out.println("------");
        // void add(int index,E e)向指定位置插入元素
        mylist.add(1,"2");
        System.out.println(mylist);
        System.out.println("------");
        // E remove(int index)删除并返回指定位置的元素
        // 删除索引号为1的元素
        old = mylist.remove(1);
        System.out.println(old);
        System.out.println(mylist);
        System.out.println("------");
        // List取子集List subList(int start,int end)
        // 包括0号元素,不包括2号元素
        List<String> subList = mylist.subList(0, 2);
        System.out.println("子集元素包括:"+subList);
        System.out.println("------");
        // 使用foreach语句遍历
        for (String e: mylist){
            System.out.print(e+' ');
        }
    }
}

2.4 ArrayList的扩容机制

        ArrayList的扩容机制是指在容量不足以容纳新元素时,自动增加ArrayList的容量,即ArrayList使用的内部数组的长度,以便能够容纳更多的元素。

        ArrayList类中定义了一个变量elementData,代表实际存储数据的数组。ArrayList的容量,指的就是这个数组的容量。

        1、初始容量

        ArrayList中的初始容量分为2种情况:未指定初始长度和手动指定初始长度。

        首先来看未指定初始长度的情况。

        ArrayList类中定义了一个空的Object类型数组,如下所示。

        在创建ArrayList对象时,如果没有指定初始容量,ArrayList会使用该数组作为内部数组,此时内部数组的长度为0。

        当第一次向该ArrayList对象中添加一个元素时,ArrayList会先初始化一个默认长度的内部数组,再将元素添加到该数组中。这个默认长度由ArrayList中的一个静态常量指定,如下所示。

        因此,ArrayList的初始容量为0,在添加第一个元素时动态扩容为10。

        接下来,我们来看手动指定初始长度的情况。

        手动指定初始长度是指通过ArrayList的带参构造器ArrayList(int initialCapacity)来创建ArrayList对象。此时,ArrayList会使用传入的值作为新建的内部数组的长度,源码入下图所示。

        因此,如果手动指定了长度,ArrayList的初始容量即为指定的长度。

        2、容量增长

        当添加元素导致ArrayList的大小超过当前容量时,ArrayList会自动进行容量增长。容量增长的策略是通过创建一个新的更大容量的数组,并将原有元素复制到新数组中。

        当需要增加容量时,ArrayList会根据一定的增量大小(通常为当前容量的一半)计算新的容量。

        新容量的计算公式为:newCapacity = oldCapacity + (oldCapacity >> 1)。

        例如,一个已经添加了10个元素的ArrayList,在添加第11个元素时,容量会扩容到10 + (10 >>1) =15。

        3、复制元素

        在进行容量增长时,ArrayList会创建一个新的数组,并将原有的元素复制到新数组中。

        复制元素的操作可能会导致一定的性能开销,特别是在ArrayList中存储大量元素时。例如,向ArrayList中依次添加128个元素,会导致ArrayList进行7次动态扩容。

        因此,集合操作的一项重要的最佳实践是:当事先知道要存储的总元素数量时,应使用ArrayList的带参构造器来创建ArrayList,以减少动态扩容带来的性能开销。

2.5 ArrayList的缩容操作

        与自动扩容机制不同,ArrayList并不会在删除元素时进行自动缩容操作。但是ArrayList提供了trimToSize方法,用于手动实现缩容的效果。

2.6 ArrayList与数组的区别

        这是一道常见的面试题,可以从以下几个方面回答。

        1、大小的固定性:数组一旦创建,大小是固定的,无法改变;ArrayList的大小是可变的,可以根据需要动态增长或缩小。

        2、对象类型和原始类型:数组可以存储对象类型和原始类型的值,例如,int[]可以存储整数,String[]可以存储字符串;ArrayList只能存储对象类型,不能直接存储原始类型,需要使用对应的包装类。

        3、功能和灵活性:数组的功能相对有限,仅提供了基本的操作,如访问和赋值;ArrayList提供了丰富的方法来插入、删除、替换和访问元素。

2.7 ArrayList与数组的转换

        1、集合转为数组

        使用 toArray() 方法将集合转换为数组,有两种形式:

  • Object[] toArray()
  • <T>T[] toArray(T[] a)

        其中,第二种比较常用:可以传入一个指定类型的数组,该数组的元素类型应与集合的元素类型一致,返回值则是转换后的数组,该数组会保存集合中所有的元素。

        2、数组转为集合

        Arrays类的静态方法 asList() 可以将一个数组转换为对应的 List 集合:

static <T>List<T> asList<T… a>

        返回的 List 的集合元素类型由传入的数组的元素类型决定。

        注意:对于返回的集合,不能对其增删元素,否则会抛出异常;并且对集合的元素进行修改会影响数组对应的元素。

3. LinkedList

3.1 LinkedList概述

        LinkedList是另一个常用的List接口实现类,内部维护了一个双向链表。

        LinkedList在常用API方面与ArrayList非常相似,只是在性能上有一定的差别,学习LinkedList的重点是掌握它内部的数据结构以及这种结构带来的优点和缺点。

        LinkedList集合中的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。如下图所示:

        集合中有多个元素之间的关系如下图所示:

        当插入一个新元素时,只需要修改元素之间的这种引用关系,删除一个节点也是如此。如下图所示:

        由图可以看出:LinkedList集合对于元素的增、删操作快捷方便。但是LinkedList集合不支持随机取值,每次都只能从一端或双向链表中的某节点开始遍历,直到找到查询的对象再返回,由于无法保存上一次的查询位置,因此实现查询操作的效率低下。

3.2 LinkedList的特点

        LinkedList的主要特点如下:

        1、链表结构:LinkedList使用双向链表的数据结构来存储和操作元素。

        2、动态大小:LinkedList不需要指定初始大小,并且可以根据需要自动调整容量。

        3、高效的插入和删除操作:由于LinkedList使用链表结构,插入和删除元素的性能相对较好。

        4、不支持随机访问: LinkedList不支持通过索引直接访问元素,需要从头节点或尾节点开始遍历链表。

        5、遍历操作:LinkedList实现了Iterable接口,因此可以使用迭代器(Iterator)或者增强型for循环来遍历链表中的元素。

3.3 LinkedList与ArrayList比较示例

        编写代码,测试LinkedList与ArrayList两种集合,在头部插入、中部插入和尾部插入时的效率。代码示意如下:

import java.util.ArrayList;
import java.util.LinkedList;

public class ListPerformanceComparison {
    public static void main(String[] args) {
        int iterations = 100000; // 迭代次数

        // 测试ArrayList
        ArrayList<Integer> arrayList = new ArrayList<>();
        long arrayListStartTime = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < iterations; i++) {
            arrayList.add(0, i); // 头部插入
        }
        long arrayListEndTime = System.currentTimeMillis(); // 结束时间
        long arrayListDuration = arrayListEndTime - arrayListStartTime; // 执行时间

        // 测试LinkedList
        LinkedList<Integer> linkedList = new LinkedList<>();
        long linkedListStartTime = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < iterations; i++) {
            linkedList.addFirst(i); // 头部插入
        }
        long linkedListEndTime = System.currentTimeMillis(); // 结束时间
        long linkedListDuration = linkedListEndTime - linkedListStartTime; // 执行时间

        System.out.println("ArrayList 头部插入耗时: " + arrayListDuration + " 毫秒");
        System.out.println("LinkedList 头部插入耗时: " + linkedListDuration + " 毫秒");

        // 清空列表
        arrayList.clear();
        linkedList.clear();

        // 测试ArrayList
        arrayListStartTime = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < iterations; i++) {
            arrayList.add(i / 2, i); // 中部插入
        }
        arrayListEndTime = System.currentTimeMillis(); // 结束时间
        arrayListDuration = arrayListEndTime - arrayListStartTime; // 执行时间

        // 测试LinkedList
        linkedListStartTime = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < iterations; i++) {
            int index = linkedList.size() / 2;
            linkedList.add(index, i); // 中部插入
        }
        linkedListEndTime = System.currentTimeMillis(); // 结束时间
        linkedListDuration = linkedListEndTime - linkedListStartTime; // 执行时间

        System.out.println("ArrayList 中部插入耗时: " + arrayListDuration + " 毫秒");
        System.out.println("LinkedList 中部插入耗时: " + linkedListDuration + " 毫秒");

        // 清空列表
        arrayList.clear();
        linkedList.clear();

        // 测试ArrayList
        arrayListStartTime = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < iterations; i++) {
            arrayList.add(i); // 尾部插入
        }
        arrayListEndTime = System.currentTimeMillis(); // 结束时间
        arrayListDuration = arrayListEndTime - arrayListStartTime; // 执行时间

        // 测试LinkedList
        linkedListStartTime = System.currentTimeMillis(); // 开始时间
        for (int i = 0; i < iterations; i++) {
            linkedList.add(i); // 尾部插入
        }
        linkedListEndTime = System.currentTimeMillis(); // 结束时间
        linkedListDuration = linkedListEndTime - linkedListStartTime; // 执行时间

        System.out.println("ArrayList 尾部插入耗时: " + arrayListDuration + " 毫秒");
        System.out.println("LinkedList 尾部插入耗时: " + linkedListDuration + " 毫秒");
    }
}

3.4 ArrayList和LinkedList的区别

        这是一道经典的Java基础阶段面试题,参考答案如下:

        ArrayList和LinkedList都是List接口的实现类,用于存储单列数据。ArrayList和LinkedList在数据结构、随机访问性能、插入和删除操作性能等方面有所区别。

        首先,ArrayList底层基于数组存储数据,可以看成是一个大小可变的数组。LinkedList底层基于双向链表存储数据。

        ArrayList支持使用下标(索引)随机访问集合中的元素,随机访问的性能较好。LinkedList不能根据下标直接访问元素,需要从头部或尾部开始遍历链表,随机访问性能较差。

        ArrayList在尾部进行插入和删除的性能较好,但在中间或头部进行插入和删除操作时,需要移动其他元素,性能较差。LinkedList在头尾进行插入和删除的性能较好。

        注意:以上的对比都是在数据量很大或者操作很频繁的情况下的对比,如果数据和运算量很小,上述对比将失去意义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhangyan_1010

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值