在创建时注册为侦听器的Java枚举

时间:2014-12-12 09:38:53

标签: java enums singleton listener

我试图结合并失败的有两种好的(大多数人都认为)java实践。

  1. 永远不要在构造函数中泄露 this
  2. 使用枚举代替单件模式。
  3. 所以,我想要一个单例,一旦创建,就会听一些事件。这是一个例子。首先,事件监听器接口:

    public interface EventListener {
        void doSomething();
    }
    

    然后,事件制作人:

    public class EventProducer implements Runnable{
    
        private EventListener listener;
    
        public EventProducer(EventListener listener) {
            if (listener == null) {
                throw new NullPointerException("Listener should not be null.");
            }
            this.listener = listener;
        }
    
        @Override
        public void run() {
            listener.doSomething(); //This may run before the listener is initialized.
            do {
                long startTime = System.currentTimeMillis();
                long currentTime;
                do {
                    currentTime = System.currentTimeMillis();
                } while ((currentTime - startTime) < 1000);
                listener.doSomething();
            } while (!Thread.currentThread().isInterrupted());
            listener = null; //Release the reference so the listener may be GCed
        }
    }
    

    然后,enum(作为第二个列出的java实践建议):

    public enum ListenerEnum implements EventListener{
    
        INSTANCE;
    
        private int counter;
        private final ExecutorService exec;
    
        private ListenerEnum() {
            EventProducer ep = new EventProducer(this); //Automatically unregisters when the producer is done.
            counter = 0;
            exec = Executors.newSingleThreadExecutor();
            exec.submit(ep);
        }
    
        @Override
        public void doSomething() {
            System.out.println("Did something.");
            counter++;
            if (counter >= 5) {
                exec.shutdownNow();
            }
        }
    }
    

    最后,有些事情要开始:

    public class TestRunner {
    
        public static void main(String[] args) {
            ListenerEnum.INSTANCE.doSomething();
        }
    
    }
    

    问题在于 ListenerEnum 构造函数的第一行,因为我们正在泄漏 this ,因此不符合第一个列出的java实践。这就是为什么我们的事件生成器可以在构造监听器之前调用监听器的方法。

    我该如何处理?通常我会使用Builder模式,但是如何使用枚举?

    修改 对于那些重要的事情,我的程序中的事件生成器实际上扩展了BroadcastReceiver,所以我的枚举不能是事件生成器,它们必须是分开的。生成器是在枚举的构造函数中创建的(作为示例),稍后以编程方式注册。所以我实际上没有泄漏这个的问题。不过,我想知道我是否可以避免它。

    编辑2:

    好的,既然有解决我问题的建议,我想澄清一些事情。首先,大多数建议都是解决方法。他们建议以完全不同的方式做同样的事情。我很欣赏这些建议,并且可能会接受一个作为答案并实施它。但真正的问题应该是“如何使用枚举实现Builder模式?”我已经知道并且人们建议的答案是“你没有,做其他方式。”。是否有人可以发布类似“你这样做!你这样做。”?

    我被要求提供接近我实际用例的代码。修改以下内容:

    public enum ListenerEnum implements EventListener{
    
        INSTANCE;
    
        private EventProducer ep;
        private int counter;
        private ExecutorService exec;
    
        private ListenerEnum() {
            ep = new EventProducer(this); //Automatically unregisters when the producer is done.
            counter = 0;
        }
    
        public void startGettingEvents() {
            exec = Executors.newSingleThreadExecutor();
            exec.submit(ep);
        }
    
        public void stopGettingEvents() {
            exec.shutdownNow();
        }
    
        @Override
        public void doSomething() {
            System.out.println("Did something.");
            counter++;
            if (counter >= 5) {
                stopGettingEvents();
            }
        }
    }
    

    以及:

    public class TestRunner {
    
        public static void main(String[] args) {
            ListenerEnum.INSTANCE.startGettingEvents();
        }
    
    }
    

    现在我要解决的问题是将EventsProducer创建移动到 startGettingEvents()方法。而已。但这也是一种解决方法。我想知道的是:一般来说,你如何避免在监听器枚举的构造函数中泄露 this ,因为你不能使用Builder模式?或者你真的可以使用枚举的Builder模式吗?是否仅根据具体情况通过变通方法完成?或者是否有一种解决这个问题的一般方法,我不知道?

3 个答案:

