87

我想避免 Netbeans 6.9.1 的(大部分)警告,但我对警告有疑问'Leaking this in constructor'

我理解这个问题,在构造函数中调用方法并传递“ this”是危险的,因为“ this”可能尚未完全初始化。

在我的单例类中修复警告很容易,因为构造函数是私有的并且只能从同一个类中调用。

旧代码(简化):

private Singleton() {
  ...
  addWindowFocusListener(this);
}

public static Singleton getInstance() {

  ...
  instance = new Singleton();
  ...
}

新代码(简化):

private Singleton() {
  ...
}

public static Singleton getInstance() {

  ...
  instance = new Singleton();
  addWindowFocusListener( instance );
  ...
}

如果构造函数是公共的并且可以从其他类调用,则此修复不起作用。如何修复以下代码:

public class MyClass {

  ...
  List<MyClass> instances = new ArrayList<MyClass>();
  ...

  public MyClass() {
    ...
    instances.add(this);
  }

}

当然,我想要一个不需要使用此类修改所有代码的修复程序(例如通过调用 init 方法)。

4

11 回答 11

47

由于您确保将您instances.add(this)放在构造函数的末尾,因此恕我直言,您应该可以安全地告诉编译器简单地抑制警告 (*)。就其性质而言,警告并不一定意味着有问题,它只是需要您注意。

如果您知道自己在做什么,则可以使用@SuppressWarnings注释。就像 Terrel 在他的评论中提到的那样,从 NetBeans 6.9.1 开始,下面的注释就做到了:

@SuppressWarnings("LeakingThisInConstructor")

(*)更新:正如 Isthar 和 Sergey 指出的那样,在某些情况下“泄漏”的构造函数代码看起来非常安全(如您的问题所示),但事实并非如此。是否有更多的读者可以批准这一点?出于上述原因,我正在考虑删除此答案。

于 2010-10-13T07:40:57.887 回答
38

[chiccodoro 的评论:解释为什么/何时泄漏this会导致问题,即使泄漏语句放在构造函数的最后:]

最终字段语义不同于“正常”字段语义。一个例子,

我们玩网络游戏。让我们创建一个从网络检索数据的 Game 对象和一个侦听来自游戏的事件以采取相应行动的 Player 对象。游戏对象隐藏了所有网络细节,玩家只对事件感兴趣:

import java.util.*;
import java.util.concurrent.Executors;

public class FinalSemantics {

    public interface Listener {
        public void someEvent();
    }

    public static class Player implements Listener {
        final String name;

        public Player(Game game) {
            name = "Player "+System.currentTimeMillis();
            game.addListener(this);//Warning leaking 'this'!
        }

        @Override
        public void someEvent() {
            System.out.println(name+" sees event!");
        }
    }

    public static class Game {
        private List<Listener> listeners;

        public Game() {
            listeners = new ArrayList<Listener>();
        }

