欢迎并且感谢大家指出我的问题,由于本人水平有限,有些内容写的不是很全面,只是把比较实用的东西给写下来,如果有写的不对的地方,还希望各路大牛多多指教!谢谢大家!🥰
一、引言
在 Java 编程的广袤领域中,集合是 Java 开发者不可或缺的强大工具。它位于 java.util 包下,为我们提供了一套统一、灵活且高效的 API,用于存储、操作和管理对象集合 ,极大地简化了我们处理数据的过程。
在实际开发中,我们常常会面临处理大量数据的情况。无论是简单的对象序列、不允许重复的元素集合,还是具有映射关系的键值对数据,集合框架都能提供相应的数据结构来满足需求。例如,在一个电商系统中,我们可以使用集合框架来存储商品信息、用户订单以及购物车中的商品等。它就像是一个功能强大的 “数据管家”,帮助我们有条不紊地管理和操作这些数据。
集合框架的重要性还体现在它提供了丰富的算法和操作方法,使得我们可以对集合中的元素进行排序、过滤、查找等操作。这些算法和方法不仅提高了开发效率,还降低了出错的概率。此外,集合框架全面支持泛型,确保了类型安全,避免了运行时因类型转换引发的错误。同时,集合接口与实现类的设计充分利用了面向对象的多态性,使得代码更具灵活性和可扩展性。在多线程环境下,集合框架也提供了线程安全的实现类,确保了数据的一致性和安全性。
接下来,让我们深入探索 Java 集合框架的奥秘,了解它的体系结构、常用接口和实现类,以及如何在实际开发中灵活运用它们。
二、集合框架基础
2.1 集合框架的定义与作用
集合框架是 Java 中为表示和操作集合而规定的统一标准体系结构,它包含了一系列接口、实现类以及操作集合的算法 。这个体系结构的设计,就像是精心搭建的一座大厦,各个部分各司其职,又紧密协作。接口部分如同大厦的蓝图,定义了集合的各种操作规范;实现类则是大厦的具体建筑材料,根据不同的需求提供了多样化的实现方式;而算法则像是大厦的维护工具,确保集合在各种操作下都能高效稳定地运行。
通过使用集合框架,我们可以避免重复造轮子,直接利用这些成熟的接口和实现类来处理数据集合。这大大提高了代码的复用性,减少了开发的工作量。同时,集合框架的灵活性也使得我们可以根据具体的业务需求选择最合适的数据结构和算法。比如,在需要频繁插入和删除元素的场景下,我们可以选择LinkedList;而在需要快速查找元素的场景下,HashMap则是更好的选择。这种灵活性使得我们的代码更加健壮和高效,能够更好地应对各种复杂的业务场景。
2.2 集合框架与数组的区别
数组和集合框架虽然都能用于存储数据,但它们在很多方面存在明显的差异。
- 长度特性:数组的长度在创建时就被固定下来,一旦确定,就无法在运行时动态改变。这就像是一个固定大小的容器,一旦装满,就无法再添加新的元素。例如,我们创建一个长度为 5 的整型数组int[] arr = new int[5];,它最多只能容纳 5 个元素,后续无法直接增加其容量。而集合框架中的大多数实现类,如ArrayList、LinkedList等,都具有动态扩容的能力。它们可以根据实际存储元素的数量自动调整内部容量,就像一个可以自动伸缩的容器,能够轻松应对不断变化的数据量。
- 元素类型:数组只能存储同一类型的元素,无论是基本数据类型还是引用数据类型。这是因为数组在内存中是连续存储的,为了保证内存访问的一致性和高效性,要求所有元素具有相同的类型。比如int[] numbers = {1, 2, 3, 4, 5};,这个数组只能存储int类型的数据。而集合框架则更加灵活,它可以存储多种不同类型的对象。因为集合在存储元素时,实际上是存储对象的引用,而不是对象本身,所以可以容纳不同类型的对象。例如,List<Object> list = new ArrayList<>();,这个集合可以同时存储String、Integer、自定义对象等各种类型的对象 。
- 操作方法:数组本身提供的操作方法相对较少,主要是通过索引来访问和修改元素。例如,arr[0] = 10;可以修改数组中索引为 0 的元素的值。对于其他复杂的操作,如排序、查找、添加和删除元素等,需要我们自己编写代码来实现。而集合框架则提供了丰富的方法来操作集合中的元素,如add()、remove()、contains()、sort()等。这些方法大大简化了我们对集合的操作,提高了开发效率。例如,list.add("Hello");可以向集合中添加一个元素,list.remove(0);可以移除集合中索引为 0 的元素。
三、集合框架体系结构
3.1 核心接口介绍
- Collection:作为所有集合接口的根接口,它定义了集合的基本操作,如添加元素(add(E e))、删除元素(remove(Object o))、判断集合是否为空(isEmpty())、获取集合大小(size())等。这些方法为后续的集合操作提供了基础,就像构建高楼大厦的基石一样,是整个集合框架的基础规范。例如,我们创建一个ArrayList集合,它实现了Collection接口,就可以使用这些基本方法来操作集合中的元素。
- List:是Collection的子接口,它代表有序可重复的集合 。在List集合中,每个元素都有其对应的索引位置,就像电影院里的座位一样,每个座位都有编号,我们可以根据这个编号(索引)来准确地访问和操作元素。List接口提供了一些额外的方法,如get(int index)用于获取指定索引位置的元素,add(int index, E element)用于在指定索引位置插入元素,remove(int index)用于移除指定索引位置的元素等。以ArrayList为例,我们可以通过索引快速地获取其中的元素,进行增删改查操作。
- Set:同样继承自Collection接口,它表示无序不可重复的集合 。在Set集合中,元素的存储顺序是不确定的,并且不允许有重复的元素存在。这就好比一个班级里的学生学号,每个学号都是唯一的,不能重复。Set集合主要用于需要确保元素唯一性的场景,比如统计一个班级里学生的唯一兴趣爱好集合。常见的Set实现类有HashSet、TreeSet等。HashSet基于哈希表实现,具有较高的查找和插入效率;TreeSet则基于红黑树实现,能够对元素进行排序。
- Queue:是Collection的子接口,用于表示队列数据结构,遵循先进先出(FIFO)的原则,即先进入队列的元素先被取出 。就像我们在排队买票,先排队的人先买到票离开队列。Queue接口定义了一些特殊的方法,如offer(E e)用于将元素添加到队列末尾,poll()用于移除并返回队列头部的元素,peek()用于返回队列头部的元素但不移除它等。Queue的常见实现类有PriorityQueue(优先队列)、LinkedList(既实现了List接口,也实现了Queue接口)等。PriorityQueue中的元素会按照自然顺序或自定义的比较器顺序进行排序,适用于需要根据元素优先级进行处理的场景。
- Map:它不是Collection的子接口,而是一个独立的接口,用于存储键值对(key - value pairs) 。每个键在Map中是唯一的,就像字典里的每个单词都对应着唯一的解释一样,通过键可以快速地查找对应的值。Map接口提供了如put(K key, V value)用于添加键值对,get(Object key)用于根据键获取对应的值,containsKey(Object key)用于判断是否包含指定的键等方法。常见的Map实现类有HashMap、TreeMap等。HashMap基于哈希表实现,具有快速的查找和插入性能;TreeMap基于红黑树实现,能够按键的自然顺序或自定义顺序对键值对进行排序。
3.2 接口之间的关系
集合框架的接口之间存在着清晰的继承关系,这种关系构成了集合框架的层次结构,使得我们能够更加清晰地理解和使用不同的集合接口。
Collection接口是整个集合框架的根接口,它就像是一棵大树的主干,其他集合接口都从它派生而来。List、Set和Queue接口都是Collection接口的子接口,它们继承了Collection接口的基本方法,并在此基础上根据各自的特点扩展了一些特定的方法。例如,List接口扩展了基于索引的操作方法,Set接口强调元素的唯一性,Queue接口定义了队列的操作方法。
Map接口与Collection接口处于同一层级,它们代表了不同的数据结构。Map接口用于存储键值对,而Collection接口及其子接口主要用于存储单个元素的集合。虽然Map接口与Collection接口没有直接的继承关系,但Map接口中的keySet()方法可以返回一个包含所有键的Set集合,values()方法可以返回一个包含所有值的Collection集合,这在一定程度上建立了它们之间的联系。
在Set接口中,SortedSet接口是Set的子接口,它表示有序的Set集合。TreeSet是SortedSet接口的一个常见实现类,它能够自动对集合中的元素进行排序。
同样,在Map接口中,SortedMap接口是Map的子接口,用于表示有序的Map集合。TreeMap是SortedMap接口的常见实现类,它可以按照键的自然顺序或自定义顺序对键值对进行排序。
通过下面的类图,我们可以更加直观地看到这些接口之间的继承关系:
@startuml
interface Collection {+add(E e) : boolean+remove(Object o) : boolean+isEmpty() : boolean+size() : int
}interface List extends Collection {+get(int index) : E+add(int index, E element) : void+remove(int index) : E
}interface Set extends Collection {// 没有额外方法,主要强调元素唯一性和无序性
}interface SortedSet extends Set {// 定义了一些与排序相关的方法,如comparator()等
}interface Queue extends Collection {+offer(E e) : boolean+poll() : E+peek() : E
}interface Map {+put(K key, V value) : V+get(Object key) : V+containsKey(Object key) : boolean
}interface SortedMap extends Map {// 定义了一些与排序相关的方法,如comparator()等
}Collection <|-- List
Collection <|-- Set
Set <|-- SortedSet
Collection <|-- Queue
Map <|-- SortedMap
@enduml
从这个类图中,我们可以清晰地看到集合框架中各个接口之间的层次关系,这有助于我们在实际开发中根据具体需求选择合适的接口和实现类。
3.3 主要实现类
- ArrayList:是List接口的典型实现类,它基于动态数组实现 。这意味着它在内存中以数组的形式存储元素,并且当数组容量不足时,会自动进行扩容。ArrayList的优点是支持快速的随机访问,因为可以通过索引直接定位到数组中的元素,就像在书架上根据编号快速找到某本书一样。例如,我们可以通过list.get(3)快速获取索引为 3 的元素。但ArrayList在进行插入和删除操作时效率较低,因为需要移动数组中的元素来调整位置。比如在数组中间插入一个元素,需要将插入位置后面的所有元素向后移动一位。ArrayList适用于需要频繁进行随机访问的场景,如统计学生成绩时,经常需要根据学生编号快速获取对应的成绩。
- LinkedList:也是List接口的实现类,它基于双向链表结构 。每个节点都包含了指向前一个节点和后一个节点的引用,这使得LinkedList在进行插入和删除操作时非常高效,因为只需要修改节点之间的引用关系,而不需要移动大量元素。例如,在链表的头部或中间插入一个元素,只需要修改几个节点的引用即可。但LinkedList的随机访问效率较低,因为需要从链表的头部或尾部开始遍历,直到找到目标元素,就像在一条长街上寻找某个门牌号,需要从街头或街尾一个一个地数过去。LinkedList适用于需要频繁进行插入和删除操作的场景,如在一个聊天记录列表中,经常需要在头部添加新的聊天消息。
- HashSet:是Set接口的常用实现类,基于哈希表实现 。它通过计算元素的哈希值来确定元素在哈希表中的存储位置,从而实现快速的插入和查找操作。例如,当我们向HashSet中添加一个元素时,它会先计算该元素的哈希值,然后根据哈希值找到对应的存储位置进行存储。在查找元素时,也会通过计算哈希值快速定位到可能的存储位置,然后再进行精确匹配。HashSet不保证元素的顺序,并且元素是不可重复的。如果添加重复的元素,HashSet会自动忽略。它适用于需要快速判断元素是否存在的场景,如在一个用户登录系统中,快速判断用户名是否已被注册。
- TreeSet:同样实现了Set接口,它基于红黑树实现 。红黑树是一种自平衡的二叉搜索树,这使得TreeSet中的元素会按照自然顺序或自定义的比较器顺序进行排序。例如,当我们向TreeSet中添加整数时,它们会自动从小到大排序;如果添加自定义对象,我们需要实现Comparable接口或提供一个Comparator比较器来定义排序规则。TreeSet不允许插入null值,并且元素不可重复。它适用于需要对元素进行排序并确保唯一性的场景,如在一个排行榜系统中,对用户的成绩进行排序并展示。
- HashMap:是Map接口的常见实现类,基于哈希表实现 。它通过键的哈希值来确定键值对在哈希表中的存储位置,从而实现快速的插入、删除和查找操作。例如,当我们调用map.put("key", "value")时,它会计算 "key" 的哈希值,然后将键值对存储到对应的位置。在获取值时,通过map.get("key"),同样先计算哈希值,快速定位到可能的位置,再进行精确匹配。HashMap允许键和值为null,但键必须唯一,如果插入相同键的键值对,新的值会覆盖旧的值。它适用于需要快速根据键查找值的场景,如在一个学生信息管理系统中,通过学生学号快速获取学生的详细信息。
- TreeMap:实现了Map接口,基于红黑树实现 。它会按照键的自然顺序或自定义的比较器顺序对键值对进行排序。例如,当键为字符串时,会按照字典序进行排序;如果是自定义对象作为键,需要实现Comparable接口或提供Comparator比较器。TreeMap不允许键为null,但值可以为null。它适用于需要对键进行排序并按顺序遍历键值对的场景,如在一个按时间顺序记录的日志系统中,使用时间作为键,日志内容作为值,方便按时间顺序查看日志。
四、List 接口及实现类
4.1 List 接口特点
List 接口是 Java 集合框架中非常重要的一员,它继承自Collection接口,具有独特的性质。其中,有序性是其显著特征之一,这意味着元素在 List 集合中的存储顺序与它们被添加的顺序一致。就像我们按顺序将书籍一本本放入书架,后续取书时也能按照放入的顺序找到。例如,当我们依次向List集合中添加元素 "apple"、"banana"、"cherry",那么在遍历集合时,也会按照这个顺序依次获取到这些元素。
可重复性也是 List 接口的一个重要特性,它允许集合中存在重复的元素 。这在很多实际场景中非常有用,比如统计学生的考试成绩,可能会有多个学生取得相同的分数,此时使用 List 集合就可以方便地存储这些成绩。
另外,List 接口提供了通过索引访问元素的能力,这使得我们可以像操作数组一样,根据元素的索引位置快速获取或修改元素。例如,通过list.get(2)可以获取到索引为 2 的元素,通过list.set(1, "newElement")可以将索引为 1 的元素修改为 "newElement" 。这种基于索引的操作方式,为我们在处理有序数据时提供了极大的便利,大大提高了数据访问的效率。
4.2 ArrayList
ArrayList 是 List 接口的一个典型实现类,它基于动态数组来实现数据的存储 。在 ArrayList 内部,维护着一个 Object 类型的数组elementData,用于存储集合中的元素。当我们创建一个 ArrayList 对象时,如果没有指定初始容量,它会默认创建一个容量为 10 的数组。随着元素的不断添加,当数组容量不足时,ArrayList 会自动进行扩容。
ArrayList 的扩容机制是将当前容量扩大为原来的 1.5 倍 。具体来说,当需要添加新元素时,ArrayList 会检查当前数组的容量是否足够。如果不足,就会创建一个新的更大容量的数组,然后将原数组中的元素复制到新数组中,最后将新元素添加到新数组中。这个过程虽然保证了 ArrayList 可以动态地存储更多元素,但由于涉及到数组的复制,在一定程度上会影响性能,尤其是在元素数量较多且频繁进行添加操作时。
ArrayList 的优点是查询速度快 ,这是因为它基于数组实现,支持随机访问。通过索引可以直接定位到数组中的元素,时间复杂度为 O (1),就像在一排编好号的邮箱中,根据编号可以快速找到对应的邮箱。例如,我们要获取 ArrayList 中索引为 5 的元素,只需要通过list.get(5)即可快速获取,效率非常高。
然而,ArrayList 在进行插入和删除操作时性能较差 。当在 ArrayList 的中间位置插入一个元素时,需要将插入位置后面的所有元素向后移动一位,以腾出空间插入新元素;删除元素时也类似,需要将删除位置后面的元素向前移动一位,以填补删除后的空位。这两个操作的时间复杂度都为 O (n),其中 n 是 ArrayList 中元素的数量。例如,在一个包含 100 个元素的 ArrayList 中,在索引为 10 的位置插入一个元素,就需要移动 90 个元素,操作效率较低。
ArrayList 提供了丰富的常用方法 ,如add(E e)用于在列表末尾添加元素;add(int index, E element)用于在指定位置插入元素;get(int index)用于获取指定位置的元素;remove(int index)用于移除指定位置的元素;remove(Object o)用于移除首次出现的指定元素;set(int index, E element)用于替换指定位置的元素等。下面是一个使用 ArrayList 的示例代码:
import java.util.ArrayList;
import java.util.List;public class ArrayListExample {public static void main(String[] args) {// 创建一个ArrayList对象List<String> list = new ArrayList<>();// 添加元素list.add("apple");list.add("banana");list.add("cherry");// 获取元素String element = list.get(1);System.out.println("获取索引为1的元素: " + element);// 修改元素list.set(1, "orange");System.out.println("修改后的列表: " + list);// 删除元素list.remove(2);System.out.println("删除后的列表: " + list);// 遍历列表for (String s : list) {System.out.println("遍历元素: " + s);}}
}
在这个示例中,我们首先创建了一个 ArrayList 对象,然后使用add方法添加了三个元素。接着,通过get方法获取索引为 1 的元素,使用set方法修改了索引为 1 的元素,再通过remove方法删除了索引为 2 的元素。最后,使用增强 for 循环遍历了整个列表,输出了每个元素。
4.3 LinkedList
LinkedList 是 List 接口的另一个重要实现类,它基于双向链表结构来存储数据 。在 LinkedList 中,每个节点(Node)都包含三个部分:存储的数据(item)、指向前一个节点的引用(prev)和指向后一个节点的引用(next)。这种双向链表的结构使得 LinkedList 在进行插入和删除操作时具有很高的效率。
当在 LinkedList 中插入一个元素时,只需要修改插入位置前后节点的引用关系即可 。例如,在链表的头部插入一个新节点,只需要将新节点的next引用指向原来的头节点,将原来头节点的prev引用指向新节点,然后将头节点指针指向新节点。在链表的中间或尾部插入元素的操作原理类似,时间复杂度都为 O (1),就像在一条链子上添加一个新的环,只需要连接好相邻的环即可。
同样,在 LinkedList 中删除一个元素时,也只需要修改相邻节点的引用关系 。例如,删除链表中的某个节点,只需要将该节点前一个节点的next引用指向该节点的后一个节点,将后一个节点的prev引用指向前一个节点,然后将该节点的引用置为null,以便垃圾回收器回收。这个过程也非常高效,时间复杂度同样为 O (1)。
然而,LinkedList 的查询性能较差 。由于它不是基于数组实现,不支持随机访问,当需要获取 LinkedList 中某个指定位置的元素时,需要从链表的头部或尾部开始,逐个遍历节点,直到找到目标元素。这个过程的时间复杂度为 O (n),其中 n 是链表中节点的数量。例如,要获取一个包含 100 个节点的 LinkedList 中索引为 50 的元素,就需要从头部开始遍历 50 个节点,效率相对较低。
LinkedList 实现了List接口和Deque接口 ,这使得它不仅可以作为普通的列表使用,还可以作为堆栈、队列或双端队列使用。作为堆栈时,可以使用push方法将元素压入栈顶,使用pop方法从栈顶弹出元素;作为队列时,可以使用offer方法将元素添加到队列末尾,使用poll方法从队列头部移除元素;作为双端队列时,可以使用addFirst和addLast方法在两端添加元素,使用removeFirst和removeLast方法在两端移除元素。下面是一个使用 LinkedList 作为堆栈和队列的示例代码:
import java.util.LinkedList;public class LinkedListExample {public static void main(String[] args) {// 使用LinkedList作为堆栈LinkedList<Integer> stack = new LinkedList<>();stack.push(10);stack.push(20);stack.push(30);System.out.println("从堆栈弹出元素: " + stack.pop());// 使用LinkedList作为队列LinkedList<Integer> queue = new LinkedList<>();queue.offer(10);queue.offer(20);queue.offer(30);System.out.println("从队列移除元素: " + queue.poll());}
}
在这个示例中,我们首先创建了一个 LinkedList 对象并将其作为堆栈使用,通过push方法将三个元素压入栈顶,然后使用pop方法从栈顶弹出一个元素。接着,我们又创建了一个 LinkedList 对象并将其作为队列使用,通过offer方法将三个元素添加到队列末尾,然后使用poll方法从队列头部移除一个元素。
4.4 Vector
Vector 也是 List 接口的一个实现类,它与 ArrayList 有很多相似之处 。Vector 同样基于动态数组实现,并且支持通过索引访问元素,元素的存储顺序与添加顺序一致,也允许元素重复。
然而,Vector 与 ArrayList 存在一些关键的区别 。其中最显著的区别是线程安全性。Vector 是线程安全的,它的所有公共方法都使用了synchronized关键字进行同步,这意味着在多线程环境下,多个线程可以安全地访问和修改 Vector 对象,而不会出现数据不一致的问题。例如,在一个多线程的银行账户管理系统中,如果使用 Vector 来存储账户交易记录,多个线程同时进行存款和取款操作时,Vector 能够保证交易记录的完整性和一致性。
相比之下,ArrayList 是非线程安全的 。在多线程环境下,如果多个线程同时对 ArrayList 进行修改操作(如添加、删除元素),可能会导致数据不一致或其他并发问题。例如,一个线程正在读取 ArrayList 中的元素,而另一个线程同时在删除该 ArrayList 中的元素,就可能会出现读取到错误数据或抛出异常的情况。
由于 Vector 的方法是同步的,这在一定程度上会影响其性能 。在单线程环境下,ArrayList 的性能通常比 Vector 更好,因为它不需要进行同步操作,减少了额外的开销。另外,Vector 的扩容机制与 ArrayList 也有所不同。当 Vector 的容量不足时,默认情况下它会将容量扩大为原来的 2 倍 ,而 ArrayList 是扩大为原来的 1.5 倍。这可能导致 Vector 在某些情况下比 ArrayList 更浪费内存,但也减少了扩容的频率。下面是一个使用 Vector 的示例代码:
import java.util.Vector;public class VectorExample {public static void main(String[] args) {// 创建一个Vector对象Vector<String> vector = new Vector<>();// 添加元素vector.add("apple");vector.add("banana");vector.add("cherry");// 获取元素String element = vector.get(1);System.out.println("获取索引为1的元素: " + element);// 修改元素vector.set(1, "orange");System.out.println("修改后的Vector: " + vector);// 删除元素vector.remove(2);System.out.println("删除后的Vector: " + vector);// 遍历Vectorfor (String s : vector) {System.out.println("遍历元素: " + s);}}
}
在这个示例中,我们创建了一个 Vector 对象,并进行了添加、获取、修改、删除和遍历元素的操作,其使用方式与 ArrayList 类似,但由于其线程安全性,更适合在多线程环境中使用。
4.5 案例演示
为了更直观地展示 List 接口及其实现类的使用,我们以一个简单的学生成绩管理系统为例。在这个系统中,我们需要存储学生的姓名和成绩,并实现添加学生、查询学生成绩、修改学生成绩等功能。
首先,我们定义一个Student类来表示学生,包含姓名和成绩两个属性:
public class Student {private String name;private int score;public Student(String name, int score) {this.name = name;this.score = score;}public String getName() {return name;}public int getScore() {return score;}public void setScore(int score) {this.score = score;}
}
然后,我们使用ArrayList来实现学生成绩管理系统的功能:
import java.util.ArrayList;
import java.util.List;public class StudentGradeManagement {private List<Student> studentList;public StudentGradeManagement() {studentList = new ArrayList<>();}// 添加学生public void addStudent(Student student) {studentList.add(student);}// 查询学生成绩public int queryStudentScore(String name) {for (Student student : studentList) {if (student.getName().equals(name)) {return student.getScore();}}return -1; // 未找到学生}// 修改学生成绩public void updateStudentScore(String name, int newScore) {for (Student student : studentList) {if (student.getName().equals(name)) {student.setScore(newScore);return;}}System.out.println("未找到学生: " + name);}// 显示所有学生信息public void displayAllStudents() {for (Student student : studentList) {System.out.println("姓名: " + student.getName() + ", 成绩: " + student.getScore());}}public static void main(String[] args) {StudentGradeManagement management = new StudentGradeManagement();// 添加学生management.addStudent(new Student("张三", 85));management.addStudent(new Student("李四", 90));management.addStudent(new Student("王五", 78));// 显示所有学生信息System.out.println("所有学生信息:");management.displayAllStudents();// 查询学生成绩int score = management.queryStudentScore("李四");if (score!= -1) {System.out.println("李四的成绩是: " + score);} else {System.out.println("未找到李四的成绩");}// 修改学生成绩management.updateStudentScore("王五", 82);System.out.println("修改后的所有学生信息:");management.displayAllStudents();}
}
在这个案例中,我们首先定义了Student类,然后在StudentGradeManagement类中使用ArrayList来存储学生对象。通过addStudent方法添加学生,queryStudentScore方法查询学生成绩,updateStudentScore方法修改学生成绩,displayAllStudents方法显示所有学生信息。在main方法中,我们创建了StudentGradeManagement对象,并进行了一系列的操作,展示了如何使用ArrayList来实现一个简单的学生成绩管理系统。如果在多线程环境下,我们可以将ArrayList替换为Vector,以确保数据的线程安全性。
五、Set 接口及实现类
5.1 Set 接口特点
Set 接口是 Java 集合框架中的重要成员,它继承自Collection接口,用于存储不重复的元素集合,具有无序性和不可重复性这两个显著特点。
无序性是指 Set 集合中元素的存储顺序与它们被添加的顺序无关 。这是因为 Set 接口的实现类通常是基于哈希表或树结构来存储元素的,它们在存储元素时会根据元素的哈希值或其他特性来确定元素的存储位置,而不是按照添加的顺序依次存储。例如,当我们向HashSet中依次添加元素 "apple"、"banana"、"cherry",在遍历HashSet时,获取元素的顺序可能与添加顺序不同,可能是 "cherry"、"apple"、"banana",这体现了其无序性。
不可重复性是 Set 接口的另一个关键特性 。Set 集合中不允许出现重复的元素,这是通过元素的equals()方法和hashCode()方法来保证的。当向 Set 集合中添加一个元素时,会先计算该元素的哈希值,然后根据哈希值在集合中查找是否已经存在相同哈希值的元素。如果存在,再通过equals()方法比较两个元素是否相等。如果相等,则认为是重复元素,不会添加到集合中;如果不相等,则会将新元素添加到集合中。例如,我们向HashSet中添加两个相同的字符串 "apple",最终HashSet中只会存储一个 "apple" 元素,确保了元素的唯一性。
5.2 HashSet
HashSet 是 Set 接口的一个常用实现类,它基于哈希表来实现数据的存储 。在 HashSet 内部,维护着一个哈希表,通过计算元素的哈希值来确定元素在哈希表中的存储位置,从而实现快速的插入、删除和查找操作。
当我们向 HashSet 中添加一个元素时 ,它会首先调用该元素的hashCode()方法计算出哈希值,然后根据这个哈希值通过特定的算法(如取模运算)确定该元素在哈希表中的存储位置。如果该位置为空,就直接将元素存储在该位置;如果该位置已经有元素存在,就会发生哈希冲突。在这种情况下,HashSet 会使用链地址法来解决哈希冲突,即将冲突的元素存储在一个链表中,该链表挂在哈希表的同一个位置上。例如,有两个元素 A 和 B,它们的哈希值相同,计算出的存储位置也相同,那么 A 和 B 就会被存储在同一个链表中。
在删除元素时 ,HashSet 同样会先计算要删除元素的哈希值,找到对应的存储位置,然后在链表中查找要删除的元素。如果找到,就将其从链表中移除;如果链表中只有一个元素,那么直接将该位置置为空。
在查找元素时 ,HashSet 也是先计算元素的哈希值,找到对应的存储位置,然后在链表中查找该元素。由于哈希表的特性,在理想情况下,HashSet 的插入、删除和查找操作的平均时间复杂度都为 O (1),这使得它在处理大量数据时具有很高的效率。
需要注意的是,HashSet 允许存储一个null元素 。同时,由于它是基于哈希表实现的,不保证元素的顺序,即元素的遍历顺序与添加顺序可能不同。下面是一个使用 HashSet 的示例代码:
import java.util.HashSet;
import java.util.Set;public class HashSetExample {public static void main(String[] args) {// 创建一个HashSet对象Set<String> set = new HashSet<>();// 添加元素set.add("apple");set.add("banana");set.add("cherry");set.add("apple"); // 重复元素,不会添加成功// 输出集合大小System.out.println("集合大小: " + set.size());// 遍历集合for (String element : set) {System.out.println("遍历元素: " + element);}// 判断集合是否包含某个元素boolean contains = set.contains("banana");System.out.println("集合是否包含banana: " + contains);// 删除元素set.remove("cherry");System.out.println("删除cherry后的集合: " + set);}
}
在这个示例中,我们创建了一个 HashSet 对象,并向其中添加了几个元素,包括一个重复元素 "apple"。通过size()方法获取集合大小,使用增强 for 循环遍历集合,通过contains()方法判断集合是否包含某个元素,最后通过remove()方法删除一个元素,并输出删除后的集合。
5.3 TreeSet
TreeSet 是 Set 接口的另一个重要实现类,它基于红黑树(一种自平衡的二叉搜索树)来实现 。在 TreeSet 中,每个元素都作为红黑树的一个节点存储,并且元素会按照自然顺序或自定义的比较器顺序进行排序。
当我们向 TreeSet 中添加一个元素时 ,它会根据元素的自然顺序或自定义的比较器来确定该元素在红黑树中的插入位置。如果元素实现了Comparable接口,那么会按照Comparable接口中定义的比较规则进行排序;如果没有实现Comparable接口,在创建 TreeSet 时可以传入一个Comparator比较器来定义排序规则。例如,当我们向 TreeSet 中添加整数时,它们会自动从小到大排序;如果添加自定义对象,我们需要实现Comparable接口或提供一个Comparator比较器来定义排序规则。
在删除元素时 ,TreeSet 会在红黑树中找到要删除的元素节点,然后按照红黑树的删除规则进行删除操作。删除操作可能会导致红黑树的结构调整,以保持其自平衡特性。
在查找元素时 ,TreeSet 会利用红黑树的特性进行高效的查找。由于红黑树是一种平衡的二叉搜索树,其查找、插入和删除操作的时间复杂度都为 O (log n),其中 n 是 TreeSet 中元素的数量。这使得 TreeSet 在处理大量有序数据时具有较好的性能。
需要注意的是,TreeSet 不允许插入null值 ,因为null值无法参与比较,无法确定其在红黑树中的位置。下面是一个使用 TreeSet 的示例代码:
import java.util.Set;
import java.util.TreeSet;public class TreeSetExample {public static void main(String[] args) {// 创建一个TreeSet对象Set<Integer> set = new TreeSet<>();// 添加元素set.add(3);set.add(1);set.add(2);// 输出集合大小System.out.println("集合大小: " + set.size());// 遍历集合,会按从小到大顺序输出for (Integer element : set) {System.out.println("遍历元素: " + element);}// 判断集合是否包含某个元素boolean contains = set.contains(2);System.out.println("集合是否包含2: " + contains);// 删除元素set.remove(1);System.out.println("删除1后的集合: " + set);}
}
在这个示例中,我们创建了一个 TreeSet 对象,并向其中添加了几个整数。通过size()方法获取集合大小,使用增强 for 循环遍历集合,会发现元素是按从小到大的顺序输出的。通过contains()方法判断集合是否包含某个元素,最后通过remove()方法删除一个元素,并输出删除后的集合。
5.4 LinkedHashSet
LinkedHashSet 是 HashSet 的子类,它继承了 HashSet 的特性,同时还维护了元素的插入顺序 。在 LinkedHashSet 内部,除了使用哈希表来存储元素外,还使用了一个双向链表来记录元素的插入顺序。
当我们向 LinkedHashSet 中添加一个元素时 ,它首先会调用父类 HashSet 的add()方法将元素添加到哈希表中,同时将该元素添加到双向链表的末尾。这样,在遍历 LinkedHashSet 时,元素的顺序就与它们被添加的顺序一致。
在删除元素时 ,LinkedHashSet 会先在哈希表中找到要删除的元素,将其从哈希表中移除,同时也会将其从双向链表中移除。
在查找元素时 ,LinkedHashSet 同样会先在哈希表中查找,找到后再通过双向链表来确定其在链表中的位置。由于它需要维护双向链表,所以在性能上略低于 HashSet,但在需要保持元素插入顺序的场景下,LinkedHashSet 非常有用。
LinkedHashSet 允许存储null元素 ,并且在插入null元素时,也会按照插入顺序将其添加到双向链表中。下面是一个使用 LinkedHashSet 的示例代码:
import java.util.LinkedHashSet;
import java.util.Set;public class LinkedHashSetExample {public static void main(String[] args) {// 创建一个LinkedHashSet对象Set<String> set = new LinkedHashSet<>();// 添加元素set.add("apple");set.add("banana");set.add("cherry");// 输出集合大小System.out.println("集合大小: " + set.size());// 遍历集合,会按添加顺序输出for (String element : set) {System.out.println("遍历元素: " + element);}// 判断集合是否包含某个元素boolean contains = set.contains("banana");System.out.println("集合是否包含banana: " + contains);// 删除元素set.remove("cherry");System.out.println("删除cherry后的集合: " + set);}
}
在这个示例中,我们创建了一个 LinkedHashSet 对象,并向其中添加了几个元素。通过size()方法获取集合大小,使用增强 for 循环遍历集合,会发现元素是按照添加的顺序输出的。通过contains()方法判断集合是否包含某个元素,最后通过remove()方法删除一个元素,并输出删除后的集合。
5.5 案例演示
为了更好地展示 Set 接口及其实现类的使用,我们以一个简单的用户 ID 去重场景为例。假设我们从数据库中获取了一批用户 ID,其中可能存在重复的 ID,我们需要对这些 ID 进行去重处理。
import java.util.HashSet;
import java.util.Set;public class UserIdDuplicationRemoval {public static void main(String[] args) {// 模拟从数据库获取的用户ID列表String[] userIds = {"1001", "1002", "1003", "1001", "1004", "1002"};// 使用HashSet进行去重Set<String> uniqueIds = new HashSet<>();for (String userId : userIds) {uniqueIds.add(userId);}// 输出去重后的用户IDSystem.out.println("去重后的用户ID:");for (String id : uniqueIds) {System.out.println(id);}}
}
在这个案例中,我们首先定义了一个包含重复用户 ID 的字符串数组userIds。然后创建了一个HashSet对象uniqueIds,利用HashSet的不可重复性,通过循环将userIds中的每个 ID 添加到uniqueIds中。由于HashSet会自动忽略重复的元素,所以最终uniqueIds中只包含唯一的用户 ID。最后,我们通过循环遍历uniqueIds,输出去重后的用户 ID。
如果我们需要保持用户 ID 的插入顺序,可以使用LinkedHashSet来代替HashSet,代码修改如下:
import java.util.LinkedHashSet;
import java.util.Set;public class UserIdDuplicationRemoval {public static void main(String[] args) {// 模拟从数据库获取的用户ID列表String[] userIds = {"1001", "1002", "1003", "1001", "1004", "1002"};// 使用LinkedHashSet进行去重并保持顺序Set<String> uniqueIds = new LinkedHashSet<>();for (String userId : userIds) {uniqueIds.add(userId);}// 输出去重后的用户IDSystem.out.println("去重后的用户ID:");for (String id : uniqueIds) {System.out.println(id);}}
}
在这个修改后的代码中,我们创建了一个LinkedHashSet对象uniqueIds,它会在去重的同时保持用户 ID 的插入顺序,输出的结果将按照用户 ID 在userIds数组中的顺序排列。
六、Map 接口及实现类
6.1 Map 接口特点
Map 接口是 Java 集合框架中的重要组成部分,它与Collection接口处于同一层级,但代表了不同的数据结构。Map 接口用于存储键值对(key - value pairs),每个键在 Map 中是唯一的,就像每个人都有唯一的身份证号码一样,通过这个唯一的键可以快速地查找对应的值。例如,在一个学生信息管理系统中,我们可以使用学生的学号作为键,学生的姓名、年龄、成绩等信息作为值,通过学号就能快速获取该学生的所有信息。
在 Map 中,键的唯一性是通过键的equals()方法和hashCode()方法来保证的 。当向 Map 中添加一个键值对时,如果已经存在相同的键(通过equals()方法判断),则新的值会覆盖旧的值。而值是可以重复的,就像不同的学生可能有相同的年龄一样。例如,一个 Map 中可以存在多个值为 "20" 的键值对,只要它们的键不同即可。
Map 接口提供了丰富的方法来操作键值对 ,如put(K key, V value)方法用于添加键值对,如果键已存在,则更新其值;get(Object key)方法用于根据键获取对应的值,如果键不存在,则返回null;containsKey(Object key)方法用于判断 Map 中是否包含指定的键;keySet()方法返回一个包含所有键的Set集合,通过这个集合可以遍历 Map 中的所有键;values()方法返回一个包含所有值的Collection集合,用于遍历 Map 中的所有值;entrySet()方法返回一个包含所有键值对的Set集合,每个元素都是一个Map.Entry对象,通过这个集合可以同时访问键和值 。
6.2 HashMap
HashMap 是 Map 接口的一个常见且重要的实现类,它基于哈希表来实现数据的存储 。哈希表是一种根据键的哈希值直接进行访问的数据结构,通过将键映射到哈希表中的一个位置来快速定位对应的值,就像在图书馆中根据书籍的编号快速找到对应的书籍一样。
在 HashMap 内部,维护着一个数组和链表(或红黑树,在 Java 8 及之后版本,当链表长度超过阈值时会转换为红黑树)组成的数据结构 。当我们向 HashMap 中添加一个键值对时,首先会根据键的hashCode()方法计算出哈希值,然后通过哈希值与数组长度进行取模运算,得到该键值对在数组中的索引位置。如果该位置为空,就直接将键值对存储在该位置;如果该位置已经存在键值对,就会发生哈希冲突。
HashMap 使用链地址法来解决哈希冲突 ,即将冲突的键值对以链表的形式存储在同一个数组位置上。在 Java 8 及之后版本,如果链表长度达到一定阈值(默认为 8),链表会转换为红黑树,以提高查找性能。例如,有两个键 A 和 B,它们的哈希值相同,计算出的数组索引位置也相同,那么它们会被存储在同一个链表中。在查找键 A 的值时,先通过哈希值找到对应的数组位置,然后在链表中遍历,通过equals()方法比较键,找到键 A 对应的键值对,从而获取其值。
HashMap 的优点是查找、插入和删除操作的平均时间复杂度都接近 O (1) ,这使得它在处理大量数据时具有很高的效率。它允许键和值为null,但键必须唯一,如果插入相同键的键值对,新的值会覆盖旧的值。需要注意的是,HashMap 不保证键值对的顺序,即元素的遍历顺序与插入顺序可能不同。下面是一个使用 HashMap 的示例代码:
import java.util.HashMap;
import java.util.Map;public class HashMapExample {public static void main(String[] args) {// 创建一个HashMap对象Map<String, Integer> map = new HashMap<>();// 添加键值对map.put("apple", 10);map.put("banana", 20);map.put("cherry", 30);// 获取值Integer value = map.get("banana");System.out.println("banana的值是: " + value);// 修改值map.put("banana", 25);System.out.println("修改后的map: " + map);// 判断是否包含某个键boolean containsKey = map.containsKey("cherry");System.out.println("map是否包含cherry键: " + containsKey);// 删除键值对map.remove("apple");System.out.println("删除apple后的map: " + map);// 遍历HashMapfor (Map.Entry<String, Integer> entry : map.entrySet()) {System.out.println("键: " + entry.getKey() + ", 值: " + entry.getValue());}}
}
在这个示例中,我们创建了一个 HashMap 对象,并向其中添加了三个键值对。通过get方法获取了 "banana" 键对应的值,使用put方法修改了 "banana" 键对应的值,通过containsKey方法判断是否包含 "cherry" 键,使用remove方法删除了 "apple" 键值对。最后,通过entrySet方法遍历了整个 HashMap,输出了每个键值对。
6.3 TreeMap
TreeMap 是 Map 接口的另一个重要实现类,它基于红黑树(一种自平衡的二叉搜索树)来实现数据的存储和操作 。在 TreeMap 中,每个键值对都作为红黑树的一个节点存储,并且元素会按照键的自然顺序或自定义的比较器顺序进行排序。
当我们向 TreeMap 中添加一个键值对时 ,它会根据键的自然顺序或自定义的比较器来确定该键值对在红黑树中的插入位置。如果键实现了Comparable接口,那么会按照Comparable接口中定义的比较规则进行排序;如果没有实现Comparable接口,在创建 TreeMap 时可以传入一个Comparator比较器来定义排序规则。例如,当键为字符串时,会按照字典序进行排序;如果是自定义对象作为键,需要实现Comparable接口或提供Comparator比较器。
在删除元素时 ,TreeMap 会在红黑树中找到要删除的元素节点,然后按照红黑树的删除规则进行删除操作。删除操作可能会导致红黑树的结构调整,以保持其自平衡特性。
在查找元素时 ,TreeMap 会利用红黑树的特性进行高效的查找。由于红黑树是一种平衡的二叉搜索树,其查找、插入和删除操作的时间复杂度都为 O (log n),其中 n 是 TreeMap 中元素的数量。这使得 TreeMap 在处理大量有序数据时具有较好的性能。
需要注意的是,TreeMap 不允许键为null,但值可以为null 。下面是一个使用 TreeMap 的示例代码:
import java.util.Comparator;
import java.util.TreeMap;public class TreeMapExample {public static void main(String[] args) {// 创建一个TreeMap对象,使用自然顺序TreeMap<String, Integer> treeMap1 = new TreeMap<>();treeMap1.put("banana", 20);treeMap1.put("apple", 10);treeMap1.put("cherry", 30);System.out.println("使用自然顺序的TreeMap: " + treeMap1);// 创建一个TreeMap对象,使用自定义比较器TreeMap<String, Integer> treeMap2 = new TreeMap<>(new Comparator<String>() {@Overridepublic int compare(String o1, String o2) {return o2.compareTo(o1);}});treeMap2.put("banana", 20);treeMap2.put("apple", 10);treeMap2.put("cherry", 30);System.out.println("使用自定义比较器的TreeMap: " + treeMap2);// 获取键最小的键值对System.out.println("最小键的键值对: " + treeMap1.firstEntry());// 获取键最大的键值对System.out.println("最大键的键值对: " + treeMap1.lastEntry());// 获取小于等于给定键的最大键值对System.out.println("小于等于cherry的最大键值对: " + treeMap1.floorEntry("cherry"));// 获取大于等于给定键的最小键值对System.out.println("大于等于banana的最小键值对: " + treeMap1.ceilingEntry("banana"));// 删除指定键的键值对treeMap1.remove("apple");System.out.println("删除apple后的TreeMap: " + treeMap1);// 判断TreeMap是否包含指定键boolean containsKey = treeMap1.containsKey("banana");System.out.println("TreeMap是否包含banana键: " + containsKey);}
}
在这个示例中,我们首先创建了一个使用自然顺序的 TreeMap 对象treeMap1,并添加了三个键值对,输出时会发现键是按照字典序排列的。然后创建了一个使用自定义比较器的 TreeMap 对象treeMap2,该比较器使键按照逆序排列。接着,我们使用firstEntry方法获取最小键的键值对,lastEntry方法获取最大键的键值对,floorEntry方法获取小于等于给定键的最大键值对,ceilingEntry方法获取大于等于给定键的最小键值对。最后,使用remove方法删除了 "apple" 键值对,通过containsKey方法判断是否包含 "banana" 键。
6.4 LinkedHashMap
LinkedHashMap 是 HashMap 的子类,它继承了 HashMap 的特性,同时还维护了键值对的插入顺序或访问顺序 。在 LinkedHashMap 内部,除了使用哈希表来存储键值对,还使用了一个双向链表来记录键值对的顺序。
当我们向 LinkedHashMap 中添加一个键值对时 ,它首先会调用父类 HashMap 的put()方法将键值对添加到哈希表中,同时将该键值对添加到双向链表的末尾。这样,在遍历 LinkedHashMap 时,键值对的顺序就与它们被添加的顺序一致。
在删除元素时 ,LinkedHashMap 会先在哈希表中找到要删除的键值对,将其从哈希表中移除,同时也会将其从双向链表中移除。
在查找元素时 ,LinkedHashMap 同样会先在哈希表中查找,找到后再通过双向链表来确定其在链表中的位置。由于它需要维护双向链表,所以在性能上略低于 HashMap,但在需要保持元素顺序的场景下,LinkedHashMap 非常有用。
LinkedHashMap 还提供了一个可选的模式,即 “访问顺序模式” 。在这种模式下,每当访问一个键值对(即调用get或put时获取已存在的键值对),都会将该键值对移动到双向链表的末尾。这意味着最近访问的键值对会排在最后,从而形成一个按访问顺序排列的 LinkedHashMap。要启用访问顺序模式,只需在构造 LinkedHashMap 时将accessOrder设置为true,例如LinkedHashMap<K, V> map = new LinkedHashMap<>(16, 0.75f, true); 。默认情况下,accessOrder为false,表示 LinkedHashMap 按插入顺序排列。
下面是一个使用 LinkedHashMap 的示例代码:
import java.util.LinkedHashMap;
import java.util.Map;public class LinkedHashMapExample {public static void main(String[] args) {// 创建一个按插入顺序排列的LinkedHashMap对象LinkedHashMap<String, Integer> linkedHashMap1 = new LinkedHashMap<>();linkedHashMap1.put("apple", 10);linkedHashMap1.put("banana", 20);linkedHashMap1.put("cherry", 30);System.out.println("按插入顺序排列的LinkedHashMap: " + linkedHashMap1);// 创建一个按访问顺序排列的LinkedHashMap对象LinkedHashMap<String, Integer> linkedHashMap2 = new LinkedHashMap<>(16, 0.75f, true);linkedHashMap2.put("apple", 10);linkedHashMap2.put("banana", 20);linkedHashMap2.put("cherry", 30);// 访问元素linkedHashMap2.get("banana");linkedHashMap2.put("apple", 15);System.out.println("按访问顺序排列的LinkedHashMap: " + linkedHashMap2);// 获取值Integer value = linkedHashMap1.get("cherry");System.out.println("cherry的值是: " + value);// 修改值linkedHashMap1.put("banana", 25);System.out.println("修改后的linkedHashMap1: " + linkedHashMap1);// 判断是否包含某个键boolean containsKey = linkedHashMap1.containsKey("apple");System.out.println("linkedHashMap1是否包含apple键: " + containsKey);// 删除键值对linkedHashMap1.remove("banana");System.out.println("删除banana后的linkedHashMap1: " + linkedHashMap1);// 遍历LinkedHashMapfor (Map.Entry<String, Integer> entry : linkedHashMap1.entrySet()) {System.out.println("键: " + entry.getKey() + ", 值: " + entry.getValue());}}
}
在这个示例中,我们首先创建了一个按插入顺序排列的 LinkedHashMap 对象linkedHashMap1,并添加了三个键值对,输出时会发现键值对是按照添加的顺序排列的。然后创建了一个按访问顺序排列的 LinkedHashMap 对象linkedHashMap2,添加键值对后,通过访问和修改操作,再次输出时会发现键值对按照访问顺序进行了调整。接着,我们进行了获取值、修改值、判断是否包含某个键、删除键值对以及遍历 LinkedHashMap 等操作,展示了 LinkedHashMap 的基本用法。
6.5 Hashtable
Hashtable 是 Map 接口的一个早期实现类,它与 HashMap 有很多相似之处 。Hashtable 同样用于存储键值对,并且通过键的哈希值来确定键值对在内部数据结构中的存储位置。
然而,Hashtable 与 HashMap 存在一些关键的区别 。其中最显著的区别是线程安全性。Hashtable 是线程安全的,它的所有公共方法都使用了synchronized关键字进行同步,这意味着在多线程环境下,多个线程可以安全地访问和修改 Hashtable 对象,而不会出现数据不一致的问题。例如,在一个多线程的银行账户管理系统中,如果使用 Hashtable 来存储账户信息,多个线程同时进行存款和取款操作时,Hashtable 能够保证账户信息的完整性和一致性。
相比之下,HashMap 是非线程安全的 。在多线程环境下,如果多个线程同时对 HashMap 进行修改操作(如添加、删除键值对),可能会导致数据不一致或其他并发问题。例如,一个线程正在读取 HashMap 中的键值对,而另一个线程同时在删除该 HashMap 中的键值对,就可能会出现读取到错误数据或抛出异常的情况。
另外,Hashtable 不允许键和值为null 。如果尝试将null作为键或值插入到 Hashtable 中,会抛出NullPointerException异常。而 HashMap 允许键为null(但只能有一个null键),也允许值为null。
在性能方面,由于 Hashtable 的方法是同步的,这在一定程度上会影响其性能 。在单线程环境下,HashMap 的性能通常比 Hashtable 更好,因为它不需要进行同步操作,减少了额外的开销。下面是一个使用 Hashtable 的示例代码:
import java.util.Hashtable;
import java.util.Map;public class HashtableExample {public static void main(String[] args) {// 创建一个Hashtable对象Hashtable<String, Integer> hashtable = new Hashtable<>();// 添加键值对hashtable.put("apple", 10);hashtable.put("banana", 20);hashtable.put("cherry", 30);// 获取值Integer value = hashtable.get("banana");System.out.println("banana的值是: " + value);// 修改值hashtable.put("banana", 25);System.out.println("修改后的Hashtable: " + hashtable);// 判断是否包含某个键boolean containsKey = hashtable.containsKey("cherry");System.out.println("Hashtable是否包含cherry键: " + containsKey);// 删除键值对hashtable.remove("apple");System.out.println("删除apple后的Hashtable: " + hashtable);// 遍历Hashtablefor (Map.Entry<String, Integer> entry : hashtable.entrySet()) {System.out.println("键: " + entry.getKey() + ", 值: " + entry.getValue());}}
}
在这个示例中,我们创建了一个 Hashtable 对象,并进行了添加、获取、修改、删除和遍历键值对的操作,展示了 Hashtable 的基本用法。由于其线程安全性,Hashtable 更适合在多线程环境中使用,但在单线程环境下,通常更推荐使用性能更好的 HashMap。
6.6 案例演示
为了更直观地展示 Map 接口及其实现类的使用,我们以一个简单的学生信息管理系统为例。在这个系统中,我们需要存储学生的学号、姓名和成绩,并实现添加学生信息、根据学号查询
七、集合框架的应用场景
7.1 数据存储与管理
在日常的 Java 开发中,集合框架在数据存储与管理方面有着广泛的应用。以一个简单的用户管理系统为例,我们可以使用ArrayList来存储用户信息。每个用户对象包含用户名、密码、年龄等属性,通过将这些用户对象添加到ArrayList中,我们可以方便地对用户信息进行集中管理。例如:
import java.util.ArrayList;
import java.util.List;class User {private String username;private String password;private int age;public User(String username, String password, int age) {this.username = username;this.password = password;this.age = age;}public String getUsername() {return username;}public String getPassword() {return password;}public int getAge() {return age;}
}public class UserManagement {public static void main(String[] args) {List<User> userList = new ArrayList<>();// 添加用户userList.add(new User("Alice", "123456", 25));userList.add(new User("Bob", "abcdef", 30));// 遍历用户列表for (User user : userList) {System.out.println("用户名: " + user.getUsername() + ", 年龄: " + user.getAge());}}
}
在这个示例中,ArrayList提供了一种简单而有效的方式来存储和遍历用户对象。我们可以根据需要轻松地添加、删除或修改用户信息,实现对用户数据的灵活管理。
在电商系统中,我们可以使用HashMap来存储商品信息。以商品 ID 作为键,商品对象作为值,这样可以通过商品 ID 快速查找对应的商品信息。商品对象可能包含商品名称、价格、库存等属性。例如:
import java.util.HashMap;
import java.util.Map;class Product {private String name;private double price;private int stock;public Product(String name, double price, int stock) {this.name = name;this.price = price;this.stock = stock;}public String getName() {return name;}public double getPrice() {return price;}public int getStock() {return stock;}
}public class EcommerceSystem {public static void main(String[] args) {Map<Integer, Product> productMap = new HashMap<>();// 添加商品productMap.put(1, new Product("手机", 2999.0, 100));productMap.put(2, new Product("电脑", 5999.0, 50));// 根据商品ID查找商品Product product = productMap.get(1);if (product!= null) {System.out.println("商品名称: " + product.getName() + ", 价格: " + product.getPrice());}}
}
通过HashMap,我们可以高效地管理大量的商品数据,快速响应根据商品 ID 查询商品信息的请求,提高电商系统的性能和用户体验。
7.2 数据处理与分析
集合框架在数据处理与分析方面也发挥着重要作用。借助 Java 8 引入的 Stream API,我们可以结合集合进行复杂的数据处理操作。例如,有一个学生成绩列表,我们可以使用 Stream API 进行筛选、排序和统计。假设学生对象包含姓名和成绩两个属性,我们要找出成绩大于 90 分的学生,并按成绩从高到低排序,最后统计这些学生的人数。示例代码如下:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;class Student {private String name;private int score;public Student(String name, int score) {this.name = name;this.score = score;}public String getName() {return name;}public int getScore() {return score;}
}public class StudentGradeAnalysis {public static void main(String[] args) {List<Student> studentList = new ArrayList<>();studentList.add(new Student("Alice", 95));studentList.add(new Student("Bob", 88));studentList.add(new Student("Charlie", 92));// 筛选成绩大于90分的学生,按成绩从高到低排序,统计人数long count = studentList.stream().filter(student -> student.getScore() > 90).sorted(Comparator.comparingInt(Student::getScore).reversed()).collect(Collectors.toList()).size();System.out.println("成绩大于90分的学生人数: " + count);}
}
在这个例子中,stream()方法将List转换为流,filter()方法用于筛选成绩大于 90 分的学生,sorted()方法按照成绩从高到低排序,collect(Collectors.toList())将处理后的流转换回List,最后通过size()方法统计人数。通过 Stream API,我们可以用简洁的代码实现复杂的数据处理逻辑,提高代码的可读性和维护性。
再比如,有一个包含员工薪资的列表,我们可以使用 Stream API 计算员工薪资的总和、平均值、最大值和最小值。假设员工对象包含姓名和薪资两个属性,示例代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.stream.Collectors;class Employee {private String name;private int salary;public Employee(String name, int salary) {this.name = name;this.salary = salary;}public String getName() {return name;}public int getSalary() {return salary;}
}public class EmployeeSalaryAnalysis {public static void main(String[] args) {List<Employee> employeeList = new ArrayList<>();employeeList.add(new Employee("Alice", 5000));employeeList.add(new Employee("Bob", 6000));employeeList.add(new Employee("Charlie", 5500));// 计算薪资总和int totalSalary = employeeList.stream().mapToInt(Employee::getSalary).sum();// 计算薪资平均值OptionalDouble averageSalary = employeeList.stream().mapToInt(Employee::getSalary).average();// 计算薪资最大值OptionalInt maxSalary = employeeList.stream().mapToInt(Employee::getSalary).max();// 计算薪资最小值OptionalInt minSalary = employeeList.stream().mapToInt(Employee::getSalary).min();System.out.println("薪资总和: " + totalSalary);averageSalary.ifPresent(salary -> System.out.println("薪资平均值: " + salary));maxSalary.ifPresent(salary -> System.out.println("薪资最大值: " + salary));minSalary.ifPresent(salary -> System.out.println("薪资最小值: " + salary));}
}
在这个示例中,mapToInt()方法将Employee对象流转换为int类型的流,方便进行数值计算。sum()方法计算总和,average()方法计算平均值,max()方法计算最大值,min()方法计算最小值。通过 Stream API 的这些方法,我们可以轻松地对集合中的数据进行各种统计分析。
7.3 算法与数据结构实现
集合框架在实现各种算法和数据结构时也有着广泛的应用。例如,我们可以使用List来实现队列和堆栈数据结构。队列是一种先进先出(FIFO)的数据结构,我们可以使用LinkedList来实现队列。LinkedList实现了Queue接口,提供了offer()、poll()等方法,符合队列的操作特性。示例代码如下:
import java.util.LinkedList;
import java.util.Queue;public class QueueImplementation {public static void main(String[] args) {Queue<Integer> queue = new LinkedList<>();// 入队queue.offer(10);queue.offer(20);queue.offer(30);// 出队while (!queue.isEmpty()) {System.out.println("出队元素: " + queue.poll());}}
}
在这个示例中,offer()方法将元素添加到队列末尾,poll()方法从队列头部移除并返回元素,实现了队列的先进先出特性。
堆栈是一种后进先出(LIFO)的数据结构,我们可以使用LinkedList或Stack类来实现堆栈。Stack类是Vector的子类,专门用于实现堆栈数据结构。示例代码如下:
import java.util.Stack;public class StackImplementation {public static void main(String[] args) {Stack<Integer> stack = new Stack<>();// 入栈stack.push(10);stack.push(20);stack.push(30);// 出栈while (!stack.isEmpty()) {System.out.println("出栈元素: " + stack.pop());}}
}
在这个示例中,push()方法将元素压入栈顶,pop()方法从栈顶弹出元素,实现了堆栈的后进先出特性。
此外,我们还可以使用Set来实现集合运算。例如,有两个Set集合,我们可以使用retainAll()方法求交集,使用addAll()方法求并集,使用removeAll()方法求差集。示例代码如下:
import java.util.HashSet;
import java.util.Set;public class SetOperations {public static void main(String[] args) {Set<Integer> set1 = new HashSet<>();set1.add(1);set1.add(2);set1.add(3);Set<Integer> set2 = new HashSet<>();set2.add(2);set2.add(3);set2.add(4);// 求交集Set<Integer> intersection = new HashSet<>(set1);intersection.retainAll(set2);System.out.println("交集: " + intersection);// 求并集Set<Integer> union = new HashSet<>(set1);union.addAll(set2);System.out.println("并集: " + union);// 求差集Set<Integer> difference = new HashSet<>(set1);difference.removeAll(set2);System.out.println("差集: " + difference);}
}
在这个示例中,通过Set接口提供的方法,我们可以方便地实现集合的各种运算,满足不同算法和数据结构实现的需求。
八、集合框架的性能优化
8.1 选择合适的集合实现类
在 Java 集合框架中,选择合适的集合实现类对于程序性能至关重要。不同的集合实现类在数据结构、操作效率和适用场景上存在差异,因此需要根据具体的业务需求进行合理选择。
对于List接口的实现类,ArrayList基于动态数组实现,支持快速的随机访问,通过索引直接定位元素的时间复杂度为 O (1) ,就像在一排编好号的邮箱中,根据编号能快速找到对应的邮箱。例如,在一个需要频繁根据索引获取元素的学生成绩管理系统中,使用ArrayList来存储学生成绩,能快速查询到每个学生的成绩。但ArrayList在进行插入和删除操作时效率较低,因为需要移动数组中的元素来调整位置,时间复杂度为 O (n) ,其中 n 是ArrayList中元素的数量。
LinkedList基于双向链表结构,在进行插入和删除操作时非常高效,只需要修改节点之间的引用关系,时间复杂度为 O (1) ,就像在一条链子上添加或移除一个环,只需要连接或断开相邻的环即可。例如,在一个需要频繁添加和删除元素的消息队列系统中,使用LinkedList来存储消息,能快速处理消息的入队和出队操作。但LinkedList的随机访问效率较低,需要从链表的头部或尾部开始遍历,直到找到目标元素,时间复杂度为 O (n) 。
对于Set接口的实现类,HashSet基于哈希表实现,通过计算元素的哈希值来确定元素在哈希表中的存储位置,从而实现快速的插入和查找操作,平均时间复杂度为 O (1) 。例如,在一个需要快速判断元素是否存在的用户登录系统中,使用HashSet来存储已注册的用户名,能快速判断新注册的用户名是否已被使用。但HashSet不保证元素的顺序,并且元素是不可重复的。
TreeSet基于红黑树实现,元素会按照自然顺序或自定义的比较器顺序进行排序,插入、删除和查找操作的时间复杂度都为 O (log n) ,其中 n 是TreeSet中元素的数量。例如,在一个需要对元素进行排序并确保唯一性的排行榜系统中,使用TreeSet来存储用户的成绩,能自动对成绩进行排序并展示。
对于Map接口的实现类,HashMap基于哈希表实现,通过键的哈希值来确定键值对在哈希表中的存储位置,实现快速的插入、删除和查找操作,平均时间复杂度为 O (1) 。例如,在一个需要快速根据键查找值的学生信息管理系统中,使用HashMap,以学生学号作为键,学生的详细信息作为值,能快速通过学号获取学生信息。但HashMap不保证键值对的顺序,并且允许键和值为null。
TreeMap基于红黑树实现,会按照键的自然顺序或自定义的比较器顺序对键值对进行排序,插入、删除和查找操作的时间复杂度都为 O (log n) 。例如,在一个需要对键进行排序并按顺序遍历键值对的日志系统中,使用TreeMap,以时间作为键,日志内容作为值,能方便按时间顺序查看日志。
8.2 合理设置集合初始容量
集合的扩容操作对性能有着显著的影响,因此合理设置集合的初始容量是优化性能的重要手段。以ArrayList为例,它基于动态数组实现,当数组容量不足时会进行扩容。扩容的过程涉及创建一个新的更大容量的数组,并将原数组中的元素复制到新数组中。这个过程不仅消耗时间,还可能导致内存的频繁分配和释放,从而影响程序的性能。
假设我们创建一个ArrayList,并不断向其中添加元素。当添加的元素数量超过当前数组的容量时,ArrayList会进行扩容。如果初始容量设置过小,就会频繁触发扩容操作。例如,初始容量为 10,当添加第 11 个元素时就会触发扩容,将容量扩大为 15(1.5 倍扩容) ,然后将原数组中的 10 个元素复制到新数组中。如果后续继续添加元素,可能会再次触发扩容,这样就会产生多次数组复制的开销,降低程序的运行效率。
为了减少扩容次数,我们需要根据数据量预估合理设置集合的初始容量。在创建ArrayList时,可以通过构造函数指定初始容量。例如,如果我们预计要存储 100 个元素,那么可以创建ArrayList时设置初始容量为 100,即ArrayList list = new ArrayList(100); 。这样可以避免在添加元素过程中频繁触发扩容,提高程序的性能。
对于HashMap,同样需要合理设置初始容量和负载因子。初始容量决定了哈希表的大小,负载因子则控制了哈希表的填充程度。默认情况下,HashMap的初始容量为 16,负载因子为 0.75 。当哈希表中的元素数量达到容量乘以负载因子时,就会进行扩容。如果初始容量设置过小,可能会导致哈希冲突频繁发生,从而降低查找和插入的效率;如果初始容量设置过大,又会浪费内存空间。因此,在创建HashMap时,需要根据实际数据量和预期的负载情况合理设置初始容量和负载因子,以平衡性能和内存占用。
8.3 使用迭代器遍历集合
在遍历集合时,使用迭代器具有诸多优势。迭代器提供了一种统一的方式来遍历集合中的元素,而无需关心集合的内部实现细节。通过迭代器,我们可以按顺序逐个访问集合中的元素,并且可以在遍历过程中安全地删除元素。
与使用普通的 for 循环遍历集合相比,迭代器的优势更加明显。以LinkedList为例,如果使用 for 循环通过索引来访问元素,由于LinkedList是基于链表结构,每次通过索引访问元素都需要从链表的头部或尾部开始遍历,直到找到目标元素,时间复杂度为 O (n) ,其中 n 是链表中元素的数量。而使用迭代器遍历LinkedList时,迭代器会维护一个指向当前元素的指针,每次调用next()方法时,指针会移动到下一个元素,时间复杂度为 O (1) ,大大提高了遍历效率。
另外,在遍历集合的过程中,避免对集合进行结构性修改(如添加或删除元素)是非常重要的。如果在使用普通 for 循环遍历集合时进行结构性修改,可能会导致索引越界或其他异常。例如,在使用 for 循环遍历ArrayList时,如果在循环中删除一个元素,那么后续元素的索引会发生变化,可能会导致跳过某些元素或访问到错误的元素。而使用迭代器时,迭代器提供了remove()方法,允许在遍历过程中安全地删除当前元素。例如:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class IteratorExample {public static void main(String[] args) {List<Integer> list = new ArrayList<>();list.add(1);list.add(2);list.add(3);list.add(4);Iterator<Integer> iterator = list.iterator();while (iterator.hasNext()) {Integer element = iterator.next();if (element == 3) {iterator.remove();}}System.out.println(list);}
}
在这个示例中,我们使用迭代器遍历ArrayList,当遇到元素 3 时,使用迭代器的remove()方法将其删除。这样可以确保在遍历过程中对集合进行删除操作的安全性,避免出现异常。
8.4 并发集合的使用
在多线程环境下,使用并发集合是确保数据一致性和安全性的关键。ConcurrentHashMap和CopyOnWriteArrayList是两个常用的并发集合类,它们各自采用了不同的机制来实现线程安全。
ConcurrentHashMap采用了分段锁的机制 。在ConcurrentHashMap中,数据被分成多个段(默认 16 个),每个段都有自己的锁。当进行写操作时,只需要锁定对应的段,而其他段仍然可以被其他线程访问。这大大提高了并发性能,减少了锁竞争。例如,在一个多线程的电商系统中,多个线程可能同时对商品库存进行修改操作。使用ConcurrentHashMap来存储商品库存信息,每个商品的库存信息存储在不同的段中,当一个线程修改某个商品的库存时,只会锁定该商品所在的段,其他线程可以同时修改其他商品的库存,不会相互阻塞。
CopyOnWriteArrayList则采用了写时复制的策略 。在CopyOnWriteArrayList中,读操作是无锁的,直接访问共享的数组。而写操作(如添加、删除、修改元素)时,会先复制一份原数组,然后在新的数组上进行操作,最后将原数组的引用指向新数组。这样可以保证在写操作进行时,读操作仍然可以正常进行,不会受到影响。例如,在一个多线程的日志系统中,多个线程可能同时读取日志信息,而偶尔有线程需要添加新的日志记录。使用CopyOnWriteArrayList来存储日志信息,读操作可以快速进行,而写操作虽然会有一定的开销(复制数组),但不会阻塞读操作,从而提高了系统的整体性能。
在使用并发集合时,需要根据具体的业务场景选择合适的集合类。如果读操作远远多于写操作,并且对数据的实时性要求不是特别高,CopyOnWriteArrayList是一个不错的选择;如果读写操作都比较频繁,并且需要保证数据的一致性和高效的并发性能,ConcurrentHashMap则更为合适。