Утечка этого в предупреждении конструктора

Я бы хотел избежать (большинства) предупреждений 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).

Ответ 1

Поскольку вы обязательно поместите свой instances.add(this) в конец конструктора, вы должны ИМХО быть в безопасности, чтобы сообщить компилятору просто подавить предупреждение (*), Предупреждение, по своей природе, не обязательно означает, что там что-то не так, просто требуется ваше внимание.

Если вы знаете, что делаете, вы можете использовать аннотацию @SuppressWarnings. Как упоминает Террел в своих комментариях, следующая аннотация делает это как NetBeans 6.9.1:

@SuppressWarnings("LeakingThisInConstructor")

(*) Обновление: Как отмечали Истар и Сергей, есть случаи, когда код конструктора "утечка" может выглядеть совершенно безопасно (как в вашем вопросе), и все же это не так. Есть ли больше читателей, которые могут это одобрить? Я рассматриваю возможность удаления этого ответа по указанным причинам.

Ответ 2

[Примечание chiccodoro: объяснение, почему/при утечке this может вызвать проблемы, даже если в конструкторе помещается последний тег:]

Конечная полевая семантика отличается от "нормальной" семантики поля. Пример,

Мы играем в сетевую игру. Позволяет сделать объект игры, получая данные из сети, и объект 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 к игре, которая работает поток.

Финал довольно страшный:

... компиляторы имеют большую свободу в переносе чтений окончательных полей через барьеры синхронизации...

Это в значительной степени поражает всю правильную синхронизацию. Но, к счастью,

В потоке, который может видеть только ссылку на объект после, что этот объект был полностью, инициализирован, будет гарантированно видеть правильно инициализированные значения для этого объекта final,

В этом примере конструктор записывает ссылку на объекты в список. (И, таким образом, он еще не был полностью инициализирован, так как конструктор не закончил.) После записи конструктор все еще не выполнен. Он просто должен вернуться от конструктора, но предположим, что этого еще нет. Теперь исполнитель мог выполнять свою работу и передавать события всем слушателям, включая еще не инициализированный объект игрока! Конечное поле игрока (имя) не может быть записано и приведет к печати null sees event!.

Ответ 3

Лучшие варианты, которые у вас есть:

  • Извлеките часть WindowFocusListener в другой класс (также может быть внутренним или анонимным). Лучшее решение, таким образом, каждый класс имеет определенную цель.
  • Игнорировать предупреждение.

Использование singleton в качестве обходного пути для негерметичного конструктора неэффективно.

Ответ 4

Это хороший пример того, где будет полезен Factory, который создал экземпляры вашего класса. Если Factory отвечал за создание экземпляров вашего класса, то у вас было бы централизованное место, где вызывается конструктор, и было бы тривиально добавить в ваш код необходимый метод init().

Что касается вашего немедленного решения, я бы предположил, что вы перемещаете любые вызовы, которые просачивают this в последнюю строку вашего конструктора, а затем подавляют их аннотацией, как только вы "доказали", что это безопасно..

В IntelliJ IDEA вы можете подавить это предупреждение следующим комментарием прямо над строкой:
//noinspection ThisEscapedInObjectConstruction

Ответ 5

Можно написать:

addWindowFocusListener(Singleton.this);

Это предотвратит показ предупреждения NB.

Ответ 6

Использование вложенного класса (как было предложено Колином), вероятно, является вашим лучшим вариантом. Здесь псевдокод:

private Singleton() {
  ...
}

public static Singleton getInstance() {

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

  private class MyListener implements WindowFocusListener {
  ...
  }
}

Ответ 7

Нет необходимости в отдельном классе слушателя.

public class Singleton implements WindowFocusListener {

    private Singleton() {
      ...
    }    

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

    public static Singleton getInstance() {    
      ...
      if(instance != null) {
        instance = new Singleton();
        instance.init();
      }
      ...
    }
}

Ответ 8

Аннотации @SuppressWarnings ( "LeakingThisInConstructor" ), применимые только к классу, а не к самому конструктору.

Сосуждение Я бы предложил: создать частный метод init() {/* использовать это здесь */} и вызвать его из конструктора. NetBeans не предупредит вас.

Ответ 9

Предположим, что у вас был такой класс, который использовал себя как ActionListener, и поэтому вы вызываете addActionListener (это), который генерирует предупреждение.

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 на свой класс. Конечно, тогда для этого потребуется ссылка на JFrame, которую вы хотите вызвать .dispose(). Если вы не хотите заполнять пространство имен переменных и хотите использовать ActionListener для нескольких JFrames, вы можете сделать это с помощью 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 и распоряжаетесь им.

Ответ 10

Оберните this в двойные скобки. Netbeans игнорирует некоторые ошибки по умолчанию, если они находятся в подзапросах.

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

fooobar.com/info/64143/...