集合

1. Java集合框架

1.1 集合接口与实现分离

为了实现集合接口与具体的集合类的分离,Java 提供了一套接口和实现类之间的体系结构。这样做的好处是,开发者可以根据自己的需求选择合适的集合实现,同时可以在代码中使用统一的集合接口进行操作,从而使得代码更加灵活和可扩展。

1.2 Collection 接口

Collection 接口是集合框架的根接口,它继承自 java.util.Iterable 接口。Collection 接口定义了一组通用的集合操作方法,用于存储和操作一组对象(元素)。它是所有集合类的基本接口,包含了一些基本的集合操作,例如添加、删除、查询元素等。

Collection 接口中的常用方法包括:

  1. 添加元素
    • boolean add(E e): 将指定的元素 e 添加到集合中。如果添加成功,则返回 true,如果集合已经包含该元素,则返回 false。
    • boolean addAll(Collection<? extends E> c): 将另一个集合 c 中的所有元素添加到当前集合中。如果当前集合由于添加了新元素而改变,则返回 true。
  2. 删除元素
    • boolean remove(Object o): 从集合中删除指定的元素 o。如果删除成功,则返回 true,否则返回 false。
    • boolean removeAll(Collection<?> c): 删除当前集合中与给定集合 c 中相同的元素。如果当前集合由于删除而改变,则返回 true。
    • void clear(): 清空集合,将集合中的所有元素移除。
  3. 查询元素
    • boolean contains(Object o): 判断集合中是否包含指定的元素 o。如果集合包含该元素,则返回 true,否则返回 false。
    • boolean containsAll(Collection<?> c): 判断当前集合是否包含另一个集合 c 中的所有元素。如果所有元素都包含,则返回 true,否则返回 false。
    • int size(): 返回集合中的元素个数。
  4. 集合转换
    • Object[] toArray(): 将集合转换为数组。
    • <T> T[] toArray(T[] a): 将集合转换为指定类型的数组。

Collection 接口有两个主要的子接口,即 List 和 Set 接口,它们分别代表有序集合和无序集合。List 接口是有序的集合,可以包含重复元素,而 Set 接口是不允许包含重复元素的集合。

注意:Collection 接口中的方法都是基于对象的引用操作,当使用集合类存储基本数据类型时,会自动进行装箱和拆箱操作。

1.3 迭代器

迭代器(Iterator)是用于遍历集合(Collection)中元素的对象。它是集合框架的一部分,提供了一种统一的方式来访问集合中的元素,而无需了解集合的内部结构。通过迭代器,我们可以按顺序逐个访问集合中的元素,而不需要知道集合的底层实现细节。

集合框架中的所有集合类都实现了一个名为 iterator() 的方法,该方法返回一个对应集合的迭代器对象。迭代器对象可以通过调用它的方法来遍历集合中的元素,常用的方法有:

  1. boolean hasNext(): 判断集合中是否还有下一个元素。如果有下一个元素,则返回 true,否则返回 false。
  2. E next(): 返回集合中的下一个元素,并将迭代器的指针移动到下一个位置。如果没有下一个元素,则抛出 NoSuchElementException 异常。
  3. void remove(): 从集合中删除通过迭代器返回的最后一个元素。这个方法是可选的,不是所有集合都支持。如果在调用 next() 方法之前没有调用过 remove() 方法,或者在上一次调用 next() 方法之后已经调用了 remove() 方法,再次调用 remove() 方法会抛出 IllegalStateException 异常。

迭代器的使用示例:

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

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Orange");

        // 获取集合的迭代器
        Iterator<String> iterator = list.iterator();

        // 使用迭代器遍历集合
        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);
        }
    }
}

在上面的示例中,我们通过调用 list.iterator() 获取了 ArrayList 集合的迭代器对象。然后使用 while 循环和 hasNext()next() 方法遍历集合并打印出元素。

通过迭代器可以避免直接使用索引来遍历集合,从而提高了代码的灵活性和可读性。同时,迭代器还可以在遍历过程中动态地删除集合中的元素,而不会引发异常。

1.4 泛型实用方法

在 Java 集合中,泛型提供了一种类型安全的方式来操作集合,可以让我们在编译时就能检测到类型不匹配的错误。除了基本的添加、删除、查找等操作,还有一些实用的泛型方法可以帮助我们更方便地使用集合。以下是一些常见的 Java 集合泛型实用方法:

  1. addAll(Collection< ? extends E> c):将另一个集合 c 中的所有元素添加到当前集合中。其中,E 是当前集合的泛型类型的上界,表示集合中的元素必须是 E 类型或其子类。
  2. removeIf(Predicate< ? super E> filter):根据指定的条件(Predicate)删除集合中满足条件的元素。其中,E 是集合的泛型类型,super E 表示 Predicate 的泛型类型必须是 E 类型或其父类。
  3. retainAll(Collection< ? > c):保留当前集合与另一个集合 c 的交集,删除当前集合中不在 c 中的元素。此方法用于集合的交集操作。
  4. replaceAll(UnaryOperator< E > operator):对集合中的每个元素都应用指定的一元运算符(UnaryOperator),并用运算后的结果替换原始元素。
  5. sort(Comparator< ? super E> c):根据指定的比较器(Comparator)对集合进行排序。其中,E 是集合的泛型类型,super E 表示 Comparator 的泛型类型必须是 E 类型或其父类。
  6. toArray(IntFunction<T[]> generator):将集合转换为数组。其中,IntFunction<T[]> generator 是一个函数接口,用于创建一个指定类型的数组。
  7. stream():将集合转换为 Stream 对象,可以用于进行更加灵活的集合操作。

2. 集合框架中的接口

Java 集合框架中的接口是一组定义了特定集合类型的规范,用于表示集合的行为和特征,而不涉及具体的实现细节。接口定义了一组抽象方法,这些方法描述了集合的基本操作,如添加元素、删除元素、遍历元素等。

Java 集合框架中的接口提供了一种标准化的方式来处理集合数据,使得不同类型的集合可以共用相同的方法名称和语义,从而方便开发者在不同的场景中使用集合,而无需关心具体的实现细节。

以下是 Java 集合框架中的主要接口及其概念:

  1. Iterable 接口:它是所有集合的父接口,定义了一个抽象方法 iterator(),用于获取集合的迭代器。实现了 Iterable 接口的集合可以使用 for-each 循环来遍历集合中的元素。
  2. Collection 接口:它是所有单值集合的父接口,定义了一组通用的集合操作方法,如添加元素、删除元素、查询元素等。Collection 接口派生出 List、Set 和 Queue 接口。
  3. List 接口:它是有序的集合,可以包含重复的元素。List 接口的实现类按照元素的插入顺序进行排序。
  4. Set 接口:它是不允许包含重复元素的集合。Set 接口的实现类中不能包含相同的元素。
  5. Queue 接口:它是一种特殊的集合,表示一组元素按照特定顺序进行排列,并且可以通过添加和删除操作在集合的两端进行操作。Queue 接口通常用于实现队列和优先队列等数据结构。
  6. Map 接口:它表示一组键值对的映射关系,每个键对应一个值。Map 接口的实现类可以根据键快速查找对应的值,常用于实现字典和哈希表等数据结构。

通过使用接口,Java 集合框架实现了接口与实现的分离,使得开发者可以根据具体需求选择合适的集合实现类,从而实现灵活、高效的集合操作。

3. 具体集合

Java 集合框架提供了许多具体集合类,每个类都实现了相应的集合接口。以下是 Java 集合框架中一些常见的具体集合类及其实现:

  1. List 接口的实现类

    • ArrayList:动态数组,支持快速随机访问,基于数组实现。
    • LinkedList:双向链表,支持快速插入和删除,但随机访问较慢。

    总的来说,ArrayList 适用于对访问操作较多的场景,而 LinkedList 适用于对插入和删除操作较多的场景。

  2. Set 接口的实现类

    • HashSet:基于哈希表的 Set,不允许包含重复元素。
    • TreeSet:基于红黑树的有序 Set,按照元素的自然顺序或者指定的比较器进行排序。
  3. Queue 接口的实现类

    • LinkedList:双向链表,可用作队列或双端队列。
    • PriorityQueue:优先队列,按照优先级对元素进行排序。
  4. Deque 接口的实现类

    • ArrayDeque:动态数组,可用作队列或双端队列。
    • LinkedList:双向链表,可用作队列或双端队列。
  5. Map 接口的实现类

    • HashMap:基于哈希表的 Map,存储键值对。
    • TreeMap:基于红黑树的有序 Map,按照键的自然顺序或者指定的比较器进行排序。
    • LinkedHashMap:基于哈希表和双向链表的 Map,按照插入顺序或访问顺序进行排序。
  6. SortedMap 接口的实现类

    • TreeMap:基于红黑树的有序 Map,按照键的自然顺序或者指定的比较器进行排序。
  7. NavigableMap 接口的实现类

    • TreeMap:基于红黑树的有序 Map,按照键的自然顺序或者指定的比较器进行排序。

