先赞后看,南哥助你Java进阶一大半
PriorityQueue
的底层数据结构就如andrewlock.net
网站提供的图一样,虽然PriorityQueue
是一个平衡二叉堆,但JDK底层的实现却是:一个普普通通的二维数组!!
我是南哥,一个Java学习与进阶的领路人,相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。
⭐⭐⭐本文收录在《Java学习/进阶/面试指南》:https://github..JavaSouth
1. Queue集合
1.1 Queue集合概述
面试官:你说说Queue集合都有什么常用类?
JDK源码对Queue集合是这么解释的,大家看看。
A collection designed for holding elements prior to processing.
专为在处理之前保存元素而设计的集合。
南哥是这么理解的,List集合用于存储常用元素、Map集合用于存储具有映射关系的元素、Set集合用于存储唯一性的元素。Queue集合呢?所有的数据结构都是为了解决业务问题而生,而Queue集合这种数据结构能够存储具有先后时间关系的元素,很适用于在业务高峰期,需要缓存当前任务的业务场景。像Kafka、RabbitMQ、RocketMQ都是队列任务这种思想。
Queue集合底层接口提供的方法很简单,一共有 6 个。
代码语言:java复制 // 添加元素。没有可用元素则简单返回false
boolean offer(E e);
代码语言:java复制 // 添加元素。没有可用元素则抛出IllegalStateException
boolean add(E e);
代码语言:java复制 // 移除队列的头部元素。如果此队列为空则返回null 。
E poll();
代码语言:java复制 // 移除队列的头部元素。该方法和poll不同之处在于,如果此队列为空,则抛出异常。
E remove();
代码语言:java复制 // 检索但不移除此队列的头部。如果此队列为空,则返回null 。
E peek();
代码语言:java复制 // 检索但不移除此队列的头部。该方法和peek唯一不同之处在于,如果此队列为空,则抛出异常
E element();
Queue集合常用的实现类如下,我会一一讲来。包括双端队列的两个实现类:LinkedList、ArrayDeque,优先级队列:PriorityQueue,线程安全相关的Queue实现类:LinkedBlockingQueue、ArrayBlockingQueue、ConcurrentLinkedQueue。
1.2 双端队列
面试官:双端队列的两个实现类知道吧?
双端队列是Queue集合的一个子接口,顾名思义相比普通队列来说,双端队列可以往前、也可以往后顺序插入元素。比如我下面给出一段队列的初始化。
代码语言:java复制 Queue<Integer> queue = new LinkedList<>();
Deque<Integer> deque = new LinkedList<>();
queue.add(1);
deque.addLast(1);
deque.addFirst(1);
同样是new LinkedList<>()
来实例化队列,如果使用双端队列Deque接口,那这个队列就可以使用addFirst
、addLast
等双端队列特有的方法。
有南友就会问:那ArrayQueue呢?这两者都是双端队列Deque的底层实现,但底层数据结构不同,LinkedList底层数据结构是一个双向链表,看看它有前指针next
、后指针prev
。
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
而ArrayDeque底层数据结构是一个Object类型的数组。
代码语言:java复制 transient Object[] elements;
为什么要这么设计呢?其实这两种不同的设计就可以高效适用于不同的业务场景。双向链表实现的Deque随机查找的性能差,但插入、删除元素时性能非常出色,适用于修改操作更频繁的业务场景。
而数组实现的Deque,也就是ArrayDeque,它的插入、删除操作性能不如前者,但随机查找的性能非常出色,更适用于查询操作更频繁的业务场景。
1.3 优先级队列
面试官:优先级队列有什么作用?
优先级队列的实现类叫PriorityQueue,PriorityQueue虽然属于队列的一份子,不过它违背了队列最基本的原则:FIFO先进先出原则。它背叛了组织!
PriorityQueue的特性是它并不按常规队列一样顺序存储,而是根据元素的自然顺序进行排序,使用出队列的方法也是输出当前优先级最高的元素。例如以下代码输出的一样。
代码语言:java复制 public static void main(String[] args) {
Queue<Integer> queue = new PriorityQueue<>();
queue.offer(6);
queue.offer(1);
queue.offer(3);
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
}
代码语言:shell复制# 执行结果
1
3
6
但如果我们直接打印PriorityQueue的所有元素,发现他其实并不是按元素的自然顺序进行存储。
代码语言:java复制 public static void main(String[] args) {
Queue<Integer> queue = new PriorityQueue<>();
queue.offer(6);
queue.offer(1);
queue.offer(3);
System.out.println(queue);
}
代码语言:shell复制# 执行结果
[1, 6, 3]
why?其实PriorityQueue底层数据结构是一个平衡二叉堆:transient Object[] queue
,如果你直接打印,打印的是堆里面的存储元素。
对于PriorityQueue来说,它只保证你使用poll()
操作时,返回的是队列中最小、或最大的元素。
1.4 阻塞队列
面试官:阻塞队列呢?
JDK提供的阻塞队列接口为BlockingQueue,南哥先说说BlockingQueue的子类实现之一:ArrayBlockingQueue。
阻塞队列的特别之处在于当生产者线程会往队列放入元素时,如果队列已满,则生产者线程会进入阻塞状态;而当消费者线程往队列取出元素时,如果队列空了,则消费者线程会进入阻塞状态。
但队列的状态从满变为不满,消费者线程会唤醒生产者线程继续生产;队列的状态从空变为不空,生产者线程会唤醒消费者线程继续消费。
所以ArrayBlockingQueue很适合用于实现生产者、消费者场景。大家看看这个Demo。
代码语言:java复制public class Test {
public static void main(String[] args) {
// 创建一个容量为 3 的ArrayBlockingQueue
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
// 创建并启动生产者线程
Thread producerThread = new Thread(new Producer(queue));
Thread consumerThread = new Thread(new Consumer(queue));
producerThread.start();
consumerThread.start();
}
}
// 生产者类
class Producer implements Runnable {
private BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
for (int i = 1; i <= 6; i ) {
System.out.println("生产者生产了: " i);
queue.put(i);
Thread.sleep(150); // 模拟生产过程中的延迟
}
queue.put(-1); // 使用特殊值表示结束生产
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 消费者类
class Consumer implements Runnable {
private BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
Integer item = queue.take();
if (item == -1) {
break; // 遇到特殊值,退出循环
}
System.out.println("消费者消费了: " item);
Thread.sleep(100); // 模拟消费过程中的延迟
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
代码语言:shell复制# 执行结果
生产者生产了: 1
消费者消费了: 1
生产者生产了: 2
消费者消费了: 2
生产者生产了: 3
消费者消费了: 3
生产者生产了: 4
消费者消费了: 4
生产者生产了: 5
消费者消费了: 5
生产者生产了: 6
消费者消费了: 6
LinkedBlockingQueue也是阻塞队列的实现之一,不过它和上面的ArrayBlockingQueue区别在于底层数据结构是由双向链表
进行实现。
// LinkedBlockingQueue源码双向链表
transient Node<E> head;
private transient Node<E> last;
戳这,《JavaSouth》作为一份涵盖Java程序员所需掌握核心知识、面试重点的《Java学习进阶指南》。
我是南哥,南就南在Get到你的有趣评论➕点赞➕关注。
创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️