Java 集合 - 集合框架概述

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

文章目录


Java 集合框架Java Collections Framework是 Java 标准类库中的一个重要组成部分用于存储、操作和管理数据。它提供了一系列的接口、抽象类和实现类用于表示和操作不同类型的集合数据结构。

集合和数组既然都是容器它们有什么区别呢

  • 数组的长度是固定的集合的长度是可变的
  • 数组中可以存储基本数据类型值也可以存储对象而集合中只能存储对象。

1.集合框架体系结构

先来看看一个简单的集合框架类图

可见Java 集合框架定义了两个顶层的接口 Collection存储对象的集合 和 Map存储键值对也成为映射关系用于表示不同类型的集合。

2.Collection 接口

Collection 接口是 Java 集合框架的根接口它代表了一组对象的集合这些对象也称为 collection 的元素。一些 collection 允许有重复的元素而另一些则不允许。一些 collection 是有序的而另一些则是无序的。JDK 不提供此接口的任何直接实现它只负责定义常见的集合操作如添加元素、删除元素、遍历元素等。

该接口源码如下

public interface Collection<E> extends Iterable<E> {
    // Query Operations查询操作

    /**
     * 返回此集合中的元素数。
     * 说明如果此集合包含的元素多于 Integer.MAX_VALUE则返回 Integer.MAX_VALUE。
     */
    int size();

    /**
     * 返回true如果此集合不包含元素。
     */
    boolean isEmpty();

    /**
     * 返回true如果此集合包含指定的元素。
     */
    boolean contains(Object o);

    /**
     * 返回此集合中的元素上的迭代器。没有关于元素返回的顺序的保证除非此集合是某个类的实例该类提供保证。
     */
    Iterator<E> iterator();

    /**
     * 返回包含此集合中所有元素的数组。
     * 说明
     * 1.如果此集合对其元素由其迭代器返回的顺序有任何保证则此方法必须以相同的顺序返回元素。返回的数组的运行时组件类型为Object。
     * 2.返回的数组将是“安全的”因为此集合不维护对它的任何引用。换句话说即使此集合由数组支持此方法也必须分配一个新数组。
     */
    Object[] toArray();

    /**
     * 返回包含此集合中所有元素的数组返回数组的运行时类型是指定数组的类型。
     * 说明
     * 1.如果此集合适合指定的数组则返回该数组。否则将使用指定数组的运行时类型和此集合的大小分配新数组。
     * 2.如果此集合适合指定的数组并有剩余空间即数组中的元素多于此集合则紧接此集合结束的数组中的元素将设置为null。
     * 3.如果此集合对其元素由其迭代器返回的顺序有任何保证则此方法必须以相同的顺序返回元素。
     */
    <T> T[] toArray(T[] a);

    /**
     * 返回包含此集合中所有元素的数组使用提供的生成器函数来分配返回的数组。
     * 说明
     * 1.如果此集合对其元素由其迭代器返回的顺序有任何保证则此方法必须以相同的顺序返回元素。
     * 2.返回的数组的运行时组件类型是由提供的生成器函数的返回类型确定的。
     * 3.像{@link #toArray()}一样此方法充当基于数组和基于集合的API之间的桥梁。
     */
    default <T> T[] toArray(IntFunction<T[]> generator) {
        return toArray(generator.apply(0));
    }

    // Modification Operations修改操作

    /**
     * 向此集合中添加指定的元素。
     * 说明
     * 1.如果此集合因调用而更改则返回true。
     * 2.如果此集合不允许重复并且已经包含指定的元素则返回false。
     * 3.如果此集合由于其容量而拒绝添加指定的元素则抛出 IllegalStateException。
     */
    boolean add(E e);

    /**
     * 移除此集合中的单个实例指定的元素如果存在。
     * 说明
     * 1.如果此集合包含指定的元素则返回true或者等效地如果此集合因调用而更改。
     * 2.如果此集合包含多个指定的元素则仅删除第一个。
     */
    boolean remove(Object o);


    // Bulk Operations批量操作

    /**
     * 返回true如果此集合包含指定集合中的所有元素。
     */
    boolean containsAll(Collection<?> c);

    /**
     * 添加指定集合中的所有元素到此集合。即 this = this ∪ c
     */
    boolean addAll(Collection<? extends E> c);

    /**
     * 移除此集合中包含在指定集合中的所有元素。
     */
    boolean removeAll(Collection<?> c);

