ArrayDeque 不接受 null:空队列、空载荷和结束信号怎样区分
- 发布时间:2026-10-23 22:12:13
- 本文热度:浏览 2 赞 0 评论 0
- 文章标签: Java ArrayDeque 队列协议
- 全文共1字,阅读约需1分钟
ArrayDeque的poll在队列为空时返回null,往队列里放null又会直接失败。这两个设计是连在一起的:null已经用来表达没有可取元素,不能同时成为一份普通队列元素。
如果业务载荷允许空值,或者处理流程需要一个结束信号,不要试图把null塞回去。应该区分三个状态:目前没有元素、一份载荷为空的消息、生产者明确结束。它们要求调用方采取不同动作。
先把问题限制在单线程内存队列,避免用线程等待掩盖语义差别。保存为 DequeMessages.java,使用JDK 17运行 java DequeMessages.java。
import java.util.*;
public class DequeMessages {
sealed interface Message permits Data, End { }
record Data(String value) implements Message { }
enum End implements Message { INSTANCE }
record Result(List<Data> received, boolean completed) { }
static Result drain(Deque<Message> queue) {
List<Data> received = new ArrayList<>();
for (Message message; (message = queue.pollFirst()) != null; ) {
if (message instanceof Data data) received.add(data);
else if (message == End.INSTANCE) return new Result(List.copyOf(received), true);
}
return new Result(List.copyOf(received), false);
}
public static void main(String[] args) {
Deque<Message> queue = new ArrayDeque<>();
System.out.println("empty_completed=" + drain(queue).completed());
try { queue.add(null); throw new AssertionError(); }
catch (NullPointerException expected) { System.out.println("null_element=REJECTED"); }
queue.addLast(new Data(null)); queue.addLast(new Data("x")); queue.addLast(End.INSTANCE);
Result result = drain(queue);
System.out.println("received=" + result.received().stream()
.map(data -> String.valueOf(data.value())).toList());
if (result.received().get(0).value() != null) throw new AssertionError("nullable payload lost");
System.out.println("completed=" + result.completed());
try { queue.removeFirst(); throw new AssertionError(); }
catch (NoSuchElementException expected) { System.out.println("empty_remove=EXCEPTION"); }
}
}
输出:
empty_completed=false
null_element=REJECTED
received=[null, x]
completed=true
empty_remove=EXCEPTION
poll 的空,不是“业务已经结束”
队列当前为空,只能说明此刻没有元素。另一段代码以后还可以继续添加。若处理器把第一次poll为null就当成永久完成,它实际是在消费一份当前快照,不是在遵循生产完成协议。
removeFirst则在空队列上抛异常,适合调用方已经明确知道应当有元素的情境。两者差异属于空状态的反馈形式,不代表removeFirst能够等待生产者,也不代表换成异常就拥有可靠结束信号。
示例的End是一个真正入队的非空对象。只有取到它才标记completed,普通Data即使内部value为null,也仍是一份应处理的数据。封装对象把“消息存在”与“消息携带什么”分成了两层。Result继续保存Data对象,因此内部空值没有被转换成字符串。控制台为了展示才调用String.valueOf;屏幕上的null字样不能单独证明它是空值,程序还通过value() == null的断言核对。若把展示文本拿回去当业务数据,真实空值就会与字面字符串“null”混淆。
一个枚举结束项,比保留魔法字符串清楚
如果用字符串STOP当结束信号,就必须禁止普通数据等于STOP,或者增加转义协议。类型化消息让结束项与业务值不处在同一命名空间。sealed接口还把当前允许的消息种类限定在这份模型里,维护者新增消息类型时必须重新审视消费逻辑。
End只说明这段生产流程按约定不再发送后续数据,不自动证明前面的业务动作已经持久成功。若Data处理失败,需要决定立即失败、记录错误继续,还是把任务交给重试机制。不能因为队列最后有End,就把中途异常也算成功结束。
还要约定谁能发送End。对于本例的单生产者序列,生产者在全部Data入队后放一个End即可;若End之后还能继续入队,消费者已经返回,后面的内容就会被留下。生产完成是一项协议,不是一个随便可调用的add。
ArrayDeque 不会因为换了消息类型就支持并发
本例刻意顺序入队、顺序消费。ArrayDeque不是阻塞队列,也不提供多线程并发访问所需的同步。真实生产者和消费者处在不同线程时,应选择适合的并发结构与等待机制,不能在poll为null时靠循环自旋等待,也不能只给add加一把与poll不同的锁。
多消费者的结束还需要额外设计:一个End通常只被一个消费者取走,其他消费者怎样退出,必须由协调者明确安排。多生产者则需要确认所有生产者都已完成,不能由最快结束的一方提前宣布整个流程结束。
这些扩展不需要硬塞进这个小例子。先保持明确作用域:一个非并发队列,一份可空载荷的Data,一个只在生产结束时发送的End。只要这层模型不含混,后面迁到BlockingQueue或异步事件循环时,就不必重新猜null到底代表什么。
如果数据根本不允许为空,可以直接在Data构造阶段拒绝null。本文允许它,是为了验证三种状态可区分,而不是建议所有业务都支持空载荷。状态是否存在由业务决定,容器的null规则只是要求我们把这个决定表达出来。