You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确从对象列表移除对象?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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 14:42:49