1. 简介
Java 集合是 Java 程序设计语言中最基本的数据结构,用于存储和管理一组相似或者不相似的元素。Java 中的集合可以被分为两种类型:集合(Collection)和映射(Map)。
Collection 接口代表一组元素,这些元素也称为集合的成员。Map 接口代表一个映射关系,它将关键字(键,key)和值(value)一起存储在一个接口中。
2. 集合框架的概览
Java 集合框架提供了一系列的接口和实现类,包括 List、Set、Queue、Deque 和 Map 等。
- List:有序的 Collection,可以包含重复的元素。主要实现类有 ArrayList、LinkedList 等。
- Set:无序的 Collection,不包含重复的元素。主要实现类有 HashSet、LinkedHashSet 和 TreeSet 等。
- Queue/Deque:特别的 Collection,用于通过队列操作元素。主要实现类有 LinkedList、 PriorityQueue 等。
- Map:包含键值对,键唯一,值可以重复。主要实现类有 HashMap、LinkedHashMap、TreeMap 等。
2.1 List
List 是有序集合,可以包含重复元素。它提供了通过索引访问元素的能力。以下是一个使用 ArrayList 的简单示例:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("Java");
System.out.println(list.get(0)); // 输出 "Java"
}
}
2.1.1 ArrayList
ArrayList 在内部使用动态数组来保存数据。相对于常规数组,ArrayList 可以动态调整其容量大小。
如果在没有指定初始容量的情况下创建一个实例,那么它的默认初始容量是10。这个值是在ArrayList的源代码中定义的,具体定义在ArrayList类的源码中,作为一个名为DEFAULT_CAPACITY的静态常量。当添加元素时,如果当前数组已满,ArrayList 将创建一个新的更大的数组(增长策略是将原来的容量增加到原来的1.5倍,然后再加1。这是为了在添加大量元素时能提高性能),然后将老数组中的所有元素复制到新数组中。这是一个代价较大的操作,因此,如果可能,最好在创建 ArrayList 时预估大小。
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>(10); // 预估大小为 10
list.add("Java");
list.add("Python");
}
}
2.1.2 LinkedList
LinkedList 使用双向链表实现。每个元素(节点)在内存中都有一个前驱和后继指针。这意味着,我们可以在常数时间内在任何位置插入或删除元素,但寻找一个元素需要遍历整个列表。
LinkedList 是基于双向链表实现的,它不需要进行扩容。链表结构允许 LinkedList 动态地添加或删除元素,因为它不需要连续的内存空间。当你向 LinkedList 添加元素时,会创建一个新的链表节点,并将其链接到链表中,不需要重新分配整个链表的内存空间。
因此,ArrayList 和 LinkedList 的扩容方式是不同的,这也导致了它们在处理大量动态添加和删除操作时的性能特性有所不同。在选择使用哪种列表实现时,你应该根据你的具体需求来决定。
2.2 Set
Set 是无序的集合,不能包含重复元素。以下是一个使用 HashSet 的示例:
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("Java");
set.add("Python");
set.add("Java");
System.out.println(set.size()); // 输出 2,因为 "Java" 只被计算了一次
}
}
2.2.1 HashSet
HashSet 使用 HashMap 作为其内部数据结构,元素的哈希值决定了其在 HashSet 中的位置。因此,插入、删除和查找的时间复杂度可以接近 O(1)。当我们在没有指定初始容量的情况下创建一个HashSet时,其初始容量是16,加载因子是0.75。这意味着当HashSet的容量达到75%时,它将会增长。增长策略是将容量扩大一倍。
这是与ArrayList的一个重要区别。对于ArrayList,增长策略是将容量增加到原来的1.5倍。
这种不同的增长策略反映了这两种数据结构的设计理念和用途。HashSet是设计用来快速查找元素的,所以它的扩容策略偏向于保持较低的填充率,以保持良好的查找性能。而ArrayList是一种动态数组,它的扩容策略偏向于最小化扩容次数,以提高添加和删除元素的效率。
2.3 Queue/Deque
Queue 是一种特殊的集合,它的元素排队等待处理。Deque 是双端队列,可以在两端插入和删除元素。以下是一个 PriorityQueue 示例:
import java.util.PriorityQueue;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Queue<String> queue = new PriorityQueue<>();
queue.add("Java");
queue.add("Python");
System.out.println(queue.poll()); // 输出 "Java",并从队列中移除
}
}
对于Java中的Queue接口和其实现,包括LinkedList,PriorityQueue等,扩容策略会根据不同的实现类有所不同。例如,LinkedList无需扩容,因为它是基于链表实现的,所以可以动态添加和删除元素。而PriorityQueue在创建时默认初始容量是11,当元素数量超过容量时,会扩容为原来的两倍。
2.4 Map
Map 是一种集合,用于存储键值对。以下是一个 HashMap 的示例:
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Java", 1);
map.put("Python", 2);
System.out.println(map.get("Java")); // 输出 1
}
}
对于Map接口及其实现,包括HashMap,LinkedHashMap,TreeMap等,HashMap和LinkedHashMap的扩容策略类似于HashSet。默认初始容量是16,加载因子为0.75,当容量达到75%时,会扩容为原来的两倍。而TreeMap则是基于红黑树实现的,所以无需考虑扩容问题。
2.4.1 HashMap
HashMap 也使用哈希表作为其内部数据结构。每个键值对被存储在 HashMap.Entry 对象中,这些对象存储在桶中,它们的位置由键的哈希值决定。
当两个不同的键具有相同的哈希值时,会发生冲突,此时,这些键值对将以链表的形式存储在同一个桶中。但是,当链表长度超过一定阈值(默认为 8)时,链表将转换为红黑树,以提高性能。
当插入新的键值对导致 HashMap 大小超过其阈值(默认加载因子为 0.75)时,HashMap 会进行一次重新哈希(rehash),创建一个新的更大的表,并将旧表中的所有数据复制到新表中。
3. 集合的迭代
在 Java 中,我们可以使用迭代器(Iterator)或者增强的 for 循环来遍历集合。以下是使用迭代器遍历 List 和 Set 的示例:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
Iterator<String> listIterator = list.iterator();
while (listIterator.hasNext()) {
System.out.println(listIterator.next());
}
Set<String> set = new HashSet<>();
set.add("Java");
set.add("Python");
Iterator<String> setIterator = set.iterator();
while (setIterator.hasNext()) {
System.out.println(setIterator.next());
}
}
}
对于 Map,我们可以使用 entrySet 或 keySet 方法获得迭代器:
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Java", 1);
map.put("Python", 2);
Iterator<Map.Entry<String, Integer>> mapIterator = map.entrySet().iterator();
while (mapIterator.hasNext()) {
Map.Entry<String, Integer> entry = mapIterator.next();
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}
4 线程安全
Java 集合框架中的大部分实现类都不是线程安全的,如 ArrayList、LinkedList、HashSet 和 HashMap 等。
Java 提供了几种方法来获取线程安全的集合:
使用 Collections.synchronized* 方法:这些方法返回原始集合的包装类,所有的访问都通过一个同步的(synchronized)块进行。
使用并发集合:Java 并发包(java.util.concurrent)提供了几个线程安全的集合,如 CopyOnWriteArrayList、ConcurrentHashMap 等。
尽管获取线程安全的集合很容易,但在多线程环境中正确地使用它们却需要深入理解并发编程的复杂性。
Java 集合框架是 Java 编程中最重要的部分之一,理解和熟练掌握这个框架对于 Java 编程是非常重要的。希望本文能够对你有所帮助!值得注意的是,以上所述的初始容量和扩容策略是Java 8中的实现,在不同的Java版本中可能会有所不同。如果你需要考虑性能问题,你应该查阅你正在使用的Java版本的相关文档。