Java集合框架笔记--List系列集合基本使用方法 ArrayList,LinkedList和迭代器底层源码

Java集合框架笔记--List系列集合基本使用方法,ArrayList,LinkedList和迭代器底层源码

集合体系架构

Collection接口

  • 单列集合的顶层接口

  • 所有方法被List和Set系列集合共享

List系列集合

  • 特点:有序(存和取的顺序)、可重复、有索引

  • 实现类

    • ArrayList

    • LinkedList

    • Vector

Set系列集合

  • 特点:无序(存和取的顺序)、不重复、无索引

  • 实现类

    • HashSet → LinkedHashSet

    • TreeSet

Map

  • 双列集合

Collection常用方法

方法描述
boolean add(E e)添加元素
void clear()清空集合
boolean remove(Object o)删除指定元素
boolean contains(Object o)判断是否包含指定元素
boolean isEmpty()判断集合是否为空
int size()获取集合长度
Collection<String> list = new ArrayList<>();//多态
list.add("aaa");
list.add("bbb");
list.add("ccc");
​
System.out.println(list); // [aaa, bbb, ccc]
list.clear(); // 清空集合
​
// 删除元素
System.out.println(list.remove("aaa")); // true
System.out.println(list); // [bbb, ccc]
​
// 判断元素是否包含
boolean result = list.contains("bbb");
System.out.println(result); // true
​
// 判断集合是否为空
System.out.println(list.isEmpty()); // false
​
// 获取集合长度
System.out.println(list.size()); // 2

注意

  • Collection是一个接口,不能直接创建对象

  • 使用多态创建对象:Collection<String> list = new ArrayList<>();

  • 添加元素细节:

    • List系列集合:方法永远返回true,因为允许重复

    • Set系列集合:

      • 如果添加元素不存在,返回true,表示添加成功

      • 如果添加元素已存在,返回false,表示添加失败

集合遍历方式

1. 迭代器遍历

Collection<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
​
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String element = iterator.next();
    System.out.println(element);
}

迭代器常用方法

  • boolean hasNext():判断当前位置是否有元素

  • E next():获取当前位置的元素,并将迭代器移动到下一个位置

注意事项

  1. NoSuchElementException:迭代器没有更多元素时调用next()方法会抛出此异常

  2. 迭代器遍历完毕后,指针不会复位,需要重新获取迭代器

  3. 循环中只能使用一次next()方法,多次使用需用变量保存返回值

  4. 迭代器遍历时,不能用集合的方法进行增删元素,否则会抛出ConcurrentModificationException

2. 增强for循环

Collection<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
​
for (String s : list) {
    System.out.println(s);
}

特点

  • 底层就是Iterator迭代器

  • 所有单列集合和数组都可以使用

  • 修改循环变量不会改变集合中原本的数据

3. Lambda表达式遍历

Collection<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
​
// 匿名内部类方式
list.forEach(new Consumer<String>() {
    @Override
    public void accept(String string) {
        System.out.println(string);
    }
});
​
// Lambda表达式方式
list.forEach(string -> System.out.println(string));

List系列集合独有方法

方法描述
void add(int index, E element)在指定位置添加元素
E remove(int index)删除指定索引位置的元素,并返回被删除的元素
E get(int index)获取指定索引位置的元素
E set(int index, E element)修改指定索引位置的元素,并返回被修改的元素
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
list.add("java");
​
// add方法
list.add(1, "world111");
System.out.println(list); // [hello, world111, world, java]
​
// remove方法
String remove = list.remove(1);
System.out.println(remove); // world111
​
// get方法
String get = list.get(1);
System.out.println(get); // world
​
// set方法
String set = list.set(1, "world222");
System.out.println(set); // world
System.out.println(list); // [hello, world222, java]

注意

  • List系列集合有两个删除方法:

    1. 直接删除元素:remove(Object o)

    2. 通过索引删除:remove(int index)

  • 方法重载时,优先调用实参与形参类型一致的方法

列表迭代器遍历

还有一个普通的for循环遍历,加上迭代器,增强for循环,lambda一共是五种遍历方式。

List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
list.add("java");
​
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
    String next = listIterator.next();
    System.out.println(next);
}
​
// 额外方法
// listIterator.add():在遍历过程中添加元素
// previous():返回列表中的前一个元素
// hasPrevious():判断列表中是否有前一个元素

ArrayList集合底层原理

核心特点

  1. 底层是数组结构

  2. 利用空参创建的集合,在底层创建一个默认长度为0的数组

  3. 添加第一个元素时,底层创建一个长度为10的数组

  4. 存满时,会扩容1.5倍

  5. 如果一次添加多个元素,1.5倍还放不下,则新创建数组的长度以实际为准