    /**
     * 移除此集合中满足给定谓词的所有元素。迭代或谓词抛出的错误或运行时异常被中继给调用者。
     */
    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }

    /**
     * 从此集合中移除未包含在指定集合中的所有元素。
     */
    boolean retainAll(Collection<?> c);

    /**
     * 移除此集合中的所有元素。此方法返回后集合将为空。
     */
    void clear();


    // Comparison and hashing比较和哈希

    /**
     * 比较指定的对象与此集合是否相等。
     * 说明当且仅当指定的对象也是一个集合并且两个集合中包含相同的元素时返回true。
     */
    boolean equals(Object o);

    /**
     * {@code c1.hashCode()==c2.hashCode()}.
     * 返回此集合的哈希码值。
     * 说明
     * 虽然Collection接口对Object.hashCode方法没有添加规定但是程序员应该注意
     * 任何重写Object.equals方法的类也必须重写Object.hashCode方法以满足Object.hashCode方法的一般合同。
     */
    int hashCode();

    /**
     * 创建此集合中元素的Spliterator。
     */
    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
    }

    /**
     * 该方法用于创建一个顺序流该流包含此集合中的元素。
     * 说明所谓的顺序流就是按照集合中元素的顺序依次将元素读取出来。
     */
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

    /**
     * 该方法用于创建一个并行流该流包含此集合中的元素。
     * 说明所谓的并行流就是将一个内容分成多个数据块并用不同的线程分别处理每个数据块的流。
     */
    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

2.1 Iterator

2.1.1 使用迭代器遍历集合

迭代是一种通用的获取集合元素的方式。在获取元素之前需要先判断集合中是否有元素。如果有就取出这个元素并继续判断是否还有元素。如果有就继续取出。重复这个过程直到集合中的所有元素都被取出。

想要遍历 Collection 集合那么就要获取该集合迭代器完成迭代操作在上面的源码中提供有一个 Iterator<E> iterator() 方法专门用来获取集合对应的迭代器用来遍历集合中的元素。

java.util.Iterator 接口的源码如下

public interface Iterator<E> {
    /**
     * 如果迭代器还有元素返回true
     */
    boolean hasNext();

    /**
     * 返回迭代器的下一个元素
     */
    E next();