        public void start() {
            Executors.newFixedThreadPool(1).execute(new Runnable(){

                @Override
                public void run() {
                    for(;;) {
                        try {
                            //Listen to game server over network
                            Thread.sleep(1000); //<- think blocking read

                            synchronized (Game.this) {
                                for (Listener l : listeners) {
                                    l.someEvent();
                                }
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }            
            });
        }

        public synchronized void addListener(Listener l) {
            listeners.add(l);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Game game = new Game();
        game.start();
        Thread.sleep(1000);
        //Someone joins the game
        new Player(game);
    }
}
//Code runs, won't terminate and will probably never show the flaw.

似乎一切都很好:对列表的访问已正确同步。缺陷在于此示例将 Player.this 泄漏给运行线程的 Game。

决赛相当可怕

...编译器有很大的自由来跨同步障碍移动对最终字段的读取...

这几乎破坏了所有正确的同步。但幸运的是

只有在对象完全初始化才能看到对该对象的引用的线程可以保证看到该对象字段的正确初始化值。final

在示例中,构造函数将对象引用写入列表。(因此还没有完全初始化,因为构造函数没有完成。)写入之后,构造函数仍然没有完成。它只需要从构造函数返回,但我们假设它还没有。现在执行器可以完成它的工作并向所有监听器广播事件,包括尚未初始化的播放器对象!玩家(姓名)的最后一个字段可能不写,将导致打印null sees event!

于 2014-04-14T19:54:29.010 回答
13

您拥有的最佳选择:

  • 在另一个类中提取你的WindowFocusListener部分(也可以是内部的或匿名的)。最好的解决方案,这样每个类都有一个特定的目的。
  • 忽略警告信息。

使用单例作为泄漏构造函数的解决方法并不是很有效。

于 2010-10-13T07:42:52.827 回答
12

这是一个很好的例子,说明创建类实例的工厂会有所帮助。如果一个工厂负责创建您的类的实例,那么您将有一个调用构造函数的集中位置,并且init()向您的代码添加所需的方法将是微不足道的。

关于您的直接解决方案,我建议您将任何泄漏this到构造函数的最后一行的调用移动,然后在您“证明”这样做是安全的之后用注释抑制它们。

在 IntelliJ IDEA 中,您可以在行上方使用以下注释来抑制此警告:
//noinspection ThisEscapedInObjectConstruction

于 2010-10-13T07:46:03.077 回答
4

可以写:

addWindowFocusListener(Singleton.this);

这将阻止 NB 显示警告。

于 2013-01-09T14:47:14.670 回答
2

注释@SuppressWarnings("LeakingThisInConstructor")仅适用于类而不适用于构造函数本身。

解决方案我建议:创建私有方法init(){/* use this here*/}并从构造函数中调用它。NetBeans 不会警告您。

于 2011-02-07T21:57:18.123 回答
2

使用嵌套类(如 Colin 建议的那样)可能是您最好的选择。这是伪代码:

private Singleton() {
  ...
}

public static Singleton getInstance() {

  ...
  instance = new Singleton();
  addWindowFocusListener( new MyListener() );
  ...

  private class MyListener implements WindowFocusListener {
  ...
  }
}
于 2011-05-19T19:04:46.120 回答
2

不需要单独的侦听器类。

public class Singleton implements WindowFocusListener {

    private Singleton() {
      ...
    }    

    private void init() {
      addWindowFocusListener(this);
    }

    public static Singleton getInstance() {    
      ...
      if(instance != null) {
        instance = new Singleton();
        instance.init();
      }
      ...
    }
}
于 2012-01-10T09:41:55.247 回答
1

this双括号括起来。如果某些错误位于子语句中,Netbeans 默认会忽略它们。

  public MyClass() {
     ...
     instances.add((this));
  }

https://stackoverflow.com/a/8357990

于 2015-05-04T02:00:31.407 回答
0

假设您最初有一个像这样的类,它使用自己作为 ActionListener,因此您最终调用 addActionListener(this) 来生成警告。

private class CloseWindow extends JFrame implements ActionListener {
    public CloseWindow(String e) {
        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        setLayout(new BorderLayout());

        JButton exitButton = new JButton("Close");
        exitButton.addActionListener(this);
        add(exitButton, BorderLayout.SOUTH);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        String actionCommand = e.getActionCommand();

        if(actionCommand.equals("Close")) {
            dispose();
        }
    }
}

正如@Colin Hebert 提到的,您可以将 ActionListener 分离到它自己的类中。当然,这将需要对您要调用 .dispose() 的 JFrame 的引用。如果您不想填充变量名称空间,并且希望能够将 ActionListener 用于多个 JFrame,则可以使用 getSource() 来检索按钮,然后调用 getParent() 链检索扩展 JFrame 的类,然后调用 getSuperclass 以确保它是 JFrame。

private class CloseWindow extends JFrame {
    public CloseWindow(String e) {
        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        setLayout(new BorderLayout());

        JButton exitButton = new JButton("Close");
        exitButton.addActionListener(new ExitListener());
        add(exitButton, BorderLayout.SOUTH);
    }
}

private class ExitListener implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        String actionCommand = e.getActionCommand();
        JButton sourceButton = (JButton)e.getSource();
        Component frameCheck = sourceButton;
        int i = 0;            
        String frameTest = "null";
        Class<?> c;
        while(!frameTest.equals("javax.swing.JFrame")) {
            frameCheck = frameCheck.getParent();
            c = frameCheck.getClass();
            frameTest = c.getSuperclass().getName().toString();
        }
        JFrame frame = (JFrame)frameCheck;

        if(actionCommand.equals("Close")) {
            frame.dispose();
        }
    }
}

上面的代码适用于扩展 JFrame 的类的任何级别的任何子按钮。显然,如果您的对象只是一个 JFrame,则只需直接检查该类而不是检查超类。

最终使用这种方法,您将获得对以下内容的引用: MainClass$CloseWindow 它具有超类 JFrame,然后您将该引用转换为 JFrame 并处理它。

于 2013-09-23T17:18:28.937 回答
0

错误“在构造函数中泄漏”是真正令人讨厌且很难判断它是否真的是一个问题的错误之一。测试用例大部分时间都会通过,甚至可能以始终通过的方式设置它们,并且错误仅发生在生产中。

在 UI 中使用带有侦听器的此类代码很常见,它们通常不会失败,因为单个 UI 线程使一切变得更容易,但有时不会。如果您在 UI 线程中创建了一个对象,并且该对象在 UI 线程中添加了一些侦听器,那么这些侦听器很可能也在 UI 线程中被调用,因为它们对 UI 事件作出反应。如果您确定那里的设置,则可以忽略该错误。

另一方面,在处理多线程应用程序和类(例如处理一些 IO 操作和后台工作)时,此问题可能会导致难以检测的错误,通常发生在周末管理员放假和演示时第二天包括公司最大的客户。

以下是我将如何修复代码:

public class MyClass {

    private final List<MyClass> instances = new ArrayList<MyClass>();

    public static MyClass create() {
        MyClass mc = new MyClass();
        mc.init();
        return mc;
    }

    /** private constructor. */
    private MyClass() {}

    private void init() {
        instances.add(this);
    }

}

提供工厂方法或创建能够创建对象的单独工厂类。该工厂负责init()在对象创建后调用该方法。

这要求您搜索使用构造函数的引用并更新到新方法。作为中间的重构步骤,我建议弃用构造函数,以便您可以更新客户端以在更改之前使用新的工厂方法,例如:

public class MyClass {

    private final List<MyClass> instances = new ArrayList<MyClass>();

    public static MyClass create() {
        MyClass mc = new MyClass();
        // mc.init(); // will be called when the constructor call will be removed
        return mc;
    }

    /** @deprecated use {@link #create()} instead. */
    @Deprecated
    public MyClass() {
        init();
    }

    private void init() {
        instances.add(this);
    }

}

另一种选择是简单地init()公开并强迫您的客户调用它。这使客户可以控制创建对象的生命周期,我什至更愿意在init()必须做一些实际工作的情况下这样做,比如打开或使用连接。

于 2022-01-20T18:01:15.887 回答