除了上述列举的具体集合类,Java 集合框架还提供了一些其他的实现类,如 Stack(堆栈)、Vector(向量)等。每个类都有其特定的用途和特性,开发者可以根据具体需求选择合适的具体集合类,以便更好地处理集合数据。

3.1 链表(LinkedList)

链表是一种常见的数据结构,用于存储一组元素,并且每个元素都包含对前一个元素和后一个元素的引用。链表不像数组那样在内存中是连续存储的,而是通过节点(Node)之间的引用连接起来的。

Java 集合框架中有两种常见的链表实现:单向链表(Singly LinkedList)和双向链表(Doubly LinkedList)。

  • 单向链表(Singly LinkedList): 单向链表中的每个节点包含两部分:数据部分和指针部分。数据部分存储当前节点的值,指针部分存储对下一个节点的引用。最后一个节点的指针部分通常指向空值(null),表示链表的末尾。

    在 Java 中,单向链表可以通过自定义节点类来实现,例如:

    class ListNode {
        int val;
        ListNode next;
        ListNode(int val) {
            this.val = val;
            this.next = null;
        }
    }
    
  • 双向链表(Doubly LinkedList): 双向链表中的每个节点包含三部分:数据部分、指向前一个节点的指针和指向后一个节点的指针。双向链表可以从前向后遍历,也可以从后向前遍历,因为每个节点都有对前一个节点和后一个节点的引用。

    在 Java 中,双向链表也可以通过自定义节点类来实现,例如:

    class DoublyListNode {
        int val;
        DoublyListNode prev;
        DoublyListNode next;
        DoublyListNode(int val) {
            this.val = val;
            this.prev = null;
            this.next = null;
        }
    }
    

链表在插入和删除操作上比较灵活高效,因为它只需要调整节点的引用,而不需要像数组那样搬移大量元素。但是,链表的随机访问效率相对较低,因为需要从头节点开始顺序遍历到目标节点。在 Java 中,LinkedList 就是双向链表的实现,它实现了 List 接口,可以用于存储一组有序的元素,并提供了插入、删除、查找等方法。

从链表中删除一个元素

从链表中删除一个元素可以分解为以下几个步骤:

  1. 找到待删除元素的前一个节点:由于链表是单向或双向的,我们需要找到待删除元素的前一个节点,以便在删除操作时修改指针的引用。
  2. 修改指针引用:在找到待删除元素的前一个节点后,我们需要修改其指针引用,将其指向待删除元素的后一个节点,从而跳过待删除元素。
  3. 释放待删除元素:在删除元素后,需要将待删除元素从内存中释放,以防止内存泄漏。

以下是一个示例代码,演示如何从单向链表中删除一个元素:

class ListNode {
    int val;
    ListNode next;

    ListNode(int val) {
        this.val = val;
        this.next = null;
    }
}

public class LinkedListExample {

    public static ListNode deleteNode(ListNode head, int value) {
        // 处理链表为空的情况
        if (head == null) {
            return null;
        }

        // 如果要删除的节点是头节点,则直接返回头节点的下一个节点
        if (head.val == value) {
            return head.next;
        }

        ListNode current = head;
        while (current.next != null) {
            // 找到待删除节点的前一个节点
            if (current.next.val == value) {
                // 修改指针引用,跳过待删除节点
                current.next = current.next.next;
                // 释放待删除节点,可以省略此步骤,由 Java 的垃圾回收机制自动回收
                current.next = null;
                break;
            }
            current = current.next;
        }

        return head;
    }

    public static void main(String[] args) {
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(3);
        head.next.next.next = new ListNode(4);

        // 删除节点值为 3 的节点
        head = deleteNode(head, 3);

        // 遍历链表,验证是否删除成功
        ListNode current = head;
        while (current != null) {
            System.out.print(current.val + " ");
            current = current.next;
        }
    }
}

在上述示例中,我们定义了一个 ListNode 类表示链表节点,然后实现了一个 deleteNode() 方法用于从链表中删除指定值的节点。在 main() 方法中,我们创建了一个单向链表,删除值为 3 的节点,并遍历链表输出结果。输出为 1 2 4,说明成功删除了值为 3 的节点

将一个元素添加到链表中

链表的 ListIterator 是用于遍历链表并进行插入、修改和删除操作的特殊迭代器。ListIterator 是 Iterator 接口的子接口,它在 Iterator 接口的基础上增加了一些额外的功能,使得在遍历链表时更加灵活方便。

ListIterator 提供了以下常用方法:

  1. hasNext():判断是否有下一个元素可以遍历。
  2. next():获取下一个元素,并将迭代器指针向后移动。
  3. hasPrevious():判断是否有上一个元素可以遍历。
  4. previous():获取上一个元素,并将迭代器指针向前移动。
  5. add(E e):在迭代器当前位置插入一个新元素。
  6. set(E e):将迭代器最近返回的元素替换为指定元素。
  7. remove():从链表中移除迭代器最近返回的元素。

使用 ListIterator 遍历链表的示例代码如下:

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

public class LinkedListExample {

    public static void main(String[] args) {
        List<String> fruits = new LinkedList<>();
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Orange");

        // 创建 ListIterator
        ListIterator<String> iterator = fruits.listIterator();

        // 正向遍历链表
        System.out.println("正向遍历链表:");
        while (iterator.hasNext()) {
            String fruit = iterator.next();
            System.out.println(fruit);

            // 在遍历到 "Banana" 时插入新元素 "Grapes"
            if (fruit.equals("Banana")) {
                iterator.add("Grapes");
            }
        }

        // 反向遍历链表
        System.out.println("反向遍历链表:");
        while (iterator.hasPrevious()) {
            String fruit = iterator.previous();
            System.out.println(fruit);

            // 在遍历到 "Banana" 时移除元素
            if (fruit.equals("Banana")) {
                iterator.remove();
            }
        }

        // 输出链表中的元素
        System.out.println("链表中的元素:");
        for (String fruit : fruits) {
            System.out.println(fruit);
        }
    }
}

上述代码创建了一个 LinkedList 链表,并使用 ListIterator 进行正向和反向遍历,同时在遍历的过程中进行插入和删除操作。输出结果如下:

正向遍历链表:
Apple
Banana
Grapes
Orange
反向遍历链表:
Orange
Grapes
Apple
链表中的元素:
Apple
Grapes
Orange

3.2 数组列表(ArrayList)

数组列表(ArrayList)是一种常用的数据结构,它实现了 List 接口,并且基于数组来存储元素。数组列表具有动态调整大小的能力,可以根据需要自动扩容或缩减容量,因此称为动态数组。

以下是一些关键特点和操作:

  1. 动态大小:数组列表可以根据需要动态调整大小,可以随时添加或删除元素,无需事先指定大小。
  2. 快速随机访问:由于底层使用数组实现,数组列表支持快速随机访问元素,可以通过索引直接访问元素。
  3. 插入和删除效率较低:在数组列表的中间位置插入或删除元素时,需要将后续元素搬移,导致效率较低。
  4. 不支持基本数据类型:数组列表只能存储对象类型,不支持存储基本数据类型(如 int、double 等),需要使用对应的包装类。
  5. 线程不安全:数组列表不是线程安全的,如果多个线程同时对同一个数组列表进行修改,可能会导致不确定的结果,需要在多线程环境下进行同步处理。

以下是一个使用 ArrayList 的示例代码:

import java.util.ArrayList;

public class ArrayListExample {

    public static void main(String[] args) {
        // 创建一个空的数组列表
        ArrayList<String> fruits = new ArrayList<>();

        // 添加元素到数组列表
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Orange");

        // 获取数组列表的大小
        int size = fruits.size();
        System.out.println("数组列表大小:" + size);

        // 获取指定索引位置的元素
        String firstFruit = fruits.get(0);
        System.out.println("第一个水果:" + firstFruit);

        // 修改指定索引位置的元素
        fruits.set(1, "Grapes");
        System.out.println("修改后的数组列表:" + fruits);

        // 删除指定元素
        fruits.remove("Apple");
        System.out.println("删除后的数组列表:" + fruits);

        // 判断数组列表是否包含某个元素
        boolean containsOrange = fruits.contains("Orange");
        System.out.println("数组列表包含橙子吗?" + containsOrange);

        // 清空数组列表
        fruits.clear();
        System.out.println("清空后的数组列表:" + fruits);
    }
}

