Java 进阶--集合:告别数组的“僵硬”,拥抱灵活的数据容器

作者:IvanCodes

发布时间:2025年5月1日🫡

专栏:Java教程


大家好!👋 还记得我们上次聊的数组 (Array) 吗?它很基础,性能也不错,但有个致命的缺点长度一旦定了就不能改 <🔒>!想象一下,你用数组存购物车里的商品 🛒,刚开始定了装 10 件,结果顾客想买第 11 件… 咋办?重新创建一个更大的数组,再把旧的东西搬过去?太麻烦了!😫

为了解决数组这种“僵硬”的问题,以及提供更丰富的数据组织方式,Java 提供了一套强大的 集合框架 (Java Collections Framework, JCF)。你可以把它想象成一个超级工具箱 🧰,里面有各种大小可变功能各异容器 🧺,专门用来高效地存储和管理对象。今天,我们就来打开这个工具箱,看看里面有哪些宝贝!✨

一、为什么需要集合?它比数组强在哪? 🤔🚀

相比数组集合主要有这些优势

  1. 动态大小 <📏➡️<♾️>>:大部分集合的大小是可变的,你可以随时添加或删除元素,不用担心容量问题。
  2. 丰富的功能 <⚙️>: 集合框架提供了大量现成的方法来操作数据,比如添加、删除、查找、排序、迭代等,比数组方便得多。
  3. 不同的数据结构 <🗺️>: 集合框架提供了多种数据结构(如列表队列映射),每种都有不同的特点(如是否有序、是否允许重复),可以根据需求选择最合适的。
  4. 泛型支持 : 集合天生与泛型结合,保证了类型安全,避免了从集合中取出元素时的强制类型转换和潜在错误。

二、集合框架的核心:接口实现 <🧱>

Java 集合框架的设计非常优雅,它主要由一系列接口(定义规范)和实现类(提供具体的数据结构和算法)组成。我们写代码时,应该尽量面向接口编程,这样更灵活

核心接口主要有这几位大佬:

  1. Collection<E> <🧺>: 单列集合根接口,定义了所有单列集合(一次存一个元素)的基本操作,如 add(), remove(), contains(), size(), isEmpty(), iterator()。它派生出两个主要的子接口:ListSet
  2. List<E> <📝>: 有序集合(元素按插入顺序排列),允许存储重复的元素。可以把它想象成一个带编号的清单,可以根据索引(编号)精确访问。
  3. Set<E> <🚫>: 无序(通常不保证顺序)的集合不允许存储重复的元素。就像一个独特的邮票收藏册 ,每张邮票都是独一无二的。
  4. Queue<E> <🚶‍♀️➡️🚶‍♂️>: 队列接口,通常遵循先进先出(FIFO)的原则(也有例外,如优先队列)。就像排队买票 <🎫>,先来的先买。
  5. Map<K, V> <🔑➡️<🎁>>: 映射接口,存储的是键值对 (Key-Value Pair)。注意⚠️:Map 不直接继承 Collection 接口,它自成一派,但通常被认为是集合框架的一部分。Key 必须唯一,Value 可以重复。就像一本字典 📖,通过唯一的“单词”(Key)可以查到对应的“解释”(Value)。

接下来,我们详细看看最常用的 List, Set, Map 及其主要实现。

三、List 接口:有序可重复 <📝>

特点:存入和取出的顺序一致,可以包含相同的元素。

