BukkitRunnable触发空指针异常,玩家移动/出水时控制台大量报NPE如何解决
代码问题分析及修复方案
核心问题点
- 成员变量重复初始化:你将
afkCountdown、afkCountdownTask两个全局存储用的HashMap放在了PlayerMoveEvent事件方法内部初始化,该事件每秒钟会触发数十次,每次触发都会清空之前存储的所有玩家倒计时数据、任务实例。之前已经启动的定时任务运行时,从空HashMap里取值会直接返回null,拆箱为int类型时就会抛出NPE。 - 任务未正确取消:当玩家离开区域/水域时,你仅将任务实例从HashMap中移除,没有调用
cancel()方法停止定时任务,任务会持续在后台运行,访问已经被删除的键值对触发NPE。 - 定时任务逻辑缺判空:
BukkitRunnable的run方法中,直接调用afkCountdown.get(p.getUniqueId())取值,没有先判断键是否存在,值为null时拆箱直接触发NPE;且倒计时归0的逻辑存在矛盾:先移除键再重新放入,导致后续的取消逻辑永远无法触发。 - 空世界风险:创建
l1、l2、loc三个Location实例时直接调用Bukkit.getWorld("Void"),如果该世界未加载/不存在,会返回null,后续世界判断逻辑也可能触发NPE。
修复方案
第一步:调整成员变量初始化
将两个HashMap的初始化移到类成员定义阶段,不要在事件中重复初始化:
// 直接在定义时初始化,加上final避免被意外赋值 private final HashMap<UUID, Integer> afkCountdown = new HashMap<>(); private final HashMap<UUID, BukkitRunnable> afkCountdownTask = new HashMap<>();
第二步:修复事件逻辑
@EventHandler public void onPlayerMoveEvent(PlayerMoveEvent e) { Player p = e.getPlayer(); World voidWorld = Bukkit.getWorld("Void"); // 先判世界是否存在、玩家是否在Void世界,不符合直接返回,减少性能消耗 if (voidWorld == null || !p.getWorld().equals(voidWorld)) { clearPlayerTask(p.getUniqueId()); return; } // 先判断是否在液体中 if (e.getTo() == null || !e.getTo().getBlock().isLiquid()) { clearPlayerTask(p.getUniqueId()); return; } Location loc = p.getLocation(); int x = loc.getBlockX(), y = loc.getBlockY(), z = loc.getBlockZ(); // 合并坐标判断,无需多层嵌套 if (x > 52 && x <72 && y>80 && y<100 && z>255 && z<275) { UUID uuid = p.getUniqueId(); if (!afkCountdown.containsKey(uuid)) { afkCountdown.put(uuid, 5); BukkitRunnable task = new BukkitRunnable() { @Override public void run() { // 先判键是否存在,不存在直接取消任务 if (!afkCountdown.containsKey(uuid)) { cancel(); return; } int current = afkCountdown.get(uuid) -1; afkCountdown.put(uuid, current); if (current == 0) { p.sendMessage("Testing"); clearPlayerTask(uuid); } } }; afkCountdownTask.put(uuid, task); task.runTaskTimer(plugin, 20, 20); } } else { clearPlayerTask(p.getUniqueId()); } } // 封装统一的任务清理方法,避免重复代码 private void clearPlayerTask(UUID uuid) { if (afkCountdownTask.containsKey(uuid)) { // 先取消任务再移除 afkCountdownTask.get(uuid).cancel(); afkCountdownTask.remove(uuid); } afkCountdown.remove(uuid); }
内容的提问来源于stack exchange,提问作者Not_A_Dev
相关产品推荐
相关产品推荐