上述示例代码创建了一个 ArrayList 来存储水果名称,并演示了添加、获取、修改、删除、判断是否包含、清空等操作。输出结果如下:

数组列表大小:3
第一个水果:Apple
修改后的数组列表:[Apple, Grapes, Orange]
删除后的数组列表:[Grapes, Orange]
数组列表包含橙子吗?true
清空后的数组列表:[]

3.3 散列集(HashSet)

散列集(HashSet)是一种实现了 Set 接口的集合,它使用哈希表来存储元素。HashSet 中的元素是无序的,并且不允许重复。HashSet 是根据对象的哈希码来存储和访问元素的,因此具有快速的插入、删除和查找操作。

以下是散列集的一些关键特点和操作:

  1. 无序性:HashSet 中的元素没有固定的顺序,不会保持插入的顺序,也不会按照元素的大小顺序排列。
  2. 唯一性:HashSet 不允许重复的元素,如果尝试添加重复的元素,那么添加操作将被忽略。
  3. 快速插入、删除和查找:由于使用哈希表实现,HashSet 具有快速的插入、删除和查找操作,平均时间复杂度为 O(1)。
  4. 不保证元素顺序:虽然在实现上使用了哈希表,但不保证元素的顺序,即使元素的哈希码相同,它们在散列集中的顺序也可能不同。
  5. 不支持索引:HashSet 不支持像 List 那样通过索引访问元素,因为它没有固定的顺序。

以下是一个使用 HashSet 的示例代码:

import java.util.HashSet;

public class HashSetExample {

    public static void main(String[] args) {
        // 创建一个空的散列集
        HashSet<String> set = new HashSet<>();

        // 添加元素到散列集
        set.add("Apple");
        set.add("Banana");
        set.add("Orange");
        set.add("Apple"); // 添加重复元素,将被忽略

        // 获取散列集的大小
        int size = set.size();
        System.out.println("散列集大小:" + size);

        // 判断散列集是否包含某个元素
        boolean containsBanana = set.contains("Banana");
        System.out.println("散列集包含香蕉吗?" + containsBanana);

        // 删除元素
        set.remove("Orange");
        System.out.println("删除后的散列集:" + set);

        // 清空散列集
        set.clear();
        System.out.println("清空后的散列集:" + set);
    }
}

上述示例代码创建了一个 HashSet 来存储水果名称,并演示了添加、获取大小、判断是否包含、删除和清空等操作。输出结果如下:

散列集大小:3
散列集包含香蕉吗?true
删除后的散列集:[Apple, Banana]
清空后的散列集:[]

3.4 树集(TreeSet)

树集(TreeSet)是一种实现了 SortedSet 接口的集合,它是一个有序的集合,按照元素的自然顺序或者指定的比较器进行排序。树集内部使用红黑树(Red-Black Tree)数据结构来存储元素,保证了元素在集合中的有序性。

以下是树集的一些关键特点和操作:

  1. 有序性:树集中的元素是有序的,可以根据元素的自然顺序或者指定的比较器进行排序。对于元素的自然顺序,元素类必须实现 Comparable 接口,重写 compareTo() 方法来定义比较规则。
  2. 唯一性:树集不允许重复的元素,如果尝试添加重复的元素,那么添加操作将被忽略。
  3. 快速插入、删除和查找:由于使用红黑树作为底层数据结构,树集具有快速的插入、删除和查找操作,时间复杂度为 O(log n),其中 n 是树集的大小。
  4. 支持范围查询:树集提供了一些有用的方法,如 subSet()、headSet()、tailSet(),可以根据指定的范围获取子集。
  5. 不允许存储空值:树集不允许存储空值,如果尝试添加空值,将会抛出 NullPointerException。

以下是一个使用 TreeSet 的示例代码:

import java.util.TreeSet;

public class TreeSetExample {

    public static void main(String[] args) {
        // 创建一个空的树集,按照元素的自然顺序排序
        TreeSet<Integer> set = new TreeSet<>();

        // 添加元素到树集
        set.add(5);
        set.add(2);
        set.add(8);
        set.add(3);
        set.add(1);
        set.add(5); // 添加重复元素,将被忽略

        // 获取树集的大小
        int size = set.size();
        System.out.println("树集大小:" + size);

        // 判断树集是否包含某个元素
        boolean contains3 = set.contains(3);
        System.out.println("树集包含数字 3 吗?" + contains3);

        // 删除元素
        set.remove(8);
        System.out.println("删除后的树集:" + set);

        // 获取最小元素和最大元素
        int min = set.first();
        int max = set.last();
        System.out.println("最小元素:" + min);
        System.out.println("最大元素:" + max);

        // 获取小于等于指定元素的最大元素
        int floor = set.floor(4);
        System.out.println("小于等于 4 的最大元素:" + floor);

        // 获取大于等于指定元素的最小元素
        int ceiling = set.ceiling(6);
        System.out.println("大于等于 6 的最小元素:" + ceiling);
    }
}

上述示例代码创建了一个 TreeSet 来存储整数,并演示了添加、获取大小、判断是否包含、删除、获取最小和最大元素等操作。输出结果如下:

树集大小:5
树集包含数字 3 吗?true
删除后的树集:[1, 2, 3, 5]
最小元素:1
最大元素:5
小于等于 4 的最大元素:3
大于等于 6 的最小元素:null

注意最后一行输出为 null,因为树集中没有大于等于 6 的元素。

3.5 队列与双端队列(Queue&Deque)

队列(Queue)和双端队列(Deque)是两种特殊的集合接口,它们用于存储一组元素,并且可以按照一定规则进行添加、删除和访问操作。它们都继承自 Collection 接口。

  1. 队列(Queue)
    • 队列是一种先进先出(First-In-First-Out,FIFO)的数据结构,类似于排队的概念。新元素被添加到队列的末尾,称为队尾,而元素从队列的开头被移除,称为队头。
    • 队列的常用方法包括:
      • add(element):将元素添加到队列的末尾,如果队列已满则抛出异常。
      • offer(element):将元素添加到队列的末尾,如果队列已满则返回 false。
      • remove():移除并返回队头的元素,如果队列为空则抛出异常。
      • poll():移除并返回队头的元素,如果队列为空则返回 null。
      • element():返回队头的元素,但不移除它,如果队列为空则抛出异常。
      • peek():返回队头的元素,但不移除它,如果队列为空则返回 null。
  2. 双端队列(Deque)
    • 双端队列是一种既支持在队头添加和移除元素,又支持在队尾添加和移除元素的数据结构。它既可以用作队列,也可以用作栈。
    • 双端队列的常用方法包括:
      • addFirst(element) / offerFirst(element):将元素添加到队头。
      • addLast(element) / offerLast(element):将元素添加到队尾。
      • removeFirst() / pollFirst():移除并返回队头的元素。
      • removeLast() / pollLast():移除并返回队尾的元素。
      • getFirst() / peekFirst():返回队头的元素,但不移除它。
      • getLast() / peekLast():返回队尾的元素,但不移除它。

在 Java 集合框架中,两种接口的常用实现类如下:

  • 队列:常用实现类有 LinkedList、PriorityQueue(优先队列)等。
  • 双端队列:常用实现类有 LinkedList、ArrayDeque 等。

3.6 优先队列(PriorityQueue)

优先队列(PriorityQueue)是一种特殊的队列,它根据元素的优先级进行排序,优先级高的元素先被取出。优先队列通常用于实现任务调度、事件处理等场景,确保高优先级的任务或事件先被处理。

优先队列的特点是:

  • 元素按照优先级被排序,优先级高的元素排在队列的前面。
  • 默认情况下,优先队列使用元素的自然顺序来进行排序,即元素类必须实现 Comparable 接口,重写 compareTo() 方法来定义比较规则。也可以通过在创建优先队列时传入自定义的比较器来指定排序规则。

优先队列的常用方法包括:

  • add(element) / offer(element):将元素添加到优先队列中。
  • remove() / poll():移除并返回优先队列中优先级最高的元素。
  • peek():返回优先队列中优先级最高的元素,但不移除它。

以下是一个使用优先队列的简单示例代码:

import java.util.PriorityQueue;

public class PriorityQueueExample {

