如何正确从对象列表移除对象?Effect与Effects循环引用疑问
你说得没错,这种Effect持有Effects引用、Effects管理Effect列表的双向依赖设计确实存在明显的耦合问题——两者的职责边界变得模糊,代码可读性和可维护性都会下降,甚至可能引发内存泄漏(比如其中一方被销毁时,另一方的引用还持有它)。下面给你几个更合理的替代方案:
方案1:观察者模式解耦依赖
核心思路是让Effect不直接依赖Effects类,而是通过一个抽象的监听器接口通知“到期事件”,Effects作为监听器实现这个接口,负责处理移除逻辑。这样两者的依赖被抽象层隔离,耦合度大幅降低。
代码实现:
首先定义监听器接口:
public interface EffectExpirationListener { void onEffectExpired(Effect effect); }
修改Effect类,移除对Effects的直接依赖,改为持有监听器:
public class Effect extends Thread { private String title; private Long duration; private EffectExpirationListener listener; public Effect(String title, Long duration, EffectExpirationListener listener) { this.title = title; this.duration = duration; this.listener = listener; } @Override public void run() { try { sleep(duration * 1000); if (listener != null) { listener.onEffectExpired(this); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 正确处理中断,避免吞掉中断信号 } } // getter方法,方便监听器获取信息 public String getTitle() { return title; } }
修改Effects类,实现监听器接口,并处理移除逻辑:
import java.util.ArrayList; import java.util.List; public class Effects implements EffectExpirationListener { private List<Effect> effects = new ArrayList<>(); public void addEffect(Effect effect) { effects.add(effect); System.out.println("Added effect: " + effect.getTitle() + ", current list size: " + effects.size()); effect.start(); } @Override public void onEffectExpired(Effect effect) { effects.remove(effect); System.out.println("Removed expired effect: " + effect.getTitle() + ", current list size: " + effects.size()); // 注意:不要调用stop(),这是已废弃的方法,推荐用interrupt()配合线程内部处理 effect.interrupt(); } }
主方法不需要大改,只是创建Effects时它已经是监听器了:
import java.util.Random; import java.util.Scanner; public class Main { public static void main(String[] args) { Effects effects = new Effects(); Random random = new Random(); Scanner scanner = new Scanner(System.in); String msg; do { System.out.println("Press any key to add effect (type 'q' to quit)"); msg = scanner.nextLine(); if (!"q".equals(msg)) { effects.addEffect(new Effect("Effect-" + random.nextInt(100), (long) random.nextInt(20), effects)); } } while (!"q".equals(msg)); scanner.close(); } }
方案2:使用ScheduledExecutorService替代Thread继承
这个方案更简洁:把Effect改成普通业务类,不需要继承Thread,由Effects通过JDK自带的调度器来处理延迟移除逻辑。这样Effect完全独立,和Effects没有任何依赖。
代码实现:
首先修改Effect为普通类:
public class Effect { private String title; private Long duration; public Effect(String title, Long duration) { this.title = title; this.duration = duration; } public String getTitle() { return title; } public Long getDuration() { return duration; } }
修改Effects类,使用ScheduledExecutorService调度移除任务:
import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class Effects { private List<Effect> effects = new ArrayList<>(); private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); public void addEffect(Effect effect) { effects.add(effect); System.out.println("Added effect: " + effect.getTitle() + ", current list size: " + effects.size()); // 调度延迟任务,到期后移除该effect scheduler.schedule(() -> { effects.remove(effect); System.out.println("Removed expired effect: " + effect.getTitle() + ", current list size: " + effects.size()); }, effect.getDuration(), TimeUnit.SECONDS); } // 程序退出时记得关闭调度器 public void shutdown() { scheduler.shutdown(); } }
主方法调整:
import java.util.Random; import java.util.Scanner; public class Main { public static void main(String[] args) { Effects effects = new Effects(); Random random = new Random(); Scanner scanner = new Scanner(System.in); String msg; do { System.out.println("Press any key to add effect (type 'q' to quit)"); msg = scanner.nextLine(); if (!"q".equals(msg)) { effects.addEffect(new Effect("Effect-" + random.nextInt(100), (long) random.nextInt(20))); } } while (!"q".equals(msg)); effects.shutdown(); scanner.close(); } }
方案3:Effects主动轮询清理(适合低频率场景)
如果不想用调度器或观察者,也可以让Effects自己启动一个线程,定期检查列表中的Effect是否到期(需要Effect记录创建时间),到期则移除。这个方案的优点是Effect完全独立,但缺点是存在轮询开销,适合Effect数量少、到期频率低的场景。
代码示例:
import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; public class Effects extends Thread { private List<Effect> effects = new ArrayList<>(); private volatile boolean running = true; public Effects() { this.start(); } public void addEffect(Effect effect) { effects.add(effect); System.out.println("Added effect: " + effect.getTitle() + ", current list size: " + effects.size()); } @Override public void run() { while (running) { try { TimeUnit.SECONDS.sleep(1); // 每秒检查一次 effects.removeIf(effect -> System.currentTimeMillis() - effect.getCreateTime() >= effect.getDuration() * 1000); // 可选:打印列表变化 } catch (InterruptedException e) { Thread.currentThread().interrupt(); running = false; } } } public void stopMonitoring() { running = false; this.interrupt(); } } // Effect类 public class Effect { private String title; private Long duration; private long createTime; public Effect(String title, Long duration) { this.title = title; this.duration = duration; this.createTime = System.currentTimeMillis(); } // getter方法省略 }
方案对比与推荐
- 如果追求最低耦合:优先选择方案2(调度器),代码最简洁,职责划分最清晰。
- 如果需要扩展更多事件逻辑(比如Effect中途暂停、恢复时通知):方案1(观察者模式)更灵活,便于后续扩展。
- 如果是简单低频率场景:方案3的轮询方式也可以接受,但要注意控制轮询间隔,避免不必要的性能消耗。
另外注意:原代码中使用的Thread.stop()是已废弃的方法,它会强制终止线程,可能导致资源泄漏或数据不一致,推荐用interrupt()配合线程内部的中断检查来优雅终止线程。
内容的提问来源于stack exchange,提问作者Ilja Tarasovs