常用实现类

  • ArrayList<E> <🧱>:
    • 底层:基于动态数组实现。
    • 优点👍:查询(根据索引 get()速度快⚡️(随机访问快)。
    • 缺点👎:增删add() / remove(),尤其是在中间位置)速度相对较慢🐢,因为可能需要移动大量元素。
    • 适用场景🎯:查找多增删少的情况。最常用的 List 实现。
  • LinkedList<E> <🔗>:
    • 底层:基于双向链表实现。
    • 优点👍:增删(尤其是在首尾速度快🚀,不需要移动元素,只需修改指针。
    • 缺点👎:查询(根据索引 get()速度相对较慢🐌,需要从头或尾开始遍历链表。
    • 适用场景🎯:增删多查找少的情况。它也实现了 Queue 接口,常被用作队列或栈。

代码示例 (ArrayList):

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

public class ListDemo {
    public static void main(String[] args) {
        // 创建一个存储 String 的 ArrayList (面向接口编程)
        List<String> names = new ArrayList<>(); // new

        // 添加元素
        names.add("Alice"); // 索引 0
        names.add("Bob");   // 索引 1
        names.add("Charlie"); // 索引 2
        names.add("Bob"); // 允许重复

        System.out.println("Names list: " + names); // 输出保持添加顺序

        // 获取元素 (按索引)
        String secondName = names.get(1); // 获取索引为 1 的元素
        System.out.println("Second name: " + secondName); // Bob

        // 修改元素
        names.set(3, "David"); // 修改索引为 3 的元素
        System.out.println("After modification: " + names);

        // 删除元素
        names.remove(0); // 删除索引为 0 的元素
        System.out.println("After removing first element: " + names);

        // 获取大小
        System.out.println("Current size: " + names.size());

        // 遍历 (增强 for)
        System.out.print("Iterating through names: ");
        for(String name : names) {
            System.out.print(name + " ");
        }
        System.out.println();
    }
}

四、Set 接口:无序 (通常),不重复 <🚫>

特点:不能包含重复的元素。大部分实现不保证元素的存取顺序。

常用实现类

HashSet<E> <🧺><#️⃣>:

  • 底层:基于哈希表 (HashMap 实现)。
  • 优点👍:添加删除查找contains()速度非常快⚡️(平均 O(1))。
  • 缺点👎:不保证元素的顺序
  • 要求:存入 HashSet 的元素必须正确重写 hashCode()equals() 方法。
  • 适用场景🎯:需要快速去重快速判断元素是否存在,且不关心顺序最常用的 Set 实现。

代码示例 (HashSet):

import java.util.HashSet;
import java.util.Set;

public class HashSetDemo {
    public static void main(String[] args) {
        // 创建一个存储 Integer 的 HashSet (面向接口编程)
        Set<Integer> uniqueNumbers = new HashSet<>(); // new

        // 添加元素
        uniqueNumbers.add(5);
        uniqueNumbers.add(10);
        uniqueNumbers.add(5); // 尝试添加重复元素 5
        uniqueNumbers.add(15);
        uniqueNumbers.add(10); // 尝试添加重复元素 10

        // 输出 Set,重复元素自动被忽略,顺序不一定是添加顺序
        System.out.println("Unique numbers set: " + uniqueNumbers); // 可能输出 [5, 10, 15] 或其他顺序

        // 检查是否包含元素
        boolean hasTen = uniqueNumbers.contains(10);
        System.out.println("Set contains 10? " + hasTen); // true

        // 删除元素
        uniqueNumbers.remove(5);
        System.out.println("After removing 5: " + uniqueNumbers);

        System.out.println("Current size: " + uniqueNumbers.size()); // 2

        // 遍历 (顺序不保证)
        System.out.print("Iterating through the set: ");
        for(Integer num : uniqueNumbers) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

LinkedHashSet<E> <🔗><#️⃣>:

  • 底层:基于哈希表双向链表
  • 优点👍:既有 HashSet 的快速查找 (O(1)),又能保持元素的插入顺序 <➡️>!遍历时会按照添加的顺序输出。
  • 缺点👎:性能略低于 HashSet(因为维护链表的额外开销),内存占用也稍大。
  • 要求:同样需要元素正确重写 hashCode()equals()
  • 适用场景🎯:需要去重,同时希望保持元素添加时的顺序

代码示例 (LinkedHashSet):

import java.util.LinkedHashSet;
import java.util.Set;

public class LinkedHashSetDemo {
    public static void main(String[] args) {
        // 创建 LinkedHashSet,保持插入顺序
        Set<String> orderedUniqueNames = new LinkedHashSet<>(); // new

        orderedUniqueNames.add("Charlie");
        orderedUniqueNames.add("Alice");
        orderedUniqueNames.add("Bob");
        orderedUniqueNames.add("Alice"); // 重复的 Alice 被忽略

        // 输出时会保持添加顺序!
        System.out.println("Ordered unique names: " + orderedUniqueNames);
        // Output: Ordered unique names: [Charlie, Alice, Bob]
    }
}

TreeSet<E> <🌳>:

  • 底层:基于红黑树 (TreeMap 实现)。
  • 优点👍:元素会自动排序!可以按照元素的自然顺序(元素需实现 Comparable 接口)或者指定的比较器(创建 TreeSet 时传入 Comparator)进行排序。
  • 缺点👎:增删查的速度略慢HashSet(O(log n))。
  • 要求:存入 TreeSet 的元素必须可比较的
  • 适用场景🎯:需要去重的同时保持元素有序

代码示例 (TreeSet):

import java.util.Set;
import java.util.TreeSet;

public class TreeSetDemo {
    public static void main(String[] args) {
        // 创建 TreeSet,元素会自动排序
        Set<Integer> sortedNumbers = new TreeSet<>(); // new

        sortedNumbers.add(50);
        sortedNumbers.add(20);
        sortedNumbers.add(80);
        sortedNumbers.add(20); // 重复的 20 被忽略

        // 输出时元素是排序好的!
        System.out.println("Sorted unique numbers: " + sortedNumbers);
        // Output: Sorted unique numbers: [20, 50, 80]
    }
}

五、Map 接口:键值对存储 <🔑➡️<🎁>>

特点:存储的是 Key-Value 对,Key 必须唯一Value 可以重复。通过 Key 可以快速查找到对应的 Value。

常用实现类

HashMap<K, V> <🧺><#️⃣>:

  • 底层:基于哈希表

  • 优点👍:增删查(根据 Key)速度极快⚡️(平均 O(1))。

  • 缺点👎:不保证键值对的存储顺序

  • 要求:Key 必须正确重写 hashCode()equals()

  • 允许 Key 和 Value 为 null (Key 只能有一个 null )。

  • 适用场景🎯:需要快速 Key-Value 查找不关心顺序最常用

  • LinkedHashMap<K, V> <🔗><#️⃣>:

    • 底层:基于哈希表双向链表
    • 优点👍:既有 HashMap 的快速查找,又能保持键值对的插入顺序 <➡️>(或访问顺序,构造时可指定)。
    • 缺点👎:性能略低于 HashMap,内存占用稍大。
    • 要求:Key 同样需要 hashCode()equals()
    • 适用场景🎯:需要保持插入顺序映射关系,如实现LRU缓存(使用访问顺序)。
  • TreeMap<K, V> <🌳>:

    • 底层:基于红黑树
    • 优点👍:键值对会根据 Key 自动排序
    • 缺点👎:增删查速度略慢(O(log n))。
    • 要求:Key 必须可比较的
    • 不允许 Key 为 null
    • 适用场景🎯:需要按 Key 排序映射关系。
  • Hashtable<K, V> <🔒><#️⃣>:

    • 底层:也是基于哈希表
    • 特点线程安全!所有方法都是 synchronized 的。
    • 缺点👎:因为所有方法都加锁,并发性能很差 <🐢>。现在基本不推荐使用
    • 不允许 Key 或 Value 为 null <🚫>。
    • 替代方案💡:需要线程安全的 Map,优先考虑 <font color="purple">ConcurrentHashMap</font> (来自 java.util.concurrent 包),它提供了更好的并发性能

(HashMap 和 TreeMap 示例省略,见上一版本)

六、 集合工具类:Collections <🛠️>

别和 Collection 接口搞混了!Collections (带 s) 是一个工具类,里面全是 static 方法,用来方便地操作集合

一些常用方法

  • Collections.sort(List<T> list): 对 List 进行排序 <📊>。
  • Collections.reverse(List<?> list): 反转 List 中元素的顺序 <🔄>。
  • Collections.shuffle(List<?> list): 随机打乱 List 中元素的顺序 <🔀>。
  • Collections.max(Collection<? extends T> coll) / min(...): 找到集合中的最大/最小值 <🥇><🥉>。
  • Collections.frequency(Collection<?> c, Object o): 计算指定元素在集合中出现的次数 <🔢>。
  • Collections.synchronizedList/Set/Map(...): 返回指定集合线程安全版本 <🔒> (性能不如 java.util.concurrent 包下的类)。
  • Collections.unmodifiableList/Set/Map(...): 返回指定集合只读视图 <🚫>✍️。

代码示例:

import java.util.ArrayList;
import java.util.Collections; // 导入工具类
import java.util.List;

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

        System.out.println("Original list: " + numbers);

        // 排序
        Collections.sort(numbers);
        System.out.println("Sorted list: " + numbers);

        // 反转
        Collections.reverse(numbers);
        System.out.println("Reversed list: " + numbers);

        // 查找最大值
        Integer max = Collections.max(numbers);
        System.out.println("Maximum value: " + max);
    }
}

七、 别忘了迭代器 (Iterator) <🔍>

迭代器遍历所有 Collection (List, Set, Queue) 的标准、通用方式。虽然增强型 for 循环更简洁,但迭代器允许你在遍历过程中安全地删除元素(使用 iterator.remove())。

(迭代器代码示例省略,见上一版本)

八、总结:选择合适的容器 🏁

Java 集合框架提供了一套丰富、灵活、高效的数据容器。选择时主要考虑:

  • 是否有序? (List 有序, 大部分 Set/Map 无序, LinkedHashSet/LinkedHashMap 插入有序, TreeSet/TreeMap 排序)
  • 是否允许重复? (List 允许, Set 不允许)
  • 存储结构? (单个元素用 List/Set, 键值对用 Map)
  • 性能需求? (查找为主选 ArrayList/HashSet/HashMap, 增删为主选 LinkedList, 需要排序选 TreeSet/TreeMap)
  • 是否需要线程安全? (优先考虑 ConcurrentHashMapjava.util.concurrent 包下的类)

核心原则面向接口编程始终使用泛型!根据需求选择最合适实现类


九、练练手,检验成果!✏️🧠

检验一下学习成果吧!

⭐ 选择与应用 ⭐

  1. 如果你需要存储用户访问网站的页面顺序(URL 字符串),并且可能会频繁在列表开头添加新的访问记录(最新的访问总在最前面),你会选择 ArrayList<String> 还是 LinkedList<String>?为什么?
  2. 你需要存储一个班级所有学生的学号 (String),要求不能重复,并且希望能够快速检查某个学号是否已经存在,对顺序没有要求。选择哪个 Set 实现?
  3. 你想记录每个单词 (String) 在一篇文章中出现的次数 (Integer)。选择哪个 Map 实现最合适?
  4. 新增:如果第3题中,你希望输出单词及其次数时,单词能按照字母顺序排列,应该选择哪个 Map 实现?
  5. 新增:你需要一个线程安全Map 来存储缓存数据,应该优先选择哪个类?

⭐ 工具类与遍历 ⭐

  1. 创建一个 ArrayList<Integer>,添加一些数字,然后使用 Collections.sort() 对其排序,并使用 Collections.reverse() 将其反转,最后打印结果。
  2. 使用迭代器遍历一个 HashSet<String>,并在遍历过程中安全地删除所有长度小于 5 的字符串。

⭐ 概念辨析 ⭐

  1. CollectionCollections 有什么区别?
  2. 为什么 HashMap 的 Key 需要正确重写 hashCode()equals() 方法?如果只重写 equals() 而不重写 hashCode() 会有什么问题?
  3. 新增ArrayListVector 有什么主要区别?为什么现在很少推荐使用 Vector

十、参考答案 ✅💡

⭐ 选择与应用答案 ⭐

  1. 选择 LinkedList

    • 原因:需要在列表开头进行频繁的添加操作。LinkedList首部添加元素的时间复杂度是 O(1),非常。而 ArrayList 在开头添加元素需要将所有现有元素向后移动,时间复杂度是 O(n),效率
  2. 选择 HashSet<String>

    • 原因:需求是去重 (Set 特性),快速检查是否存在 (HashSet 提供 O(1) 的 contains() 操作),且不关心顺序HashSet 完美符合这些要求。
  3. 选择 HashMap<String, Integer>

    • 原因:需要存储单词 (Key) 到出现次数 (Value) 的映射关系 (Map 功能)。单词 (Key) 是唯一的。通常查找某个单词的次数(get(word))和更新次数(put(word, count + 1))需要HashMap 提供 O(1) 的平均性能。对单词的存储顺序通常不关心。
  4. 选择 TreeMap<String, Integer>

    • 原因TreeMap 会根据 Key (String) 的自然顺序(字母顺序)自动对键值对进行排序
  5. 优先选择 ConcurrentHashMap<K, V> (来自 java.util.concurrent 包)。

    • 原因ConcurrentHashMap 提供了高效线程安全机制(如分段锁或 CAS),并发性能远好于使用全局锁的 HashtableCollections.synchronizedMap()

⭐ 工具类与遍历答案 ⭐

  1. 排序与反转 List:

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.Arrays; // 方便初始化
    
    public class SortReverseDemo {
        public static void main(String[] args) {
            List<Integer> numbers = new ArrayList<>(Arrays.asList(5, 1, 8, 3, 2));
            System.out.println("Original list: " + numbers);
    
            Collections.sort(numbers); // 排序
            System.out.println("Sorted list:   " + numbers);
    
            Collections.reverse(numbers); // 反转
            System.out.println("Reversed list: " + numbers);
        }
    }
    
  2. 使用迭代器安全删除:

    import java.util.HashSet;
    import java.util.Iterator;
    import java.util.Set;
    
    public class IteratorRemoveDemo {
        public static void main(String[] args) {
            Set<String> words = new HashSet<>();
            words.add("Java");
            words.add("is");
            words.add("fun");
            words.add("and");
            words.add("powerful");
    
            System.out.println("Original set: " + words);
    
            // 必须使用迭代器进行遍历中的删除操作
            Iterator<String> iterator = words.iterator();
            while (iterator.hasNext()) {
                String word = iterator.next();
                if (word.length() < 5) {
                    // 安全删除当前元素
                    iterator.remove(); // 使用迭代器的 remove() 方法
                    System.out.println("Removed: " + word);
                }
            }
            // 不能在增强 for 循环中直接调用 words.remove(word),会抛 ConcurrentModificationException
    
            System.out.println("Set after removal: " + words);
        }
    }
    

⭐ 概念辨析答案 ⭐

  1. Collection vs Collections:

    • Collection: 是一个 接口 <📝>,单列集合
    • Collections: 是一个 工具类 <🛠️>,提供操作集合static方法。
  2. 为什么 HashMap Key 需要 hashCode()equals(): (答案同上一版本,此处省略)

  3. ArrayList vs Vector:

    • 主要区别: Vector线程安全的,它的所有方法都使用 synchronized 修饰ArrayList非线程安全的。此外,Vector 是 Java 早期 (JDK 1.0) 就存在的类,而 ArrayList 是在集合框架 (JDK 1.2) 中引入的。Vector默认扩容大小通常是翻倍,而 ArrayList 通常是增长 50%
    • 为什么少用 Vector: 因为其全局同步导致性能低下 <🐢>,在不需要线程安全的场景下完全没必要用它(用 ArrayList 更快)。而在需要线程安全的场景下,有性能更好的替代品,如使用 Collections.synchronizedList() 包装 ArrayList(虽然锁粒度仍然较大),或者直接使用 java.util.concurrent 包下的 CopyOnWriteArrayList(适用于读多写少的场景)。因此,Vector 现在基本被视为遗留类,在新代码中很少推荐使用

集合框架是 Java 中使用极其频繁的部分,掌握好它对日常开发至关重要!希望这篇笔记能帮你理清思路。如果觉得有帮助,别忘了 点赞👍、收藏⭐、关注 哦! 😉

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值