答案 0 :(得分:3)

只需创建一个静态初始化块:

public enum ListenerEnum implements EventListener{

    INSTANCE;

    private int counter;
    private static final ExecutorService exec; //this looks strange. I'd move this service out of enum.
    private static final EventProducer ep;

    static{
            exec = Executors.newSingleThreadExecutor();
            ep = new EventProducer(INSTANCE); //Automatically unregisters when the producer is done.
            exec.submit(ep); 
    }

    @Override
    public void doSomething() {
        System.out.println("Did something.");
        counter++;
        if (counter >= 5) {
            exec.shutdownNow();
        }
    }
}

只要枚举值是final和static,它们are initialized before就是静态初始化块。如果您对enum进行反编译,您将看到一个初始化块:

        static{
                INSTANCE = new ListenerEnum();
                exec.submit(INSTANCE.ep); 
        }

答案 1 :(得分:2)

首先,考虑为什么 this不应该逃脱:

  1. 如果实例发布不当,您将失去final字段安全发布保证
  2. 即使安全发布,在泄漏时构造函数中未执行的所有操作也存在不一致
  3. 在子类的情况下,你将逃脱一个不完整的实例,因为到目前为止尚未调用子类的构造函数
  4. 在这种狭隘的情况下,这不适用于你。提交到Executor不是一个不正确的出版物,除了你在构造函数中实现的那个之外,enum不能以任何其他方式逃脱。它是构造函数中的最后一件事,而enum s不能有子类。


    既然你已经编辑了你的问题,那就更没意义了。构造函数

    private ListenerEnum() {
        ep = new EventProducer(this);
        counter = 0;
    }
    
    只要this不是ep变量并且static的构造函数不泄漏EventProducer

    就不是“泄漏this”同样。这很重要,因为程序员必须能够创建圆形对象图而不必担心突然的不一致。

    但是你仍然没有什么可以轻松的。如上所述,它依赖于EventProducer关于泄漏的行为,并且EventProducer不得回调ListenerEnum这可能会破坏事情而不会“泄漏this” ,技术上。毕竟,您可以在不破坏线程安全的情况下创建破解的代码。

    因此,当您需要了解其他课程时,您无法看到正确性的代码。

    有些用例将this传递给另一个对象因为众所周知的行为而被认为是安全的,例如: weakThis=new WeakReference(this);是一个真实的例子。但是,将this传递给名为EventProducer的内容可能会让每个读者都响起警钟,即使您确定知道它是虚假警报也是如此。


    然而,大的设计气味在于单独使用Singleton模式。毕竟,您创建的每个实例都是独一无二的。 Singleton模式的特殊之处在于它提供了对该实例的全局public访问。这真的是你想要的吗?您是否认为通过使用Singleton模式,整个应用程序中的每个人都可以再次注册该侦听器?

答案 2 :(得分:1)

您的班级是单身人士(无论是基于词汇还是其他)与您的问题无关。您的问题只是如何在对象的构造函数中注册侦听器。答案是:安全无法实现。

我建议你做两件事:

  • 确保您的听众不会错过事件,方法是让队列轮询工作。这样,如果它暂时不听,那么工作就会排队。事实上,这意味着它并不需要成为传统意义上的倾听者。它只需要在队列中进行轮询。

  • 使用单独的方法将类注册为侦听器,如注释中所述。

我会考虑避免单身人士。它没有提供许多优势(除了能够从任何地方呼叫SomeClass.INSTANCE的微小优势)。在测试过程中,最强烈的感觉是,在你想要测试而不实际通过网络发送内容时,你会发现很难嘲笑课程。


这是一个具体的例子,说明为什么泄漏this在您的情况下是危险的。在将counter设置为零之前,构造函数会传递this

private ListenerEnum() {
    ep = new EventProducer(this);
    counter = 0;
}

现在,只要this转义,您的事件生成器可能会在构造函数完成之前调用doSomething() 5次:

@Override
public void doSomething() {
    System.out.println("Did something.");
    counter++;
    if (counter >= 5) {
        exec.shutdownNow();
    }
}

这种方法的第六次调用应该失败吗?除了你的构造函数现在完成并设置counter = 0;。从而允许制作人多次拨打doSomething() 5次。

注意:如果重新排序这些行并不重要,因为构造函数可能无法按照它在代码中出现的顺序执行。