理解Java集合的基本用法—Collection:List、Set 和 Queue,Map

发布于:2024-12-07 ⋅ 阅读:(28) ⋅ 点赞:(0)

本博文部分参考 博客 ,强烈推荐这篇博客,写得超级全面!!!


图片来源

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。

我们来比较一下 ArrayListLinkedList

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 有三种方法

  1. 调用toArray()方法直接返回一个Object[]数组,这种方法会丢失类型信息,所以实际应用很少。
List<String> list = List.of("apple", "pear", "banana"); // 如果我们调用List.of(),它返回的是一个只读List
Object[] array = list.toArray();
  1. 给 toArray(T[]) 传入一个类型相同的 Array,List 内部自动把元素复制到传入的 Array 中。
List<Integer> list = List.of(12, 34, 56); //如果我们调用List.of(),它返回的是一个只读List
Integer[] array = list.toArray(new Integer[list.size()]);
  1. 通过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);
}