    public static void main(String[] args) {
        // 创建一个优先队列,默认按照元素的自然顺序排序
        PriorityQueue<Integer> pq = new PriorityQueue<>();

        // 添加元素到优先队列
        pq.add(5);
        pq.add(2);
        pq.add(8);
        pq.add(3);
        pq.add(1);

        // 获取优先队列的大小
        int size = pq.size();
        System.out.println("优先队列大小:" + size);

        // 获取优先队列中优先级最高的元素
        int highestPriority = pq.peek();
        System.out.println("优先队列中优先级最高的元素:" + highestPriority);

        // 移除并返回优先队列中优先级最高的元素
        int removedElement = pq.poll();
        System.out.println("移除的元素:" + removedElement);

        // 获取移除元素后的优先队列大小
        size = pq.size();
        System.out.println("优先队列大小:" + size);

        // 获取优先队列中优先级最高的元素(已移除)
        highestPriority = pq.peek();
        System.out.println("优先队列中优先级最高的元素:" + highestPriority);
    }
}

输出结果如下:

优先队列大小:5
优先队列中优先级最高的元素:1
移除的元素:1
优先队列大小:4
优先队列中优先级最高的元素:2

在示例代码中,我们创建了一个优先队列并添加了一些整数元素,然后演示了获取最高优先级元素和移除元素的操作。优先队列会根据元素的优先级从小到大进行排序,默认情况下是按照元素的自然顺序排序。

4. 映射(Map)

在 Java 集合框架中,映射(Map)是一种用于存储键值对(Key-Value Pair)的数据结构,它将键映射到值。每个键在映射中是唯一的,每个键可以关联一个值。映射允许通过键来快速访问对应的值,而不需要遍历整个集合。

映射在 Java 中是通过 Map 接口定义的,Map 接口是 Java 集合框架的一部分,它提供了对映射操作的通用接口定义。Map 接口的常用实现类有:HashMap、TreeMap、LinkedHashMap 等。

4.1 基本映射操作

常用的映射操作包括:

  • put(key, value):将键值对添加到映射中,如果键已经存在,则替换对应的值。
  • get(key):根据键获取对应的值。
  • remove(key):根据键移除键值对。
  • containsKey(key):判断映射中是否包含指定的键。
  • containsValue(value):判断映射中是否包含指定的值。
  • size():获取映射中键值对的数量。

以下是一个使用 HashMap 的示例代码:

import java.util.HashMap;
import java.util.Map;

public class HashMapExample {

    public static void main(String[] args) {
        // 创建一个 HashMap 来存储学生的成绩
        Map<String, Integer> scores = new HashMap<>();

        // 添加学生成绩
        scores.put("Alice", 85);
        scores.put("Bob", 90);
        scores.put("Carol", 78);
        scores.put("David", 95);

        // 获取学生成绩
        int aliceScore = scores.get("Alice");
        System.out.println("Alice 的成绩:" + aliceScore);

        // 判断是否包含指定的键
        boolean containsBob = scores.containsKey("Bob");
        System.out.println("是否包含 Bob:" + containsBob);

        // 移除学生成绩
        scores.remove("Carol");

        // 获取映射的大小
        int size = scores.size();
        System.out.println("映射的大小:" + size);
    }
}

输出结果如下:

Alice 的成绩:85
是否包含 Bob:true
映射的大小:3

在示例代码中,我们使用 HashMap 来存储学生的成绩信息,并演示了添加、获取、移除和判断键是否存在的操作。映射根据键来快速获取对应的值,可以方便地进行数据的存储和检索。

4.2 更新映射条目

映射(Map)可以更新映射条目,即修改已存在的键对应的值,或者添加新的键值对。映射的更新操作可以通过以下方法实现:

  1. put(key, value):将指定的键值对添加到映射中。如果键已经存在于映射中,则该键对应的旧值会被新值替换,并返回旧值;如果键不存在于映射中,则将新的键值对添加到映射中,并返回 null。
  2. replace(key, newValue):替换映射中指定键的值。如果键存在于映射中,则将其对应的值替换为新值,并返回旧值;如果键不存在于映射中,则返回 null。
  3. putAll(map):将另一个映射中的所有键值对添加到当前映射中。

4.3 映射视图

在 Java 集合框架中,映射视图(Map View)是指从映射(Map)中获取一种特殊视图,用于方便地查看映射中的键、值或键值对。映射视图提供了一种查看映射内容的方式,它可以让我们直接查看映射中的元素,而不需要操作映射本身。

在 Java 中,映射视图主要有以下几种类型:

  1. 键集视图(Key Set View):获取映射中所有键组成的集合。键集视图是一个 Set 集合,它包含映射中所有的键,且键是唯一的。可以通过调用 Map 的 keySet() 方法来获取键集视图。
  2. 值集视图(Value Collection View):获取映射中所有值组成的集合。值集视图是一个 Collection 集合,它包含映射中所有的值,值可以重复。可以通过调用 Map 的 values() 方法来获取值集视图。
  3. 键值对集视图(Entry Set View):获取映射中所有键值对组成的集合。键值对集视图是一个 Set 集合,它包含映射中所有的键值对,每个键值对都是一个 Map.Entry 对象。可以通过调用 Map 的 entrySet() 方法来获取键值对集视图。

通过使用映射视图,我们可以方便地遍历映射中的键、值或键值对,而无需直接操作映射本身。

以下是一个使用映射视图的示例代码:

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapViewExample {

    public static void main(String[] args) {
        Map<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 85);
        scores.put("Bob", 90);
        scores.put("Carol", 78);
        scores.put("David", 95);

        // 获取键集视图
        Set<String> keySet = scores.keySet();
        System.out.println("键集视图:" + keySet);

        // 获取值集视图
        System.out.println("值集视图:" + scores.values());

        // 获取键值对集视图
        Set<Map.Entry<String, Integer>> entrySet = scores.entrySet();
        System.out.println("键值对集视图:");
        for (Map.Entry<String, Integer> entry : entrySet) {
            System.out.println(entry.getKey() + " 的成绩是 " + entry.getValue());
        }
    }
}

输出结果如下:

键集视图:[Alice, Bob, Carol, David]
值集视图:[85, 90, 78, 95]
键值对集视图:
Alice 的成绩是 85
Bob 的成绩是 90
Carol 的成绩是 78
David 的成绩是 95

在示例代码中,我们创建了一个 HashMap 来存储学生成绩,并通过 keySet()values()entrySet() 方法获取键集视图、值集视图和键值对集视图,然后分别打印了它们的内容。

4.4 弱散列映射(WeakHashMap)

弱散列映射(WeakHashMap)是一种特殊类型的映射,它的键是弱引用(Weak Reference),而值是普通引用(Strong Reference)。弱引用的特点是,当对象只被弱引用指向时,如果发生垃圾回收,该对象会被自动回收,即使内存不足也不会阻止垃圾回收器回收这些对象。

弱散列映射的特点如下:

  • 弱散列映射允许在某些条件下,自动地将不再被其他强引用指向的键移除掉,从而释放对应的值。这使得弱散列映射非常适合用于缓存数据,可以防止由于缓存引用导致的内存泄漏。
  • 对于键来说,只有当没有强引用指向它时,才会被移除。一旦某个键被垃圾回收器回收,其对应的键值对就会从弱散列映射中被自动删除。
  • 弱散列映射没有提供迭代器支持,因为在迭代过程中可能发生键的自动删除,可能会导致并发问题。

以下是一个简单的示例代码,演示了弱散列映射的基本使用:

import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapExample {

    public static void main(String[] args) {
        Map<Key, Value> weakMap = new WeakHashMap<>();

        // 创建两个键对象
        Key key1 = new Key(1);
        Key key2 = new Key(2);

        // 将键对象和值对象放入弱散列映射中
        weakMap.put(key1, new Value("Value1"));
        weakMap.put(key2, new Value("Value2"));

        // 设置键对象为 null,不再有强引用指向它们
        key1 = null;
        key2 = null;

        // 垃圾回收
        System.gc();

        // 由于弱引用,键对象已被回收,相关的键值对会被从弱散列映射中删除
        System.out.println("弱散列映射中的键值对数量:" + weakMap.size());
    }

    static class Key {
        private int id;

        public Key(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "Key-" + id;
        }
    }

    static class Value {
        private String value;

        public Value(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return "Value-" + value;
        }
    }
}

输出结果可能是:

弱散列映射中的键值对数量:0

在示例代码中,我们创建了一个弱散列映射 weakMap 并向其中添加两个键值对。然后将键对象 key1key2 设置为 null,不再有强引用指向它们。接着调用 System.gc() 进行垃圾回收,由于键对象是弱引用,它们会被回收,从而相关的键值对会从弱散列映射中被自动删除。所以输出的结果是弱散列映射中的键值对数量为0。

