JAVA集合框架
大家好,好久不见。最近学习Java集合顺带复习数据结构,学习到了一些java集合框架的知识,将它整理总结一下,分享给大家,希望能让大家对于框架体系有更好的把握。学习阶段,缺少开发经验,文章有纰漏错误之处还请多多指教!
在开发过程中,不同的数据结构会在实现风格和性能方面产生很大的差异。需要快速搜索成千上万个有序的数据项吗?需要快速地在有序的序列中间插入元素或删除元素吗?需要建立键与值的关联吗…
Java集合框架在软件开发中扮演着至关重要的角色,它提供了丰富多样的数据结构和操作方法,用于高效地组织和处理数据。本文将带你深入探索Java集合框架,从概述到常用接口及其特点、实现类,再到适用场景,来完善你对集合知识体系的理解,方便在今后的应用中游刃有余的使用合适的数据结构,开发性能优越的系统。好了,且听我娓娓道来~
文章目录
1.概述
Java集合框架是Java编程中不可或缺的部分,它提供了一系列的数据结构和算法,用于存储、组织和操作各种类型的数据。无论是处理简单的数据集合还是复杂的数据结构,Java集合框架都为开发者提供了高效、灵活的工具。在软件开发过程中,深入理解和熟练运用集合框架将大大提升代码质量、效率和可维护性。
-
集合框架的重要性
- **数据管理和组织:**集合框架为开发者提供了多种数据结构,从简单的列表到复杂的映射,使得数据可以以合理、有序的方式进行存储和访问。
- 算法效率: 集合框架中的实现经过优化,可以高效地执行增加、删除、查找等操作。通过选择适当的集合类,可以避免手动实现这些操作,提高代码的效率。
- 代码可读性: 使用集合框架可以使代码更加易读、易懂。它提供了抽象的数据结构和方法,使得开发者能够专注于业务逻辑,而不必关心底层实现。
-
集合框架应用场景
- 数据存储与操作:无论是存储用户信息、商品列表,还是管理任务队列、消息传递、缓存管理集合框架都提供了合适的数据结构和操作方法。
- 性能优化:合理选择集合类可以在处理大量数据时获得更好的性能。使用
HashMap
进行快速的查找、使用ArrayList
进行高效的随机访问或者使用链表执行大量迅速的增删操作。 - **算法实现:**许多算法和数据结构的实现依赖于集合框架,例如大多排序算法依赖
List
接口下的类实现,BFS(广度优先搜索)算法依赖队列实现,利用优先队列(PriorityQueue
类)实现Dijkstra算法等需要按优先级处理元素的场景。
看到这里,集合的重要性可见一斑。那么接下来让我们开始了解集合框架具体的内容吧。Let‘s Go!
2.主要接口及其特点
与数据结构类库常见的情况一样,Java集合类库也将接口(interface
)与实现(implementation
)分离。所以让我们从集合框架所包含的接口开始,java集合框架中定义了大量接口,如下图所示:
Collection 接口
java集合框架中的核心接口,它定义了一组通用的集合操作和方法。所有实现了Collection
接口的类都必须提供这些方法的实现,无论是列表(List
)、集(Set
)、还是队列(Queue
),都可以使用这些方法来操作和管理集合中的元素。
下面介绍一些Collection
提供的常见方法:
boolean add(E e)
:将指定的元素添加到集合中,添加成功返回true
,否则返回false
boolean remove(Object o)
:从集合中移除指定的元素,移除成功返回true
,否则返回false
boolean contains(Object o)
:判断集合是否包含指定元素,若包含返回true
,否则返回false
int size()
:返回集合中的元素数量
boolean isEmpty()
判断集合是否为空
void clear
: 清空集合中的所有元素
Iterator<E> iterator
:返回一个迭代器,用于遍历结合中的元素
boolean addAll(Collection<? extends E> c)
:将另一个集合中的所有元素添加到当前集合中
boolean removeAll(Collection<?> c)
:从当前集合中移除与另一个集合共有的元素
boolean retainAll(Collection<?> c)
:保留当前集合中与另一个集合共有的元素,移除其他元素
Object[] toArray()
:将集合转换为数组
<T> T[] toArray(T[] a)
:将集合中的元素转换为指定类型的数组
List 接口
List
接口代表一个有序可重复的集合,允许元素按照索引进行访问。可以采用两种方式访问元素:迭代器访问与整数索引访问。List接口的实现类有ArrayList
和LinkedList
。
除了父接口Collection
提供的方法外,List
中还定义了如下方法:
void add(int index, E element)
:将指定元素添加到索引位置,若没有指定索引,则默认添加到末尾
boolean addAll(int index, Collection<? extends E> collection)
:将指定集合中的所有元素从指定索引位置开始插入
E remove(int index)
:移除并返回指定索引位置的元素
E get(int index)
:返回指定索引位置的元素
E set(int index, E element)
:用指定元素替换指定索引位置的元素,并返回被替换的元素
int indexOf(Object object)
:返回指定对象第一次出现的索引,如果不存在则返回 -1
int lastIndexOf(Object object)
:返回指定对象最后一次出现的索引,如果不存在则返回 -1。
ListIterator<E> listIterator()
:返回在列表上进行双向迭代的列表迭代器
ListIterator<E> listI terator(int index)
:返回从指定索引位置开始的列表迭代器
Set 接口
Set
接口代表一个无序不可重复的集合,确保每个元素唯一。集(set)的add
方法不允许添加重复的元素,要适当的定义集的equals
方法:只要两个集包含同样的元素就认为是相等的,而不要求这些元素有相同的顺序。hashCode
方法的定义要保证包含相同元素的两个集会得相同的散列码。Set
的常用实现类有:HashSet
和 TreeSet
.
Set
中定义的方法有:
boolean add(E element)
:将指定元素添加到集合中,如果元素已存在,则不添加并返回false
Queue 接口
继承了Collection
接口,但也具有自己的特点。它可以用于实现队列(Queue
)数据结构。队列是一种**先进先出(FIFO)**的数据结构,即最先添加的元素将被最先移除。
Queue
接口常用于实现先进先出的任务调度、消息传递等场景,Queue
还派生出了子接口(Deque
)和实现类:
java.util.LinkedList
: 这是一个双端队列(Deque
接口)的实现,同时也实现了Queue
接口java.util.PriorityQueue
: 这是一个基于优先级的队列实现,元素按照优先级排序,最先添加的具有最高的优先级java.util.ArrayDeque
: 这是一个基于数组的双端队列(Deque
接口)实现,同时也实现了Queue
接口
Queue
接口定义了一组 用于操作队列的方法:
boolean add(E e)
:将指定的元素添加到队列末尾,如果队列已满则抛出异常
boolean offer(E e)
:将指定的元素添加到队列末尾 ,如果队列已满则返回false
E remove()
: 移除并返回队列的头部元素 ,如果队列为空则抛出异常
E poll()
: 移除并返回队列的头部元素,如果队列为空则返回null
E element()
:返回队列的头部元素但不移除,如果队列为空则抛出异常
E peek()
:返回队列的头部元素但不移除,如果队列为空返回null
Map 接口
Java集合框架中的另一个核心接口,它表示键-值(key - value)对的集合,其中每个键是唯一的,并且可以使用键来检索关联的值。我把它理解成一个地图(map),地图上的每一个点跟我们现实中的地点是一一对应的。想必这也算是对映射概念的一种理解吧。
Map接口定义了一组操作用于管理和操作键值对数据:
int size()
: 返回当前Map
中键值对的数量
boolean isEmpty()
: 判断Map
是否为空
boolean containsKey(Object key)
:判断Map
中是否包含特定的键
boolean containsValue(Object Value)
: 判断Map
中是否包含特定的值
V get(Object key)
: 根据给定的获取对应的value
值,如果键不存在返回null
V put(K key, V value)
: 将给定的键值对添加到Map
中,如果键已存在,则更新对应的值
V remove(Object key)
: 根据键值移除对应的键值对,并返回被移除的value
值
void clear()
: 清空所有键值对
Collection<V> values()
:返回一个包含所有值的集合
Set<Map.Entry<K, V>> entrySet()
: 返回一个包含所有键值对(Map.Entry
对象)的集合
Map
接口有许多实现类,其中一些常见的包括:
java.util.HashMap
: 使用哈希表实现的Map
,不保证键值对的顺序,查找速度很快。java.util.LinkedHashMap
: 继承自HashMap
的Map
,但保持键值对的插入顺序java.util.TreeMap
: 使用红黑树实现的Map
,键值对根据键的自然顺序(或指定的比较器)进行排序。java.util.Hashtable
: 类似于HashMap
,但线程安全,性能较差。
Iterator 接口
Iterator
接口是java集合框架的核心接口之一,它用于迭代遍历集合中的元素,允许访问集合中每个元素并逐个处理,它提供了一种统一的方式来遍历不同的集合,而不必关心集合的具体实现细节。
Iterator
接口中定义了如下方法:
boolean hasNext()
:检查是否还有下一个元素可以遍历。如果有下一个元素,返回true
;否则,返回false
E next()
:返回下一个元素,并将迭代器的位置向前移动。如果没有更多元素可遍历,则会抛出NoSuchElementException
异常
void remove()
:从集合中删除上一个由next()
返回的元素(可选操作)。这个方法只能在调用next()
之后调用一次,连续调用会抛出IllegalStateException
异常。
如何使用Iterator
遍历集合:
public class IteratorExample {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// 获取迭代器
Iterator<String> iterator = fruits.iterator();
// 使用迭代器遍历集合
while (iterator.hasNext()) {//判断是否有下一个元素可遍历
String fruit = iterator.next();
System.out.println(fruit);
}
}
}
RandomAcess
接口是Java SE 1.4引入的一个标记接口,这个接口用来测试一个特定的集合是否支持高效的随机访问,它不包含任何方法。由于它不是我们学习的重点,这里就不再赘述了。
了解完这些主要接口的特征,是不是对Java集合体系有了更好的理解呢?接下来让我们深入到接口的实现当中,了解它们各自独特的作用与使用场景吧~
3.常用实现类
java集合框架提供了丰富的实现类,他们各有千秋,下面介绍具体的集合。如下图:
ArrayList 动态数组
ArrayList
类实现自List
接口,是一个基于动态数组实现的集合类,它允许存储一组元素并按照索引快速的进行随机访问。可以通过迭代器或者get()
与set()
方法访问。然而和数组一样,数组列表有一个不足,就是在中间位置删除或添加一个元素需要付出很大代价,原因是数组中处于被添加或删除元素之后的所有元素都要依次后移或前移。
但在需要频繁访问和**修改(内容)**元素的情况下,ArrayList
是一个不错的选择:
List<String> names = new ArrayList<>();
names.add("Alice"); //add方法如果没有下标,那么默认添加到动态数组的末尾
nemes.add("Bob");
names.add("CharLie");
names.set(1,"Jack");//替换后names["Alice","Jack","CharLie"] 方法返回:Bob
还有一个类似的类是Vector
,不同之处它是线程安全的。Vector类的所有方法都是同步的,可以由两个线程安全地访问一个Vector
对象。但是由一个线程访问Vector
,代码要在同步操作上耗费大量的时间。因此,建议在不需要同步时使用ArrayList
,而非Vector
。
LinkedList 链表与队列
LinkedList
也是List
接口的实现,如果说数组是在连续的地址上存放对象引用,那么链表与它恰好相反。链表将每个对象放在独立的节点中,每个节点还放着序列中下一个节点的引用。这些节点在内中的分布是随机的,非连续的。在Java
中,所有链表是双链表——即每个节点还存放着指向前一个节点的引用。
在链表中任意位置添加或者删除元素是很轻松的操作。在删除一个元素时,我们只需要找到被删节点前一个节点再让它的指针指向下一个节点就可以实现删除(JVM会自动将无用的对象回收)。同理,在添加时,让被添加节点的头指针指向前一个节点,为指针指向下一个节点便实现了添加。由此可见,在链表中,我们只需要通过对节点指针的修改便可以实现链表元素的增删操作。然而,指针绕来绕去给人不好的印象,LinkedList
中提供了**Iterator
(迭代器)**方法,我们只需要调用迭代器方法就可以实现增删而不用关注内部细节。
链表中的add
方法只能将元素添加到链表的末尾,要想在链表的其他位置添加元素,需要通过ListIterator
接口提供的add
方法。当然,ListIterator
接口还提供了previous()
方法和hasPrevious()
方法等来实现反向遍历链表…
LinkedList
既是 List
接口的实现,也是 Queue
接口的实现。这使得它可以充当双向队列(既有队列的特性,又能支持在队尾插入和删除元素),也可以仅作为队列使用。它对于在队首和队尾进行元素插入和删除操作非常高效。
实例 1:先添加三个元素,然后在将第2个元素删除
List<String> staff = new LinkedList<>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
Iterator iter = staff.iterator();
String first = iter.next(); //访问到第一个元素
String second = iter.next(); //访问到第二个元素
iter.remove(); //删除上一个访问的元素
实例 2:在链表中第二个节点位置添加一个新元素
List<String> staff = new LinkedList<>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
ListIterator<String> iter = staff.listIterator();
iter.next(); //跳过第一个元素
iter.add("Juliet");
LinkedList
实现队列
Queue<String> queue = new LinkedList<>();
queue.offer("Alice"); //入队操作,在队首添加元素
queue.offer("Bob");
queue.offer("Charlie");
String frontElement = queue.poll(); // 出队操作,返回队首元素并删除
需要区分的是,remove
方法会删除next()
指针左侧的元素,而add方法在next()
的右侧添加元素。
当然,对于get方法而言,尽管设计者做出了优化:如果索引大于size()/2
就从链表末尾开始搜索元素。但并没有提高实质性的效率,所以在面对查询次数较多的操作时,需要慎重选择使用链表。
HashSet 散列集
HashSet
是基于HashTable
(散列表)实现的集合类,它保证元素的唯一性,并且没有固定的顺序。可以用add
方法添加不重复的元素,contains
方法被重写,用来快速的查看是否某个元素已经在集中。HashSet使用散列(Hash)机制来存储和管理元素,不考虑哈希冲突的情况下,它查找、删除和添加的操作平均时间复杂度为O(1)。另外,HashSet
不是线程安全的,如果在多个线程同时修改一个 HashSet
实例,可能会导致不一致的结果。如果需要线程安全,可以使用 java.util.concurrent
包下的类,如 ConcurrentHashSet
。
HashSet
在快速查找和去重场景中表现出色。
HashSet<String> set = new HashSet<>();
// 添加元素
set.add("apple");
set.add("banana");
set.add("orange");
set.add("apple"); // 重复元素,将被忽略
// 查找元素
boolean containsBanana = set.contains("banana");
// 删除元素
boolean removedApple = set.remove("apple");
TreeSet 树集
TreeSet
与HashSet
十分相似,但有所改进。树集是java结合框架中基于红黑树实现的有序集合类。它实现了SortedSet
接口,因此保证了元素的有序性。TreeSet
对元素进行排序,可以根据元素的自然顺序或者通过指定的比较器进行排序。
TreeSet
保证有序性也保证去重,底层是一颗红黑树,所以它的插入、删除和查找操作在平均情况下的时间复杂度为 O(log n
),非常高效。
// 使用默认构造函数创建一个空的 TreeSet,将使用元素的自然顺序进行排序
TreeSet<Integer> treeSet1 = new TreeSet<>();
treeSet1.add(5);treeSet1.add(3);treeSet1.add(8);
treeSet1.remove(3);
// 创建一个使用自定义比较器的 TreeSet
TreeSet<Integer> treeSet2 = new TreeSet<>(new MyComparator());
Integer min = treeSet1.first(); //first()方法获取最小值
Integer max = treeSet1.last(); //last()方法获取最大值
TreeSet<Integer> subSet = treeSet1.subSet(3, 8); // 包含3但不包含8的子集
注意:要使用树集,必须能够比较元素。这些元素必须实现Comparable
接口,或者构造集时必须提供一个Comparator
比较器。
EnumSet 枚举集
EnumSet
是java中专门用于处理枚举类型的集合实现类。它是一个高效的、基于位向量的集合,专门为枚举类型设计,因此在性能和内存方面都有很好的表现。EnumSet
保证了集合中的元素是唯一且有序的,元素的顺序与枚举常量在枚举类中的声明顺序相同。
//创建一个包含指定枚举常量的 EnumSet
EnumSet<DayOfWeek> weekdays =
EnumSet.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY);
//EnumSet 支持基本的集合操作,如添加、删除、包含、清空
weekdays.add(DayOfWeek.SATURDAY); // 添加元素
weekdays.remove(DayOfWeek.TUESDAY); // 删除元素
boolean contains = weekdays.contains(DayOfWeek.WEDNESDAY); // 检查是否包含元素
weekdays.clear(); // 清空集合
EnumSet<DayOfWeek> weekend = EnumSet.complementOf(weekdays); // complementOf 获取工作日的补集
EnumSet<DayOfWeek> allDays = EnumSet.allOf(DayOfWeek.class); // allOf 包含所有枚举值的 EnumSet
EnumSet<DayOfWeek> copiedSet = EnumSet.copyOf(weekdays); // copyOf 复制一个 EnumSet
EnumSet
适用于需要处理枚举类型的集合操作,由于其基于位向量实现,所以在性能和内存方面表现出色。它在处理枚举类型的集合时通常比其他集合类(如 HashSet
)更为高效。
PriorityQueue 优先级队列
PriorityQueue
是一个基于堆实现的优先级队列,它可以根据元素的优先级进行排列。在优先级队列中,具有最高优先级的元素将被最先取出。默认情况下,PriorityQueue
是按照元素的自然顺序(如果元素实现了 Comparable
接口)进行排序,也可以通过指定的比较器来实现自定义排序。
优先级队列中的元素可以按照任意的顺序插入,却总是按照排序的顺序检索。也就是说,无论何时调用remove
方法,总会获得当前优先级队列中最小的元素。优先级队列使用的数据结构堆(heap
),是一个可自我调整的二叉树,对树执行添加和删除操作,可以让最小的元素移动到根,而不必花时间对元素进行排序。
Queue<Integer> priorityQueue = new PriorityQueue<>();
priorityQueue.offer(3);
priorityQueue.offer(1);
priorityQueue.offer(2);
int highestPriority = priorityQueue.poll(); // 出队操作,返回优先级最高的元素 1
ArrayDeque 双端队列
ArrayDeque
是 Java 中的双端队列(Deque 接口)实现,它可以在队列的两端进行插入和删除操作(不支持在中间添加元素),因此既可以用作队列,也可以用作栈。ArrayDeque
的底层实现是一个循环数组,它可以动态地增长和缩小,具有高效的插入和删除操作,且不需要像链表那样频繁地分配和释放内存。
ArrayDeque
不是线程安全的。
ArrayDeque<Integer> arrayDeque = new ArrayDeque<>();
arrayDeque.addFirst(1); // 在队头添加元素
arrayDeque.addLast(2); // 在队尾添加元素
int first = arrayDeque.removeFirst(); // 移除并返回队头元素
int last = arrayDeque.removeLast(); // 移除并返回队尾元素
int first = arrayDeque.getFirst(); // getFirst 获取队头元素
int last = arrayDeque.getLast(); // getLas 获取队尾元素
文行至此,对于队列的实现有两种方式:LinkedList
实现与ArrayDeque
实现。
当在程序中使用队列时,一旦构建了集合就不需要知道究竟使用了那种实现。因此,只有在构建集合对象时,使用具体的类才有意义。我们可以使用接口类型存放集合的引用。
Queue<Customer> expressLane = new ArrayDeque<>(100);
expressLane.add(new Customer("Harry"));
利用这种方式,一旦改变了想法,可以轻松的使用另外一种不同的实现。只需要对程序调用构造器的地方做出修改:
Queue<Customer> expressLane = new LinkedList<>();
expressLane.add(new Customer("Harry"));
接口本身并不能说明哪种实现的效率究竟如何。循环数组比链表更高效,但是循环数组是一个有界集合,即容量有限。一般如果程序要收集的对象没有上限,使用链表方式实现。
关于Collection接口下的主要实现类介绍完了,接下来让我们继续看Map接口下有哪些实现类吧…
HashMap 散列映射
Map
接口的实现类,它实现了基于哈希表的映射(键-值对)数据结构。HashMap
不允许存储重复的键(键是唯一的),并且每个键关联一个值(值可以重复),如果对一个键调用两次put
方法,那么第二个值便会取代第一个值。
HashMap
对键进行散列,散列或比较函数只能作用与键,与键关联的值不能进行散列或比较,并通过键的哈希码来快速定位和访问对应的值。HashMap是无序的集合,允许多个值为null
,但只允许一个键为null
。
HashMap
不是线程安全的。
以下是一些常用的 HashMap
方法:
public static void main(String[] args) {
// 创建一个 HashMap
Map<String, Integer> studentScores = new HashMap<>();
// 添加键值对
studentScores.put("Alice", 90);
studentScores.put("Bob", 85);
studentScores.put("Charlie", 95);
// 获取值
int aliceScore = studentScores.get("Alice");
// 遍历键值对
for (Map.Entry<String, Integer> entry : studentScores.entrySet()) {
String name = entry.getKey();
int score = entry.getValue();
System.out.println(name + "'s score: " + score);
}
// 判断键是否存在
boolean containsBob = studentScores.containsKey("Bob");
// 删除键值对
studentScores.remove("Charlie");
}
HashMap
是一个常用的数据结构,适用于需要快速查找和映射键到值的场景。
TreeMap 树映射
Map接口的实现类,它实现了基于红黑树(一种自平衡二叉搜索树)的映射(键-值对)数据结构。与 HashMap
不同,TreeMap
用键的整体顺序对元素进行排序,并将其组织成搜索树。
TreeMap
不允许键为 null
,因为在红黑树中需要进行比较操作,而 null
无法进行比较。但TreeMap
允许值为 null
。
TreeMap
不是线程安全的
以下是一些常用的 TreeMap
方法:
public static void main(String[] args) {
// 创建一个 TreeMap
TreeMap<String, Integer> studentScores = new TreeMap<>();
// 添加键值对
studentScores.put("Alice", 90);
studentScores.put("Bob", 85);
studentScores.put("Charlie", 95);
// 获取值
int aliceScore = studentScores.get("Alice");
// 获取最小和最大键
String minKey = studentScores.firstKey();
String maxKey = studentScores.lastKey();
// 获取小于指定键的最大键
String lowerKey = studentScores.lowerKey("Charlie");
// 删除键值对
studentScores.remove("Bob");
}
LinkedHashMap
是HashMap
的子类,不同点在于LinkedHashMap
保留了插入顺序。它在HashMap
的基础上添加了一个双向链表来维护插入顺序。它也是线程不安全的。
LinkedHashMap
在需要保留插入顺序的情况下非常有用,它兼具了 HashMap
的快速查找和 LinkedList
的保留插入顺序的特点。
4.集合的遍历与排序
集合的遍历
集合的遍历也是重要的知识点,接下来我们以HashMap
为例介绍集合遍历的几种方式:
import java.util.*;
public class HashMapTraversalExample {
public static void main(String[] args) {
// 创建一个 HashMap 并添加键值对
Map<String, Integer> ageMap = new HashMap<>();
ageMap.put("Alice", 25);
ageMap.put("Bob", 30);
ageMap.put("Charlie", 28);
// 使用迭代器遍历键值对
Iterator<Map.Entry<String, Integer>> iterator = ageMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
String name = entry.getKey();
int age = entry.getValue();
System.out.println(name + ": " + age);
}
// 使用增强的for循环遍历键值对
for (Map.Entry<String, Integer> entry : ageMap.entrySet()) {
String name = entry.getKey();
int age = entry.getValue();
System.out.println(name + ": " + age);
}
// 遍历键集合并获取对应的值
Set<String> names = ageMap.keySet();
for (String name : names) {
int age = ageMap.get(name);
System.out.println(name + ": " + age);
}
// 遍历值集合
Collection<Integer> ages = ageMap.values();
for (int age : ages) {
System.out.println("Age: " + age);
}
}
}
集合的排序
java框架中实现排序的方式大致有两种:使用Colletions.sort()
排序和Comparator
接口
排序方式 1:Collections.sort()
方法
Collections.sort()
方法可以用于实现对List
接口的集合进行排序,因为它保留了元素的插入顺序。Collections
是一个工具类,在默认情况下,sort
方法只能对集合的元素进行自然排序,我们只需要调用sort方法就能实现排序,但如果我们的元素是对象类型,大小界定存在歧义,此时我们就要在让被实现的类中实现Comparable
接口并重写CompareTo
方法,在重写的方法中定义我们排序的标准。
比如我们定义了一个格子(Cell)类,每个格子都由横纵两个坐标,我们要对格子排序,但默认的方法不知道如何判断哪个格子大哪个格子小,这时候我们就可以让Cell类实现Comparable接口并重写compareTo方法就可以明确排序的标准了。比如说我们要让它做到行优先排序:
public class Cell implements Comparable<Cell> {
private int row;
private int col;
public Cell(int row, int col) {
this.row = row;
this.col = col;
}
//重写compareTo方法实现行优先排序
@Override
public int compareTo(Cell o) {
int m = this.row - o.row;
return m;
}
//重写toStirng方法方便输出
@Override
public String toString() {
return "Cell{" +
"row=" + row +
", col=" + col +
'}';
}
}
class TestCell {
public static void main(String[] args) {
List<Cell> cells = new ArrayList<>();
cells.add(new Cell(2, 3));
cells.add(new Cell(4, 2));
cells.add(new Cell(1, 5));
//调用的sort方法不是默认的,是我们重写过排序规则的方法
Collections.sort(cells);
System.out.println(cells);
//[Cell{row=1, col=5}, Cell{row=2, col=3}, Cell{row=4, col=2}]
}
}
当然如果你想对实现了 Set
接口的集合进行排序,你可以将集合元素先放入一个 List
中,然后使用 Collections.sort()
对这个 List
进行排序,最后再将排序后的元素重新放回 Set
中。这样虽然需要一些额外的操作,但可以实现对 Set
的排序。
import java.util.*;
public class SetSortingExample {
public static void main(String[] args) {
Set<Integer> numberSet = new HashSet<>();
numberSet.add(5);
numberSet.add(2);
numberSet.add(8);
List<Integer> sortedList = new ArrayList<>(numberSet);
Collections.sort(sortedList);
System.out.println("Sorted Set: " + sortedList);
}
}
排序方式 2 :自定义的比较器:
Collections
类还提供了一个sort
方法的重载:<T> void Collections.sort(List<T> list, Comparator<? super T> c)
,这个重载允许我们通过比较器(Comparator
) 制定自定义排序的规则,然后将比较器的结果(整数)Collections.sort()
方法,最终由默认的sort方法来实现排序。这其实就是将抽象的比较结果转变成具体的容易比较的内容。
我们继续来对Cell类实现行优先排序:
public class Cell {
public int row;
public int col;
public Cell(int row, int col) {
this.row = row;
this.col = col;
}
@Override
public String toString() {
return "Cell{" +
"row=" + row +
", col=" + col +
'}';
}
}
class TestCell {
public static void main(String[] args) {
List<Cell> cells = new ArrayList<>();
cells.add(new Cell(2, 3));
cells.add(new Cell(4, 2));
cells.add(new Cell(1, 5));
//使用匿名内部类的方式创建Comparator比较器
Comparator<Cell> ct = new Comparator<Cell>() {
@Override
public int compare(Cell o1, Cell o2) {
return o1.row - o2.row;
}
};
Collections.sort(cells,ct);
System.out.println(cells);
//[Cell{row=1, col=5}, Cell{row=2, col=3}, Cell{row=4, col=2}]
}
}
当然,我们也可以定义一个外部类来实现Comparator
:
//外部类,实现Comparator比较器的功能
class MyComparator implements Comparator<Cell> {
@Override
public int compare(Cell o1, Cell o2) {
return o1.row - o2.row;
}
}
使用这种方法在调用比较器时,实例化它的对象也会达到同样的效果。
5.适用场景
选择适当的集合类:
- List vs Set vs Queue: 根据需求选择使用
List
、Set
还是Queue
。如果需要有序、可重复的集合,选择List
;如果需要无序且不可重复的集合,选择Set
;如果需要先进先出的队列操作,选择Queue
。 - 选择实现类: 根据操作的特点和性能需求,选择合适的实现类。例如,如果需要随机访问和修改元素,可以选择
ArrayList
;如果需要频繁插入和删除操作,可以选择LinkedList
。 - 键值对映射: 如果需要存储键值对的映射,选择使用
Map
接口。根据是否需要有序和键的唯一性,选择HashMap
、TreeMap
等实现类。
集合性能优化的常用技巧:
- 初始容量: 在创建集合时,根据预期的元素数量设置合适的初始容量。这可以避免不必要的扩容操作,提高性能。
- 避免频繁扩容: 如果能够预估集合的大小,可以通过构造函数指定初始容量来避免频繁的扩容。
- 使用合适的集合实现: 根据操作的特点选择合适的集合实现,避免性能瓶颈。例如,避免在大量元素插入和删除时使用
ArrayList
。 - 优先级队列的注意: 使用
PriorityQueue
时,确保元素实现了正确的比较器或者可比较接口,以保证优先级顺序正确。 - 避免不必要的装箱拆箱: 当集合存储基本数据类型时,避免频繁的装箱拆箱操作,可以使用相应的基本数据类型集合类(如
IntArrayList
、LongHashSet
)。 - 并发操作: 在多线程环境下,选择合适的线程安全的集合类,或者使用同步机制来保证数据的一致性。
- 遍历优化: 遍历时使用增强的for循环或迭代器,而不是传统的for循环,以提高代码的可读性和性能。
6.实际应用案例
字典系统: 使用 HashMap
或 TreeMap
可以构建一个字典系统,其中键是单词,值是单词的释义。这在语言学习应用中非常有用,可以实现单词查找、翻译等功能。
Map<String, String> dictionary = new HashMap<>();
dictionary.put("apple", "a round fruit with red or green skin");
dictionary.put("car", "a vehicle with four wheels");
String definition = dictionary.get("apple"); // 获取单词的释义
缓存系统: 使用 LinkedHashMap
可以构建一个简单的缓存系统。在读取和写入数据时,可以将数据存储在 LinkedHashMap
中,同时使用 LRU(最近最少使用)算法来限制缓存大小,保持最常用的数据。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
String value = cache.get("key1"); // 获取缓存中的数据
7.面试准备
- ArrayList 和 LinkedList 的区别是什么?在什么情况下应该使用它们?
- HashMap 和 HashTable 之间的主要区别是什么?
- HashSet 和 TreeSet 的区别是什么?你如何保证自定义对象能够正确地在 TreeSet 中排序?
- 如何在集合中查找元素?ArrayList 和 HashSet 的查找性能有何不同?
- 什么是散列码和哈希冲突?你可以描述解决哈希冲突的几种方法吗?
- 什么是迭代器?你可以展示如何使用迭代器遍历集合吗?
- 什么是 Java 中的同步集合?它们在多线程环境中的作用是什么?
- 解释 Java 集合框架中的并发修改异常。你如何避免这种异常?
- 什么是优先级队列?你可以描述一下优先级队列的应用场景吗?
- 为什么要使用比较器?你可以给出一个使用自定义比较器的示例吗?
- 你能解释一下集合框架中的 fail-fast 和 fail-safe 是什么意思吗?
- 如何在多线程环境下安全地使用集合?你可以提供一个使用线程安全集合的场景吗?
- 什么是迭代器的快速失败属性?这对于多线程环境是否有影响?
- 如何使用
Collections
类中的方法来操作集合,例如排序和查找? - 你是否了解
ConcurrentHashMap
?它如何在多线程环境中提供高效的并发访问? - HashMap 在 jdk 1.7 和 1.8 的区别?
8.总结
Java 的 Collection 集合框架为我们提供了丰富的数据结构和操作方法,使得数据的组织和处理变得更加高效和便捷。无论你是在学习 Java,还是在实际项目中应用,深入了解集合框架都是至关重要的。通过这篇博客,大致介绍了 Collection 集合框架的基本原理、常见接口和实现类,以及它们在实际开发中的应用。继续探索和学习,希望对你的 Java 编程有所帮助。
由于还处在学习阶段,也缺乏开发经验,本文可能出现不严谨的地方甚至错误,还请大佬们多多包涵,如果能指出错误将不胜感激!
本文内容参照《Java技术核心技术 卷I(第十版)》。