理解Java集合的基本用法—Collection:List、Set 和 Queue,Map
本博文部分参考 博客 ,强烈推荐这篇博客,写得超级全面!!!
图片来源
Java 集合框架
主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合(单列集合);另一种是 Map ,存储键/值对映射。
Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类。
-
Collection:所有单列集合的根接口。
-
List:有序、允许重复元素的集合。
-
Set:无序、不允许重复元素的集合。
-
Queue:用于存储按特定顺序处理的元素(如先进先出 FIFO)。
-
-
Map:映射接口(双列接口),用于存储键值对(Key-Value),提供对键值的快速查找。
图片来源
由于Java的集合设计非常久远,中间经历过大规模改进,我们要注意到有一小部分集合类是遗留类,不应该继续使用:
- Hashtable:一种线程安全的Map实现;
- Vector:一种线程安全的List实现;
- Stack:基于Vector实现的LIFO的栈。
还有一小部分接口是遗留接口,也不应该继续使用:
- Enumeration:已被Iterator取代。
List(有序集合,允许重复元素)
List 是一个有序的集合,元素按插入顺序排列,允许重复元素。
实现类:
- ArrayList 基于动态数组实现,支持随机访问和快速查找,但插入和删除操作的性能较差,尤其是在中间位置操作时。
- LinkedList 基于双向链表实现,插入和删除操作效率较高,适合频繁的插入和删除,但随机访问的性能差。
- Vector(ArrayList是非线程安全的,效率高;Vector是基于线程安全的 List 实现,效率低 ,遗留类,通常不推荐使用) 。
ArrayList 是最常用的 List 实现类。
List<String> list = new ArrayList<>(); // 初始化空列表
List<Integer> list = new ArrayList<>(10); //初始化容量为 10 的列表
List<Integer> list = new ArrayList<>(Arrays.asList(3, 4, 5)); //初始化带有元素 3, 4, 5 的列表
List<Integer> list = new ArrayList<>(other_list); //用其他列表 list 初始化
常用方法:
- boolean add(E element):在末尾添加一个元素。
- boolean add(int index, E element):在指定索引添加一个元素。
- E get(int index):获取指定索引的元素。
- E set(int index, E element):设置指定位置的元素,返回先前在 index 处出现的元素。
- boolean remove(Object o):删除指定元素。
- E remove(int index):删除指定索引的元素。
- int size():获取集合大小。
- boolean contains(Object o):判断是否包含某元素。
- int indexOf(Object o):可以返回某个元素的索引,如果元素不存在,就返回-1。
我们来比较一下 ArrayList 和 LinkedList :
ArrayList | LinkedList | |
---|---|---|
获取指定元素 | 速度很快 | 需要从头开始查找元素 |
添加元素到末尾 | 速度很快 | 速度很快 |
在指定位置添加/删除 | 需要移动元素 | 不需要移动元素 |
内存占用 | 少 | 较大 |
通常情况下,我们总是优先使用 ArrayList。
另外,我们要始终坚持使用迭代器 Iterator 来访问 List ,因为总是具有最高的访问效率。
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
}
}
Java 的 for each 循环本身可以帮我们使用 Iterator 遍历。可以把上面的代码改写成:
ublic class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (String s : list) {
System.out.println(s);
}
}
}
List 可以和 Array 相互转换:
把 List 变为 Array 有三种方法:
- 调用toArray()方法直接返回一个Object[]数组,这种方法会丢失类型信息,所以实际应用很少。
List<String> list = List.of("apple", "pear", "banana"); // 如果我们调用List.of(),它返回的是一个只读List
Object[] array = list.toArray();
- 给 toArray(T[]) 传入一个类型相同的 Array,List 内部自动把元素复制到传入的 Array 中。
List<Integer> list = List.of(12, 34, 56); //如果我们调用List.of(),它返回的是一个只读List
Integer[] array = list.toArray(new Integer[list.size()]);
- 通过List接口定义的T[] toArray(IntFunction<T[]> generator)方法。
Integer[] array = list.toArray(Integer[]::new);
把 Array 变为 List,通过 List.of(T…) 方法最简单:
Integer[] array = { 1, 2, 3 };
List<Integer> list = List.of(array);
总结
-
List 是按索引顺序访问的长度可变的有序表,优先使用 ArrayList 而不是 LinkedList;
-
可以直接使用 for each 遍历 List;
-
List 可以和 Array 相互转换。
Set(无序集合,不允许重复元素)
Set 是一个不允许重复元素的集合,它不保证元素的顺序。Set 实际上相当于只存储 key、不存储 value 的 Map。我们经常用 Set 用于去除重复元素。
Set 接口并不保证有序,而 SortedSet 接口则保证元素是有序的。PS:注意输出的顺序既不是添加的顺序,也不是 String 或 Integer 排序的顺序,在不同版本的JDK中,这个顺序也可能是不同的。
实现类:
- HashSet 基于哈希表实现,元素无序,性能较好,适合用于查找。它实现了Set接口,并没有实现SortedSet接口;
- LinkedHashSet 基于哈希表和链表实现,元素有序(按插入顺序)。
- TreeSet 基于红黑树实现,元素按自然顺序(或提供的 Comparator)排序。它实现了SortedSet接口。
HashSet 是 Set 接口最常用的实现
Set<String> set = new HashSet<>();
常用方法:
- boolean add(E e):添加元素。
- boolean remove(Object o):删除指定元素。
- boolean contains(Object o):判断是否包含某元素。
- int size():获取集合大小。
- clear():清空集合。
Queue / Deque
Queue 是一个先进先出(FIFO:First In First Out)的集合,只能一头进,另一头出。
实现类:
-
PriorityQueue:基于优先级堆实现,支持优先级排序的队列。PriorityQueue 并不是一个比较标准的队列实现,PriorityQueue 保存队列元素的顺序并不是按照加入队列的顺序,而是按照队列元素的大小进行重新排序。
-
Deque:Queue 是队列,只能一头进,另一头出。双端队列 Deque(Double Ended Queue)允许两头都进,两头都出。
-
ArrayDeque:基于数组实现,作为栈或队列使用,效率较高。
-
LinkedList:实现了 Queue 接口,适合用于队列操作。
-
Queue<String> queue = new LinkedList<>();
Queue
常用方法:
- boolean add(E) / boolean offer(E):添加元素到队尾;
- E remove() / E poll():获取队首元素并从队列中删除;
- E element() / E peek():获取队首元素但并不从队列中删除。
- int size():获取队列大小。
对于具体的实现类,有的Queue有最大队列长度限制,有的Queue没有。注意到添加、删除和获取队列元素总是有两个方法,这是因为在添加或获取元素失败时,这两个方法的行为是不同的:
throw Exception | 返回false或null | |
---|---|---|
添加元素到队尾 | add(E e) | boolean offer(E e) |
取队首元素并删除 | E remove() | E poll() |
取队首元素但不删除 | E element() | E peek() |
注意:不要把 null 添加到队列中,否则 poll() 方法返回 null 时,很难确定是取到了 null 元素还是队列为空。
Deque
Deque 接口继承自 Queue 接口
Queue 和 Deque 出队和入队的方法比较:
Queue | Deque | |
---|---|---|
添加元素到队尾 | add(E e) / offer(E e) | addLast(E e) / offerLast(E e) |
取队首元素并删除 | E remove() / E poll() | E removeFirst() / E pollFirst() |
取队首元素但不删除 | E element() / E peek() | E getFirst() / E peekFirst() |
添加元素到队首 | 无 | addFirst(E e) / offerFirst(E e) |
取队尾元素并删除 | 无 | E removeLast() / E pollLast() |
取队尾元素但不删除 | 无 | E getLast() / E peekLast() |
Deque 接口实际上扩展自 Queue,因此,Queue 提供的 add()/offer() 方法在 Deque 中也可以使用,但是,使用 Deque,最好不要调用offer(),而是调用 offerLast()。即使用 Deque,推荐总是明确调用 offerLast() / offerFirst() 或者 pollFirst() / pollLast() 方法。
Deque 是一个接口,它的实现类有 ArrayDeque 和 LinkedList。
LinkedList,它即是List,又是Queue,还是Deque。但是我们在使用的时候,总是用特定的接口来引用它,这是因为持有接口说明代码的抽象层次更高,而且接口本身定义的方法代表了特定的用途。
// 不推荐的写法:
LinkedList<String> d1 = new LinkedList<>();
// 推荐的写法:
Deque<String> d2 = new LinkedList<>();
可见面向抽象编程的一个原则就是:尽量持有接口,而不是具体的实现类。
总结
Deque实现了一个双端队列(Double Ended Queue),它可以:
-
将元素添加到队尾或队首:addLast()/offerLast()/addFirst()/offerFirst();
-
从队首/队尾获取元素并删除:removeFirst()/pollFirst()/removeLast()/pollLast();
-
从队首/队尾获取元素但不删除:getFirst()/peekFirst()/getLast()/peekLast();
-
总是调用xxxFirst()/xxxLast()以便与Queue的方法区分开;
-
避免把null添加到队列。
Stack
栈(Stack)是一种后进先出(LIFO:Last In First Out)的数据结构,元素的插入和删除操作都发生在栈的顶端。虽然 Stack 类是 Java 提供的传统类,但它已被 Deque 接口的 ArrayDeque 替代,ArrayDeque 提供了更高效的栈操作。常见实现:Stack(遗留类,较旧,不推荐使用)、ArrayDeque(推荐使用)。
Deque<String> stack = new ArrayDeque<>();
在Java中,我们用 Deque 可以实现 Stack 的功能:
-
把元素压栈:push(E) / addFirst(E);
-
把栈顶的元素“弹出”:pop() / removeFirst();
-
取栈顶元素但不弹出:peek() / peekFirst()。
为什么 Java 的集合类没有单独的 Stack 接口呢?因为有个遗留类名字就叫 Stack,出于兼容性考虑,所以没办法创建 Stack 接口,只能用Deque 接口来“模拟”一个 Stack 了。
当我们把 Deque 作为 Stack 使用时,注意只调用 push() / pop() / peek() 方法,不要调用 addFirst() / removeFirst() / peekFirst() 方法,这样代码更加清晰。
Map(映射,存储键值对)
Map 是一个存储键值对(key-value)的集合,Map 中的键是唯一的,值可以重复。
实现类:
-
HashMap:基于哈希表实现,查找和插入的性能较高,元素无序。(HashMap 非线程安全,高效,支持null)
-
LinkedHashMap:保持插入顺序的 HashMap 实现。
-
TreeMap:基于红黑树实现,元素按键的自然顺序或指定的 Comparator 排序。
-
Hashtable:遗留类,过时的线程安全版本,不推荐使用。(HashTable 线程安全,低效,不支持null )
HashMap 是 Map 接口最常用的实现,基于哈希表实现。它在内部会对 Key 进行排序,这种 Map 就是 SortedMap。注意到 SortedMap 是接口,它的实现类是 TreeMap。
Map<String, String> map = new HashMap<>();
常用方法:
-
put(K key, V value):添加键值对。
-
get(Object key):根据键获取对应的值。
-
remove(Object key):删除指定键的键值对。
-
boolean containsKey(Object key):判断是否包含指定键。
-
containsValue(Object value):判断是否包含指定值。
-
keySet():获取所有键。
-
values():获取所有值。
PS: Map 中不存在重复的 key,因为放入相同的 key ,只会把原有的 key-value 对应的 value 给替换掉。
遍历 Map (无序,既不是插入顺序,也不是某个逻辑下的排序顺序)
for (String key : map.keySet()) { // keySet()方法返回的Set集合,它包含不重复的key的集合
Integer value = map.get(key);
System.out.println(key + " = " + value);
}
for (Map.Entry<String, Integer> entry : map.entrySet()) { // entrySet()集合,它包含每一个key-value映射
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}