    /**
     * 移除迭代器最后一个元素
     * 说明
     * 1. 这个方法是可选的不是所有的迭代器都支持这个方法
     * 2. 这个方法只能在next方法之后调用一次如果再次调用会抛出IllegalStateException异常
     * 3. 如果在调用next方法之前调用remove方法也会抛出IllegalStateException异常
     * 4. 如果在调用remove方法之后再次调用remove方法也会抛出IllegalStateException异常
     */
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    /**
     * 对剩余的每个元素执行给定的操作直到所有元素都被处理或操作抛出异常。
     * 说明如果指定了顺序操作将按迭代顺序执行。
     */
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

下面通过一个案例来演示迭代器的使用方式

public class IteratorTest {
    public static void main(String[] args) {
        // 创建集合
        Collection<String> collection = new ArrayList<>();

        // 添加元素
        collection.add("张三");
        collection.add("李四");
        collection.add("王五");

        // 从集合中获取迭代器
        Iterator<String> iterator = collection.iterator();
        // 遍历集合,条件为迭代器中还有元素
        while (iterator.hasNext()) {
            // 获取迭代器中的下一个元素
            String next = iterator.next();
            // 输出元素
            System.out.println(next);
        }
    }
}

程序执行结果如下

张三
李四
王五

其底层迭代原理是通过一个指针或称游标来指向当前遍历到的元素每次调用 next() 方法时指针向后移动一位并返回当前指向的元素。当指针遍历到集合末尾时hasNext() 方法返回 false表示遍历结束。

例如ArrayList 的迭代器实现中指针指向当前元素的下标每次调用 next() 方法时返回当前下标对应的元素并将下标加一。而 LinkedList 的迭代器实现中则需要保存一个指向当前节点的引用每次调用 next() 方法时返回当前节点的值并将指针指向下一个节点。

2.1.2 使用迭代器删除集合元素

java.util.Iterator 中还提供了一个 void remove() 方法既然 Collection 已经有 remove(Object o) 方法了为什么 Iterator 迭代器还要提供删除方法呢这是因为因为在 JDK1.8 之前 Collection 接口没有 removeIf(Predicate<? super E> filter) 方法即无法根据条件进行删除。

例如要删除以下集合元素中的偶数

public class IteratorTest02 {
    public static void main(String[] args) {
        // 创建集合
        Collection<Integer> collection = new ArrayList<>();

        // 添加元素
        collection.add(1);
        collection.add(2);
        collection.add(3);
        collection.add(4);

        // collection.remove(?) JDK 1.8 以前没有 removeIf 方法,没办法删除集合中的 “偶数” 元素

        // 方式一通过迭代器删除集合中的偶数元素
        Iterator<Integer> iterator = collection.iterator();
        while (iterator.hasNext()) {
            // 获取迭代器中的下一个元素
            Integer next = iterator.next();
            // 判断元素是否为偶数
            if (next % 2 == 0) {
                // 删除偶数元素
                iterator.remove();
            }
        }
        
        // 方式二通过增强for循环删除集合中的偶数元素(底层还是迭代器)
        for (Integer integer : collection) {
            if (integer % 2 == 0) {
                collection.remove(integer);
            }
        }
        
        // 方式三通过 removeIf 方法删除集合中的偶数元素
        collection.removeIf(integer -> integer % 2 == 0);
    }
}

2.1.3 Iterator 迭代器的 fail-fast 机制

“fail-fast” 是 Java 集合框架中迭代器的一个特性。它主要指的是当多个线程同时修改一个集合时如果一个线程正在通过迭代器遍历集合而另一个线程修改了集合如添加、删除或修改元素那么正在遍历的线程会立即抛出 ConcurrentModificationException 异常。

这种机制的设计原因是为了防止在遍历过程中由于集合的结构发生改变导致结果的不可预见性。对于 fail-fast 系统如果有错误发生系统会立即对错误进行响应而不是尝试在错误的状态下继续运行。这有助于避免在运行时发生更严重的错误也更容易找出问题的原因。

要注意的是fail-fast 机制并不能保证在所有情况下都能发现并防止并发修改它只能在迭代过程中尽可能地检测出错误。另外Java 的并发包 java.util.concurrent 中的一些集合类如 CopyOnWriteArrayListConcurrentHashMap则提供了 fail-safe 的迭代器这种迭代器在遍历时不会抛出 ConcurrentModificationException 异常因为它们在迭代时工作在集合的一个副本上而不是直接在集合上。

下面基于 ArrayList 分析实现原理可暂时跳过有基础的可以直接继续阅读。

迭代器如何实现快速失败机制以 ArrayList 为例它继承了一个变量 protected transient int modCount = 0; 用于记录集合的结构修改次数。当进行新增、删除等操作时modCount++ 将记录数量加1。在使用 Iterator 迭代器遍历集合时创建集合迭代器的对象时记录当前集合的 modCount例如int expectedModCount = modCount;。之后每次使用迭代器的 next() 方法迭代元素时都要检查 expectedModCount 是否等于当前的 modCount如果不相等则说明集合结构被修改了可能是在使用迭代器的同时使用了集合的 add、remove 等方法导致 modCount++最终会抛出 ConcurrentModificationException 异常。

下面来看一看 ArrayList 中的 Itr 迭代器的具体实现

/**
* 返回一个迭代器按照正确的顺序遍历此列表中的元素。
*/
public Iterator<E> iterator() {
return new Itr();
}

/**
* 一个优化版本的AbstractList.Itr
*/
private class Itr implements Iterator<E> {
// 下一个元素的索引
int cursor;
// 最后一个元素的索引如果没有则为-1
int lastRet = -1;
// 期望的修改次数modCount 继承自 AbstractList,创建迭代器时记录
int expectedModCount = modCount;

// prevent creating a synthetic constructor
Itr() {
}

// 判断是否有下一个元素
public boolean hasNext() {
    // cursor < size 表示还有元素
    return cursor != size;
}

// 获取下一个元素
@SuppressWarnings("unchecked")
public E next() {
    // 检查是否有并发修改即modCount是否等于expectedModCount
    checkForComodification();
    // 获取下一个元素的索引
    int i = cursor;
    // 如果下一个元素的索引大于等于元素的个数抛出异常(因为没有元素了)
    if (i >= size)
        throw new NoSuchElementException();
    // 获取ArrayList的数组
    Object[] elementData = ArrayList.this.elementData;
    // 如果下一个元素的索引大于等于数组的长度抛出异常(因为容量不够了)
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    // 将下一个元素的索引加1
    cursor = i + 1;
    // 返回下一个元素其实就是elementData[i]
    return (E) elementData[lastRet = i];
}

::: warning

迭代器的快速失败行为不能得到保证。通常在存在不同步的并发修改时不可能对其进行坚决的保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException 异常。因此将依赖于此异常的程序编写是错误的。正确的方法是使用迭代器的快速失败行为来检测 bug而不应将其用于其他用途。

:::

2.2 Iterable

通过该接口的声名我们也看到了 Collection 接口继承了 java.lang.Iterable 接口因此它的实现类可以使用 for-each 循环进行遍历。

该接口源码如下

/**
 * {@code for} statement (sometimes called the "for-each loop" statement).
 * 实现此接口允许对象成为增强的{@code for}语句有时称为“for-each循环”语句的目标。
 */
public interface Iterable<T> {
    /**
     * 返回一个迭代器用于遍历此集合中的元素。
     */
    Iterator<T> iterator();