源码分析

//1.利用空参创建的集合 elementData   DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {} size = 0
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
​
        //2.添加元素 调用add
        public boolean add(E e) {
            modCount++;
​
            add(e, elementData, size);//要添加的元素,数组名字,当前集合的长度/现在元素应存入的位置,
//            //size代表下一个新元素应该被放入的位置(索引)
            return true;
        }
​
        //3.调用add(e, elementData, size);
        //elementData.length是容量,size表示当前元素数量 判断数组容量和当前元素数量是否相等,满足扩容条件
        private void add(E e, Object[] elementData, int s) {
            if (s == elementData.length)                
                elementData = grow();
            elementData[s] = e;
            size = s + 1;
        }
​
        //4.调用grow()扩容
        private Object[] grow() {
            return grow(size + 1);  //size + 1 代表的是添加一个新元素所需的最小容量。
            // grow() 方法需要确保扩容后的数组至少能容纳 size + 1 个元素,这是它进行扩容计算的目标和依据
​
        }
​
        //5.调用grow(size + 1);
        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           /*默认新增的容量大小  /2  0.5  */);
                return elementData = Arrays.copyOf(elementData, newCapacity);
            } else {
                 //DEFAULT_CAPACITY = 10;
                return elementData = new Object[Math.max(DEFAULT_CAPACITY /* 10 */, minCapacity)];//        第一个加入的数据小于10,则创建一个长度为10的数组
            }
        }
        //6.newLength()
        public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
            //新数组真正的长度
            int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
​
            if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {  /*SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8*/
                return prefLength; //返回新数组的长度
            } else {
                return hugeLength(oldLength, minGrowth);//处理极端情况下的容量计算,在可能的情况下尽量满足容量需求,在无法满足时提供清晰的错误信息(OutOfMemoryError)
            }
        }  
        //7.Arrays.copyOf();
        //elementData = Arrays.copyOf(elementData, newCapacity);
        //1.根据新容量创建新的数组,2.把原数组的元素复制到新数组中,3.把新数组赋给elementData,并返回新数组
​
        //8.回到add(e, elementData, size);
        //elementData = 返回的新数组
        //执行elementData[s] = e; size = s + 1;
​
        //9.回到add() return true;

关键点

  • SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8,为JVM数组头信息预留空间

  • hugeLength()方法处理极端情况下的容量计算,确保不会创建超过JVM支持的数组大小

  • 第一次添加元素时,会创建一个长度为10的数组(如果所需容量小于10)

集合中的"空位置"问题

  • ArrayList:底层是数组,但空位置只存在于数组末尾,迭代器只会遍历到size-1

  • LinkedList:每个元素都是节点,节点之间通过指针连接,不存在空位置

  • HashSet/TreeSet:基于哈希表或树,可能有空桶,但迭代器会跳过这些空桶

  • 集合允许存储null值,但这不同于"空位置"

  • 迭代器设计为只遍历集合中实际存在的元素,不会访问任何"空位置"

  • 当从集合中删除元素时,集合会调整其内部结构,确保不会留下"空位"

并发修改异常(ConcurrentModificationException)

原因

迭代器的"快速失败(fail-fast)"机制在单线程内检测到了意料之外的修改

机制原理

  1. 创建迭代器时,迭代器内部记录当前集合的修改次数(modCount)

  2. 集合本身也有一个变量记录修改次数

  3. 每次对集合进行结构性修改时,modCount值增加

  4. 迭代器在每次操作前检查modCount是否与记录值一致

  5. 如果不一致,抛出ConcurrentModificationException

解决方案

  • 在迭代过程中,如果要删除元素,必须使用迭代器自身的remove()方法

  • 使用Java 8+的removeIf()方法

  • 在多线程环境下,使用并发集合类如CopyOnWriteArrayList

LinkedList 集合

特点

  • 底层数据结构是双向链表

  • 查询慢,增删快

  • 如果操作的是首尾元素,速度极快

  • 提供了很多直接操作首尾元素的特有 API

特有 API 方法

方法描述
void addFirst(E e)在列表开头添加一个元素
void addLast(E e)将指定元素追加到列表末尾
E getFirst()返回列表的第一个元素
E getLast()返回列表的最后一个元素
E removeFirst()删除并返回列表的第一个元素
E removeLast()删除并返回列表的末尾元素

LinkedList 底层原理