4.5 枚举集与映射(EnumSet&EnumMap)

在 Java 集合框架中,枚举集合(EnumSet)和枚举映射(EnumMap)是专门用于处理枚举类型的集合和映射。

  • 枚举集合(EnumSet):
    • EnumSet 是一个抽象类,用于存储枚举类型的元素,它是一个非常高效的集合实现,内部使用位向量进行存储,因此具有较小的内存开销和较高的执行效率。
    • EnumSet 只能包含指定枚举类型的元素,不能包含其他类型的元素。
    • EnumSet 中的元素是有序的,并且根据枚举中的声明顺序进行排列。
    • EnumSet 不允许添加 null 元素。

以下是使用 EnumSet 的示例代码:

import java.util.EnumSet;

public class EnumSetExample {

    enum Color {
        RED, GREEN, BLUE, YELLOW
    }

    public static void main(String[] args) {
        EnumSet<Color> colors = EnumSet.of(Color.RED, Color.GREEN);
        System.out.println("枚举集合中的元素:" + colors);

        // 添加其他枚举元素
        colors.add(Color.BLUE);
        System.out.println("添加后的枚举集合:" + colors);

        // 移除枚举元素
        colors.remove(Color.RED);
        System.out.println("移除后的枚举集合:" + colors);

        // 清空集合
        colors.clear();
        System.out.println("清空后的枚举集合:" + colors);
    }
}

输出结果如下:

枚举集合中的元素:[RED, GREEN]
添加后的枚举集合:[RED, GREEN, BLUE]
移除后的枚举集合:[GREEN, BLUE]
清空后的枚举集合:[]
  • 枚举映射(EnumMap):
    • EnumMap 是一个具体的映射实现,它专门用于存储键为枚举类型,值为任意类型的映射。
    • EnumMap 中的键必须来自一个特定的枚举类型,且在创建 EnumMap 时需要指定该枚举类型的 class 对象。
    • EnumMap 内部使用数组来存储键值对,因此在具有相同枚举类型的情况下,EnumMap 的性能通常优于其他映射实现。
    • EnumMap 不允许添加 null 键,但允许添加 null 值。

以下是使用 EnumMap 的示例代码:

import java.util.EnumMap;

public class EnumMapExample {

    enum Day {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
    }

    public static void main(String[] args) {
        EnumMap<Day, String> schedule = new EnumMap<>(Day.class);

        // 添加映射条目
        schedule.put(Day.MONDAY, "Meeting");
        schedule.put(Day.WEDNESDAY, "Workshop");
        schedule.put(Day.FRIDAY, "Conference");
        System.out.println("映射:" + schedule);

        // 获取映射值
        String wednesdayEvent = schedule.get(Day.WEDNESDAY);
        System.out.println("星期三的活动:" + wednesdayEvent);

        // 移除映射条目
        schedule.remove(Day.FRIDAY);
        System.out.println("移除后的映射:" + schedule);
    }
}

输出结果如下:

映射:{MONDAY=Meeting, WEDNESDAY=Workshop, FRIDAY=Conference}
星期三的活动:Workshop
移除后的映射:{MONDAY=Meeting, WEDNESDAY=Workshop}

在示例代码中,我们使用 EnumMap 来存储星期几对应的活动,演示了如何添加映射条目、获取映射值以及移除映射条目的操作。注意在创建 EnumMap 时需要传入对应的枚举类型 Day.class。

4.6 标识散列映射(IdentityHashMap)

IdentityHashMap 是一种特殊类型的散列映射,它与普通的散列映射(如 HashMap)有所不同。

IdentityHashMap 中,键的比较是基于对象的身份(identity),而不是基于对象的值(value)。在 Java 中,每个对象都有一个唯一的身份标识,即使它们的值相等也可能具有不同的身份。这意味着,对于 IdentityHashMap 来说,只有当两个键是同一个对象时(即身份相同),它们才会被认为是相等的,即使它们的值可能不同。

IdentityHashMap 的主要特点如下:

  • 键的比较是基于对象的身份,而不是对象的值。
  • 对象的身份是由对象在内存中的地址决定的,因此不同对象的身份是不同的,即使它们的值相同。
  • IdentityHashMap 适用于需要根据对象身份而不是对象值进行映射的场景。
  • IdentityHashMap 没有实现 Map 接口的常规 equals()hashCode() 方法,而是使用 == 运算符进行比较。

以下是一个简单的示例代码,演示了如何使用 IdentityHashMap

import java.util.IdentityHashMap;

public class IdentityHashMapExample {

    public static void main(String[] args) {
        IdentityHashMap<String, Integer> identityMap = new IdentityHashMap<>();

        String key1 = new String("Alice");
        String key2 = new String("Alice");

        // 不同的对象,但是身份不同
        identityMap.put(key1, 1);
        identityMap.put(key2, 2);

        System.out.println("IdentityHashMap 中的键值对数量:" + identityMap.size());
        System.out.println("key1 对应的值:" + identityMap.get(key1));
        System.out.println("key2 对应的值:" + identityMap.get(key2));
    }
}

输出结果可能是:

IdentityHashMap 中的键值对数量:2
key1 对应的值:1
key2 对应的值:2

在示例代码中,我们创建了两个具有相同值的字符串对象 key1key2,然后将它们作为键存储在 IdentityHashMap 中。由于这两个对象具有不同的身份(即不同的内存地址),它们在 IdentityHashMap 中被视为不同的键,因此可以成功地存储两个键值对。

5. 副本与视图

在集合框架中,有两种常见的概念:副本(Copy)和视图(View)。这两者之间的区别在于它们在处理数据时是否会影响原始集合。

  • 副本(Copy): 副本是指通过复制原始集合中的元素创建一个新的集合,这个新集合与原始集合是完全独立的,它们之间互不影响。对副本进行的任何修改都不会反映到原始集合中,反之亦然。

    List<String> originalList = new ArrayList<>();
    originalList.add("A");
    originalList.add("B");
    originalList.add("C");
    
    List<String> copyList = new ArrayList<>(originalList); // 创建副本
    copyList.add("D");
    
    System.out.println(originalList); // 输出:[A, B, C]
    System.out.println(copyList); // 输出:[A, B, C, D]
    
  • 视图(View): 视图是指基于原始集合创建的一个新的集合对象,但它与原始集合共享相同的数据源。当对视图进行修改时,会直接反映到原始集合中,反之亦然。

    List<String> originalList = new ArrayList<>();
    originalList.add("A");
    originalList.add("B");
    originalList.add("C");
    
    List<String> viewList = originalList.subList(0, 2); // 创建视图
    viewList.add("D");
    
    System.out.println(originalList); // 输出:[A, B, D, C]
    System.out.println(viewList); // 输出:[A, B, D]
    

请注意,不是所有集合类都支持视图操作,只有一些特定的集合类提供了创建视图的方法,比如 List.subList() 方法可以创建列表的视图。

因此,在 Java 集合中,使用副本可以创建一个与原始集合完全独立的集合,不会互相影响。而使用视图则可以在原始集合的基础上创建一个子集合,可以实现对原始集合的部分数据进行操作,这样可以节省内存和避免额外的复制操作。

5.1 小集合-java9

Java 9引入了一组新的小集合工厂方法,用于创建不可变(不可修改)的小集合对象。这些小集合工厂方法位于java.util包中,主要用于创建不可变的列表、集合和映射,以简化代码并提高性能。

这些小集合工厂方法的优势在于,它们创建的集合对象是不可修改的,也就是说,一旦创建后就不能再添加、删除或修改其中的元素。不可变集合在多线程环境下具有线程安全性,不需要进行同步,从而提高了并发性能。

以下是 Java 9 提供的小集合工厂方法:

  • List.of() 创建不可变列表。
javaCopy code
List<String> immutableList = List.of("A", "B", "C");
  • Set.of() 创建不可变集。
javaCopy code
Set<String> immutableSet = Set.of("A", "B", "C");
  • Map.of() 创建不可变映射。
javaCopy code
Map<String, Integer> immutableMap = Map.of("A", 1, "B", 2, "C", 3);

这些方法都接受一系列的参数,并使用这些参数创建相应的不可变集合对象。在使用这些工厂方法创建的集合中,如果尝试添加、删除或修改元素,将会抛出UnsupportedOperationException异常,因为它们是不可变的。

请注意,由于这些小集合是不可变的,因此它们在创建后不能修改。如果需要动态地添加、删除或修改集合元素,可以继续使用普通的可变集合类,如ArrayListHashSetHashMap