    /**
     * 对于{@code Iterable}的每个元素执行给定的操作直到处理完所有元素或操作抛出异常。
     *
     * @param action 要对每个元素执行的操作
     * @throws NullPointerException 如果指定的操作为 null
     */
    default void forEach(Consumer<? super T> action) {
        // 进行非空判断
        Objects.requireNonNull(action);
        // 遍历集合
        for (T t : this) {
            // 对每个元素执行给定的操作
            action.accept(t);
        }
    }

    /**
     * 创建一个{@link Spliterator}用于遍历此{@code Iterable}描述的元素。
     * 说明
     * 1. Spliterator是Java 8中新增的接口它是支持并行遍历的迭代器。
     * 2. Spliterator是splitable iterator的简称它是可分割迭代器。
     */
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

可见其中的 Iterator<T> iterator() 方法就是上一节中所讲的内容从源码中也不难看出其实 for-each 循环底层其实就是使用 Iterator 迭代器来完成元素的遍历的。

下面是一个简单案例

public class IterableTest {
    public static void main(String[] args) {
        // 创建集合
        Collection<String> collection = new ArrayList<>();

        // 添加元素
        collection.add("张三");
        collection.add("李四");
        collection.add("王五");
        
        // 使用增强for循环遍历集合
        for (String s : collection) {
            System.out.println(s);
        }
    }
}

通过查看编译后的 class 文件我们便能证实上述结论

2.3 List 集合

List 接口继承自 Collection 接口它表示一个有序的集合可以包含重复的元素List 接口定义了一些与索引相关的操作如按索引访问元素、按索引插入元素、按索引删除元素等。

下面是一个简单的总结表格

特性ArrayListVectorLinkedListStack
元素的顺序插入顺序插入顺序插入顺序LIFO先进后出
允许 null
线程安全
性能中等中等
基于动态数组动态数组双链表Vector
特殊功能双端操作出栈入栈

2.4 Set 集合

Set 接口继承自 Collection 接口它表示一个不允许包含重复元素的集合。Set 接口定义了一些与集合操作相关的方法如判断元素是否存在、添加元素、删除元素等。

下面是一个简单的总结表格

特性HashSetLinkedHashSetTreeSet
元素的顺序无序插入顺序有序
允许 null
性能中等较低
基于HashMapLinkedHashMapTreeMap
特殊功能记录插入顺序排序

2.5 Queue

Queue 接口是 Java 中表示队列的一种抽象数据类型它遵循**先进先出FIFO**的原则提供了在队尾插入元素、在队头删除元素以及查看队头元素等操作常用于实现消息传递、任务调度等场景。

下面是一个简单的总结表格

特性 / 类型LinkedListArrayDequePriorityQueue
底层数据结构双向链表动态数组环形堆Heap
顺序FIFOFirst-In-First-OutFIFOFirst-In-First-Out按照自然排序或比较器排序
允许null元素
多线程安全
主要功能队列操作、栈操作、列表操作队列操作、栈操作优先级队列操作

3.Map 集合

Map 接口表示一组键值对的集合每个键对应一个值。Map 接口中的键是唯一的值可以重复Map 接口定义了一些与键值对操作相关的方法如根据键获取值、添加键值对、删除键值对等。

下面是一个简单的总结表格

特性HashMapHashtableLinkedHashMapTreeMapProperties
Key 的顺序无序无序插入顺序或访问顺序有序自然排序、定制排序无序
允许 null
线程安全
性能较高中等
基于Hash表Hash表Hash表+链表红黑树Hash表
特殊功能记录插入顺序排序存储字符串
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java