1. 创建空链表(无参构造)
// LinkedList 成员变量
transient int size = 0;          // 链表大小
transient Node<E> first;         // 头节点
transient Node<E> last;          // 尾节点
​
// 无参构造函数
public LinkedList() {
    // 初始化空链表
}
2. 添加元素
//1.创建一个空链表,无参构造
        /**
         * transient int size = 0;
         * transient LinkedList.Node<E> first;
         * transient LinkedList.Node<E> last;
         */
         public LinkedList() {
         }
​
        //2.添加元素 调用add(E e);
        public boolean add(E e) {
            linkLast(e);
            return true;
        }
        //3.调用linkLast(E e);
        //尾插法
        void linkLast(E e) {
            final LinkedList.Node<E> l = last;
            final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null); //创建一个新节点
            last = newNode;  //链表的头节点中的last 指向新节点
            if (l == null)
                first = newNode;  //链表为空,新节点作为头节点
            else
                l.next = newNode; //链表不为空,新节点作为尾节点
            size++;   //链表的长度加1
            modCount++; //链表结构改变次数加1(防止多线程篡改)
        }
        //4.add(E e); return true
3. 节点结构
// 双向链表节点定义
private static class Node<E> {
    E item;         // 存储的元素
    Node<E> next;   // 指向下一个节点
    Node<E> prev;   // 指向上一个节点
​
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

迭代器底层原理

迭代器创建

// 获取迭代器
public Iterator<E> iterator() {
    return new Itr();  // 创建内部类Itr的实例
}
​
// 每次调用iterator()方法都会创建一个新的迭代器对象

迭代器内部类实现

// 迭代器内部类
private class Itr implements Iterator<E> {
    int cursor;       // 光标,表示迭代器指针,默认指向0索引
    int lastRet = -1; // 上一次操作的索引
    int expectedModCount = modCount; // 记录创建迭代器时的修改次数
    
    // 构造函数
    Itr() {
    }
    
    // 判断是否还有元素
    public boolean hasNext() {
        return cursor != size;  // 光标不等于集合大小表示还有元素
    }
    
    // 获取下一个元素
    public E next() {
        checkForComodification();  // 检查是否发生并发修改
        int i = cursor;            // 记录当前指针位置
        
        if (i >= size) {
            throw new NoSuchElementException();
        }
        
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        
        cursor = i + 1;           // 移动指针到下一个位置
        return (E) elementData[lastRet = i];  // 返回当前元素并记录上一次操作索引
    }
    
    // 检查并发修改
    final void checkForComodification() {
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    
    // 删除当前元素
    public void remove() {
        if (lastRet < 0) {
            throw new IllegalStateException();
        }
        
        checkForComodification();
        
        try {
            ArrayList.this.remove(lastRet);  // 调用集合的remove方法
            cursor = lastRet;                // 调整光标位置
            lastRet = -1;                    // 重置上一次操作索引
            expectedModCount = modCount;     // 同步修改次数
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
}

关键机制解释

1. 快速失败 (Fail-Fast) 机制
// 检查并发修改的核心方法
final void checkForComodification() {
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
  • modCount:集合实际的修改次数,每次结构性修改都会增加

  • expectedModCount:迭代器期望的修改次数,在迭代器创建时记录

  • 当两者不相等时,说明集合在迭代过程中被意外修改,抛出异常

2. 迭代器删除操作的特殊处理

迭代器的 remove() 方法做了特殊处理:

  1. 调用集合的 remove() 方法删除元素

  2. 调整光标位置 (cursor = lastRet)

  3. 重置上一次操作索引 (lastRet = -1)

  4. 同步修改次数 (expectedModCount = modCount)

这一步同步操作是关键,它确保了迭代器知道集合已经被修改,并且更新了自己的期望值,从而避免了 ConcurrentModificationException

总结对比

LinkedList vs ArrayList

特性LinkedListArrayList
底层结构双向链表动态数组
查询效率慢 (O(n))快 (O(1))
增删效率快 (O(1))慢 (O(n))
内存占用较高 (每个元素需要额外指针)较低 (连续内存)
适用场景频繁增删操作频繁查询操作

迭代器使用注意事项

  1. 并发修改异常:迭代过程中不要直接使用集合的增删方法

  2. 多次获取迭代器:每次调用 iterator() 都会返回一个新的迭代器对象

  3. 迭代器删除:使用迭代器自身的 remove() 方法可以安全删除元素

  4. 单向遍历:基本迭代器只能向前遍历,ListIterator 支持双向遍历

  5. 快速失败机制:这是为了保护集合在迭代过程中的一致性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值