5.2 不可修改的副本和视图

在 Java 集合框架中,我们可以通过两种方式来创建不可修改的副本和视图:使用不可修改的集合工厂方法和使用集合工具类的方法。

  • 不可修改的副本(Unmodifiable Copy):

    • 使用不可修改的集合工厂方法(Java 9+):

      List<String> originalList = new ArrayList<>();
      originalList.add("A");
      originalList.add("B");
      originalList.add("C");
      
      List<String> unmodifiableCopyList = List.copyOf(originalList); // 创建不可修改的副本
      
    • 使用集合工具类的方法:

      List<String> originalList = new ArrayList<>();
      originalList.add("A");
      originalList.add("B");
      originalList.add("C");
      
      List<String> unmodifiableCopyList = Collections.unmodifiableList(new ArrayList<>(originalList)); // 创建不可修改的副本
      
  • 不可修改的视图(Unmodifiable View):

    • 使用集合工具类的方法:

      List<String> originalList = new ArrayList<>();
      originalList.add("A");
      originalList.add("B");
      originalList.add("C");
      
      List<String> unmodifiableViewList = Collections.unmodifiableList(originalList.subList(0, 2)); // 创建不可修改的视图
      

不可修改的副本和视图都是指创建一个与原始集合相关的不可修改集合。它们的区别在于,不可修改的副本是创建了一个全新的不可修改集合对象,而不可修改的视图是基于原始集合的一个不可修改子集合。

不可修改的副本和视图都具有以下特点:

  • 一旦创建后,不能修改其中的元素(添加、删除、修改)。
  • 修改原始集合的操作不会影响不可修改的副本或视图,反之亦然。

需要注意的是,不可修改的副本和视图适用于在不希望修改集合的情况下进行只读操作。如果需要对集合进行动态修改,应该使用普通的可变集合类,如ArrayListHashSetHashMap

5.3 子范围

Java 集合框架中常见的子范围方法包括:

  • List.subList(): 用于获取列表的一个子列表。它返回一个新的 List 对象,这个对象是原始列表的一个视图,即它与原始列表共享部分元素。
List<String> originalList = new ArrayList<>();
originalList.add("A");
originalList.add("B");
originalList.add("C");
originalList.add("D");
originalList.add("E");

List<String> subList = originalList.subList(1, 4);
System.out.println(subList); // 输出:[B, C, D]

// 修改子列表会影响原始列表
subList.set(0, "X");
System.out.println(originalList); // 输出:[A, X, C, D, E]

// 修改原始列表会影响子列表
originalList.add("F");
System.out.println(subList); // 输出:[X, C, D, E, F]

通过使用子范围方法,我们可以方便地从集合中提取一部分数据,而不需要复制整个集合,这在节省内存和提高性能方面非常有用。需要注意的是,对子范围视图的修改会直接影响原始集合,反之亦然。如果需要不影响原始集合的数据操作,应该考虑使用不可修改的视图(Unmodifiable View)。

5.4 检查型视图

Java 集合框架中的 Collections.checkedXxx() 方法用于创建检查型视图。这些方法返回一个新的集合对象,该对象与原始集合是一个视图,但它会在运行时检查添加和更新的元素是否符合指定的类型,如果不符合,则会抛出 ClassCastException

下面以 List 为例,说明如何创建检查型视图:

List<String> originalList = new ArrayList<>();
originalList.add("A");
originalList.add("B");

List<String> checkedView = Collections.checkedList(originalList, String.class);

checkedView.add("C"); // 正常,符合指定的类型
checkedView.add(123); // 运行时会抛出 ClassCastException,因为不符合指定的类型

在上述例子中,我们创建了一个原始的 ArrayList 集合 originalList,并使用 Collections.checkedList() 方法创建了一个检查型视图 checkedView。我们指定该视图只允许包含 String 类型的元素。当我们尝试在 checkedView 中添加不符合指定类型的元素时(例如整数 123),在运行时会抛出 ClassCastException

检查型视图的主要优点在于它可以在编译时捕获类型不匹配的错误,避免在运行时出现意外的类型转换异常。这在某些情况下对于类型安全性非常有用,但也需要注意,检查型视图会在运行时引入额外的性能开销和类型检查开销。

除了 Collections.checkedList() 方法,Java 集合框架中还提供了类似的 checkedSet()checkedMap() 方法,用于创建检查型的集和映射视图,用法类似。

5.5 同步视图

在 Java 集合框架中,除了常见的副本和视图,还有一种特殊类型的视图,称为“同步视图”(Synchronized View)。同步视图用于在多线程环境下对集合进行同步操作,确保线程安全性。

Java 集合框架中的 Collections.synchronizedXxx() 方法用于创建同步视图。这些方法返回一个新的集合对象,该对象与原始集合是一个视图,但它会对集合的操作进行同步,从而确保在多线程环境中对集合的操作是安全的。

下面以 List 为例,说明如何创建同步视图:

List<String> originalList = new ArrayList<>();
originalList.add("A");
originalList.add("B");

List<String> synchronizedView = Collections.synchronizedList(originalList);

在上述例子中,我们创建了一个原始的 ArrayList 集合 originalList,并使用 Collections.synchronizedList() 方法创建了一个同步视图 synchronizedView。这个视图会对 originalList 的操作进行同步,确保在多线程环境中对集合的操作是线程安全的。

需要注意的是,尽管同步视图确保了线程安全性,但在高并发环境下,使用同步视图可能会带来性能上的开销。因为在同步视图中,每个方法调用都需要获得锁,这可能导致多个线程之间的竞争,从而影响性能。在高并发场景下,可以考虑使用其他并发集合,如 ConcurrentHashMapCopyOnWriteArrayList 等,以获得更好的性能。

总结:同步视图是一种用于在多线程环境下确保集合操作的线程安全性的特殊视图。它通过对集合的操作进行同步来实现线程安全。但要注意,在高并发环境下,同步视图可能会带来性能开销,可以根据具体情况选择合适的线程安全集合。

5.6 关于可选操作的说明

在 Java 集合框架中,可选操作(Optional Operations)指的是一些在某些集合实现中可能不支持的操作。这些操作在接口定义中存在,但实现类可能不支持或者可能抛出 UnsupportedOperationException 异常。

可选操作主要存在于一些通用的集合接口,比如 ListSetMap,以及它们的子接口。在这些接口中,有一些方法在某些集合实现中可能没有提供具体的实现或者没有合理的实现,因此被标记为可选操作。

例如,在 List 接口中有一些可选操作:

  • add(int index, E element): 在指定索引处插入元素。
  • remove(int index): 移除指定索引处的元素。
  • replaceAll(UnaryOperator<E> operator): 使用指定的操作符替换所有元素。

Set 接口中也有一些可选操作:

  • add(E e): 添加元素。
  • remove(Object o): 移除指定元素。
  • addAll(Collection<? extends E> c): 添加一个集合的元素。

Map 接口中也有一些可选操作:

  • put(K key, V value): 添加键值对。
  • remove(Object key): 移除指定键对应的值。
  • putAll(Map<? extends K, ? extends V> m): 添加一个映射的键值对。

要注意的是,尽管这些操作被标记为可选,但是大多数集合实现都会提供对这些操作的合理实现。只有在特定的集合实现中,可能会选择不支持这些操作或者抛出异常。

在使用集合时,可以使用 Collection 接口中的 isSupported(Operation operation) 方法来检查某个操作是否被支持,或者使用 try-catch 块来捕获可能的 UnsupportedOperationException 异常。

总结:可选操作是指在 Java 集合框架中一些方法在某些集合实现中可能不支持或抛出异常的操作。大多数集合实现都会提供合理的实现,但要注意一些特定实现可能会选择不支持这些操作。

6. 算法

6.1 为什么使用泛型算法

Java 泛型算法主要用于提供一种通用的算法实现,使得算法能够处理不同类型的数据,而无需针对不同类型编写重复的代码。使用泛型算法的好处如下:

  1. 代码复用性: 泛型算法可以处理多种数据类型,从而避免了为不同类型编写重复代码的情况。通过一个泛型算法,可以适用于不同类型的数据集合,提高了代码的复用性。
  2. 类型安全: 泛型算法在编译时会对数据类型进行检查,从而确保只能使用兼容的数据类型。这种类型检查可以在编译阶段捕获潜在的类型错误,提高了代码的可靠性和类型安全性。
  3. 简化代码: 泛型算法可以使代码更加简洁,不需要为不同类型的数据编写多个版本的算法。泛型算法可以在同一个实现中适用于多种数据类型,减少了代码的冗余。
  4. 更高的性能: 使用泛型算法可以避免使用对象的装箱和拆箱操作,提高了代码的性能。泛型算法在处理基本数据类型时,可以直接使用原始数据类型,而不需要使用对象类型,从而提高了执行效率。
  5. 适用范围广: 泛型算法适用于大部分数据类型,包括自定义的类和基本数据类型。它使得算法可以处理更广泛的数据类型,增加了算法的灵活性和通用性。

在 Java 中,标准库中的许多集合类和算法都使用了泛型来实现通用性。例如,java.util.Collections 类中提供了许多泛型算法,如 sort()binarySearch()min()max() 等,它们都可以处理不同类型的数据集合。

6.2 排序与混排

Java 算法中的排序和混洗(Shuffle)是两种常见的数据处理操作。

  • 排序: 排序是将一组元素按照特定的规则或条件进行有序排列的过程。在 Java 中,排序通常使用 java.util.Collections.sort() 方法或数组的 java.util.Arrays.sort() 方法来实现。这些方法使用了优化的排序算法,如归并排序(Merge Sort)或快速排序(Quick Sort),以达到较高的性能。

示例使用 Collections.sort() 方法对列表进行排序:

List<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(2);
numbers.add(7);
numbers.add(3);

Collections.sort(numbers); // 对列表进行排序

System.out.println(numbers); // 输出:[2, 3, 5, 7]

示例使用 Arrays.sort() 方法对数组进行排序:

int[] array = {5, 2, 7, 3};
Arrays.sort(array); // 对数组进行排序

System.out.println(Arrays.toString(array)); // 输出:[2, 3, 5, 7]
  • 混洗(Shuffle): 混洗是将一组元素打乱顺序的过程,使得每个元素出现在一个随机位置。在 Java 中,混洗通常使用 java.util.Collections.shuffle() 方法或使用洗牌算法来实现。洗牌算法通常是通过随机交换数组或列表中的元素来打乱它们的顺序。

示例使用 Collections.shuffle() 方法对列表进行混洗:

List<String> cards = new ArrayList<>();
cards.add("A");
cards.add("2");
cards.add("3");
cards.add("4");
cards.add("5");

Collections.shuffle(cards); // 对列表进行混洗

System.out.println(cards); // 输出:[3, 4, A, 5, 2](结果会因为随机性而不同)

示例使用洗牌算法对数组进行混洗:

int[] array = {1, 2, 3, 4, 5};
Random random = new Random();

for (int i = array.length - 1; i > 0; i--) {
    int index = random.nextInt(i + 1);
    int temp = array[index];
    array[index] = array[i];
    array[i] = temp;
}

System.out.println(Arrays.toString(array)); // 输出:[3, 4, 2, 1, 5](结果会因为随机性而不同)

排序和混洗是在处理数据时常见的操作,它们能够对数据进行有序排列和打乱,使得数据在不同场景下更易于使用和展示。在 Java 中,使用标准库提供的排序和混洗方法能够方便地实现这些操作。

6.3 二分查找

在 Java 集合框架中,Collections 类提供了一个名为 binarySearch() 的静态方法,用于在有序列表(List)中执行二分查找算法。该方法接受一个有序的 List 和要查找的目标元素作为输入,返回目标元素在 List 中的索引,如果目标元素不存在,则返回一个负数值来表示它应该插入的位置(即插入点)的负值形式。

binarySearch() 方法的签名如下:

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key)

其中,list 是一个有序的 List,key 是要查找的目标元素。

以下是使用 Collections.binarySearch() 方法进行二分查找的示例代码:

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

public class BinarySearchExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(3);
        numbers.add(5);
        numbers.add(7);
        numbers.add(9);
        numbers.add(11);

        int target = 5;
        int index = Collections.binarySearch(numbers, target);

        if (index >= 0) {
            System.out.println("目标元素 " + target + " 的索引为 " + index);
        } else {
            int insertPoint = -index - 1;
            System.out.println("未找到目标元素 " + target + ",应该插入到索引 " + insertPoint);
        }
    }
}

输出:

目标元素 5 的索引为 2

在上述示例中,我们使用 Collections.binarySearch() 方法在有序列表 numbers 中查找目标元素 target。由于列表 numbers 是有序的,所以使用二分查找算法可以快速找到目标元素的索引。如果目标元素存在于列表中,binarySearch() 方法返回它的索引。如果目标元素不存在,它会返回一个负数值来表示它应该插入的位置(即插入点)的负值形式。

需要注意的是,使用 Collections.binarySearch() 方法进行二分查找的前提是列表必须是有序的。如果列表无序,或者在进行查找之前没有先进行排序,那么结果将是不确定的。

此外,对于自定义的对象类型,要进行二分查找,需要确保该对象实现了 Comparable 接口,或者提供一个自定义的 Comparator 来指定对象的比较规则。否则,binarySearch() 方法无法确定元素的顺序,导致结果不正确。

6.4 简单算法

Java 集合框架提供了许多简单但实用的算法和方法,这些方法可以方便地对集合进行操作和处理。以下是 Java 集合框架提供的一些常见简单算法和方法:

  1. 排序算法: Collections.sort() 方法用于对列表进行排序。
  2. 二分查找: Collections.binarySearch() 方法用于在有序列表中执行二分查找算法。
  3. 混洗(Shuffle): Collections.shuffle() 方法用于对列表进行混洗操作,打乱元素的顺序。
  4. 复制(Copy): Collections.copy() 方法用于将一个列表的元素复制到另一个列表中。
  5. 查找最大和最小值: Collections.max()Collections.min() 方法用于查找列表中的最大值和最小值。
  6. 反转(Reverse): Collections.reverse() 方法用于反转列表中元素的顺序。
  7. 去重(Distinct): Set 接口和实现类如 HashSetTreeSet 提供了去重功能,确保集合中没有重复的元素。
  8. 筛选(Filter): 使用 Stream API 的 filter() 方法进行筛选操作,过滤出符合指定条件的元素。
  9. 映射(Map): 使用 Map 接口表示键值对映射,可以通过 put()get() 方法进行键值对的存储和检索。
  10. 填充(Fill): Collections.fill() 方法用于将列表中的所有元素替换为指定的值。
  11. 添加多个元素(addAll): Collections.addAll() 方法用于将多个元素添加到集合中。
  12. 替换所有元素(replaceAll): Collections.replaceAll() 方法用于将列表中的所有旧值替换为新值。
  13. 计算频率(frequency): Collections.frequency() 方法用于计算集合中指定元素出现的次数。
  14. 交换元素(swap): Collections.swap() 方法用于交换列表中指定两个位置的元素。
  15. 最大值和最小值的位置(indexOfSubList、lastIndexOfSubList): Collections.indexOfSubList()Collections.lastIndexOfSubList() 方法用于查找子列表在列表中第一次和最后一次出现的位置。

这些算法和方法使得对集合进行常见操作更加简单和高效。在使用这些方法时,可以根据具体的需求和场景选择合适的方法,从而提高代码的可读性和维护性。

6.5 批操作

在 Java 集合框架中,提供了一些批操作的方法,用于在一次操作中对集合进行批量处理,从而提高性能和减少不必要的开销。这些批操作方法通常比逐个元素处理更高效,因为它们能够充分利用底层数据结构的特性。

以下是 Java 集合框架提供的一些常见的批操作方法:

  • addAll(): addAll() 方法用于将多个元素添加到集合中。它可以接受一个集合或数组作为输入,并将其所有元素添加到指定的集合中。

    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            List<Integer> numbers = new ArrayList<>();
            List<Integer> newNumbers = new ArrayList<>();
            newNumbers.add(4);
            newNumbers.add(5);
            newNumbers.add(6);
    
            numbers.addAll(newNumbers); // 批量添加元素
    
            System.out.println(numbers); // 输出:[4, 5, 6]
        }
    }
    
  • removeAll(): removeAll() 方法用于从集合中批量删除指定的元素。它接受一个集合作为输入,并将集合中所有与输入集合中相同的元素从原集合中删除。

    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            List<Integer> numbers = new ArrayList<>();
            numbers.add(1);
            numbers.add(2);
            numbers.add(3);
    
            List<Integer> removeNumbers = new ArrayList<>();
            removeNumbers.add(2);
            removeNumbers.add(3);
    
            numbers.removeAll(removeNumbers); // 批量删除元素
    
            System.out.println(numbers); // 输出:[1]
        }
    }
    
  • retainAll(): retainAll() 方法用于在集合中保留指定的元素,即删除集合中不在指定集合中的元素。

    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            List<Integer> numbers = new ArrayList<>();
            numbers.add(1);
            numbers.add(2);
            numbers.add(3);
    
            List<Integer> retainNumbers = new ArrayList<>();
            retainNumbers.add(2);
            retainNumbers.add(3);
    
            numbers.retainAll(retainNumbers); // 保留指定元素
    
            System.out.println(numbers); // 输出:[2, 3]
        }
    }
    

这些批操作方法在对集合进行批量处理时非常有用。通过使用这些方法,可以减少循环和逐个元素操作,提高代码的简洁性和执行效率。在进行批操作时,要确保集合的类型和元素类型正确匹配,避免出现类型转换或不一致的问题。

6.6 集合与数组的转换

在 Java 中,可以进行集合与数组之间的转换。Java 集合框架提供了一些方法,用于将集合转换为数组,或将数组转换为集合。下面分别介绍这些转换方法:

  • 集合转数组:

    • 使用 toArray() 方法:Collection 接口提供了 toArray() 方法,用于将集合转换为数组。这个方法返回一个包含集合元素的新数组。
    • 使用带参数的 toArray(T[] a) 方法:这个方法将集合元素复制到给定的数组 a 中,如果数组 a 大小不够,会创建一个新的数组来保存元素。
    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            List<String> colors = new ArrayList<>();
            colors.add("Red");
            colors.add("Green");
            colors.add("Blue");
    
            // 转换为数组
            String[] colorArray1 = colors.toArray(new String[0]); // 不指定大小时,会创建一个新的数组
            String[] colorArray2 = colors.toArray(new String[colors.size()]); // 指定大小与集合大小一致时,不会创建新数组
    
            System.out.println("colorArray1: " + Arrays.toString(colorArray1));
            System.out.println("colorArray2: " + Arrays.toString(colorArray2));
        }
    }
    

    staff.toArray(),这样做的结果是一个对象数组,尽管你知道集合中包含的是一个特定类型的对象,但不能使用强制类型转换,返回的数组创建为一个Object[]数组,不能改变它的类型,实际上,要向toArray()方法传入一个数组构造器的表达式,这样一来,返回的数组就会有正确的表达式

    String[] values = staff.toArray(String[]::new)
    
  • 数组转集合:

    • 使用 Arrays.asList(T... a) 方法:Arrays 类提供了 asList() 方法,可以将数组转换为固定大小的 List。该方法返回一个 List 对象,但它的大小不能改变(即不能添加或删除元素)。
    import java.util.Arrays;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            String[] colorsArray = {"Red", "Green", "Blue"};
    
            // 转换为集合
            List<String> colorsList = Arrays.asList(colorsArray);
    
            System.out.println("colorsList: " + colorsList);
        }
    }
    

需要注意的是,将数组转换为集合时,由于数组的大小是固定的,所以得到的集合也是固定大小的,不能进行添加或删除元素的操作。如果需要对集合进行增删操作,可以使用其他可变大小的集合实现类,如 ArrayListLinkedList

6.7 编写自己的算法

在 Java 中,可以通过编写自己的算法来处理集合数据。编写自定义算法可以让你更好地适应特定的需求,并实现对集合的特殊操作。下面是一个简单的示例,演示如何编写一个自定义算法来对整数列表进行累加:

import java.util.List;

public class CustomAlgorithm {
    // 自定义算法:对整数列表进行累加
    public static int sum(List<Integer> numbers) {
        int total = 0;
        for (Integer number : numbers) {
            total += number;
        }
        return total;
    }

    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);

        int result = sum(numbers);
        System.out.println("累加结果:" + result); // 输出:累加结果:15
    }
}

在这个示例中,我们定义了一个名为 sum 的静态方法,接受一个整数列表 numbers 作为输入,并对列表中的元素进行累加操作。通过自定义算法,我们可以方便地对集合数据进行定制化处理。

自定义算法的实现完全取决于需求和业务逻辑。你可以根据具体场景编写更复杂的算法,例如筛选符合条件的元素、查找特定元素、自定义排序等。对于复杂的算法,可以将其封装为一个类或者实现一个接口,以便更好地组织代码和复用算法逻辑。

7. 遗留的集合

7.1 Hashtable类

Hashtable 是 Java 中的一个遗留集合类,它实现了一个哈希表(hash table)数据结构。尽管它在早期版本的 Java 中广泛使用,但在现代 Java 中已经被更先进的集合类所取代,如 HashMapConcurrentHashMap。推荐在新的代码中使用更现代的集合类,因为它们提供了更好的性能和更丰富的功能。

7.2 枚举

在 Java 中,Enumeration 是一个遗留的接口,用于遍历集合中的元素。它位于 java.util 包中,被定义为一个简单的迭代器接口,用于在集合中获取元素并按顺序访问它们。虽然 Enumeration 在早期版本的 Java 中广泛使用,但自从 Java 1.2 引入了更强大的 Iterator 接口后,Enumeration 已经被 Iterator 取代,并且不再推荐在新的代码中使用。

7.3 属性映射

在 Java 中,属性映射(Property Map)是一种遗留的集合类接口,它用于存储一组属性及其对应的值。属性映射通常用于在 JavaBeans 中存储和获取属性值。虽然在早期版本的 Java 中广泛使用,但自从 Java 1.5 引入了更强大的 JavaBeans API,如 java.beans.PropertyDescriptorjava.beans.BeanInfo,属性映射已经被更灵活、功能更丰富的 JavaBeans API 取代,并且不再推荐在新的代码中使用。

在遗留的 JavaBeans 中,PropertyMap 接口是一个简单的接口,它没有在标准 Java SE 文档中定义,而是通常由不同的 JavaBeans 实现类自行实现。这个接口允许通过属性名访问属性值,并提供一些简单的方法来操作属性。

System.getProperty() 是 Java 中用于获取系统属性的方法。它是 System 类的静态方法,可以用于获取与当前 Java 虚拟机(JVM)相关的系统信息。

方法签名如下:

public static String getProperty(String key)

参数 key 是一个字符串,表示要获取的系统属性的名称。

返回值是一个字符串,表示指定系统属性的值。如果系统属性不存在或未定义,则返回 null

下面是一个简单的示例,演示如何使用 System.getProperty() 获取系统属性的值:

public class Main {
    public static void main(String[] args) {
        // 获取 Java 运行时版本
        String javaVersion = System.getProperty("java.version");
        System.out.println("Java Version: " + javaVersion);

        // 获取操作系统名称
        String osName = System.getProperty("os.name");
        System.out.println("Operating System: " + osName);

        // 获取用户的当前工作目录
        String userDir = System.getProperty("user.dir");
        System.out.println("User Directory: " + userDir);
    }
}

在这个示例中,我们分别获取了 Java 运行时版本、操作系统名称以及用户的当前工作目录等系统属性,并将它们输出到控制台。

System.getProperty() 可以帮助我们获取与系统相关的一些信息,例如操作系统信息、Java 版本、用户信息等。它在编写跨平台的 Java 程序时非常有用,因为它允许我们根据不同的系统环境来执行不同的操作。

7.4 栈

在 Java 中,java.util.Stack 是一种遗留的集合类,它是一个后进先出(Last In First Out,LIFO)的数据结构,类似于真实世界中的栈。栈可以用于在元素的集合中进行插入和删除操作,但只能访问或操作最近插入的元素。

Stack 类继承自 Vector 类,因此具有 Vector 类的所有方法,同时还提供了额外的栈操作方法,例如 push()pop()peek() 等。虽然 Stack 类在早期版本的 Java 中被广泛使用,但从 Java 1.5 开始,更推荐使用 java.util.Deque 接口的实现类,如 java.util.ArrayDeque 来代替 Stack 类。因为 Stack 继承自 Vector,而 Vector 在多线程环境下性能较低,而 Deque 的实现类 ArrayDeque 是线程安全的,性能更好。因此,在新的代码中,应该优先考虑使用 ArrayDeque 或其他 Deque 的实现类来实现栈的功能。

7.5 位集

java.util.BitSet 是一种遗留的集合类,用于表示一组位(bit)的数据结构。位集(BitSet)是一个固定长度的序列,其中每个位的值要么是 0(表示未设置)要么是 1(表示设置了)。它可以用来高效地存储和操作大量的布尔值,节省内存空间和提高性能。

BitSet 类位于 java.util 包中,继承自 java.util.AbstractSet 类,实现了 java.util.Cloneablejava.io.Serializable 接口。它提供了一系列方法来对位集进行操作,例如设置位、清除位、获取位值、求取位集的交集、并集等等。