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

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

11 ответов

Решение

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

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

@SuppressWarnings("LeakingThisInConstructor")

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

[Примечание 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!,

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

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

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

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

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

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

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

addWindowFocusListener(Singleton.this);

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

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

private Singleton() {
  ...
}

public static Singleton getInstance() {

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

  private class MyListener implements WindowFocusListener {
  ...
  }
}

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

public class Singleton implements WindowFocusListener {

    private Singleton() {
      ...
    }    

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

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

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

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

Ошибка «Утечка этого в конструкторе» — одна из ошибок, которые действительно раздражают, и трудно сказать, действительно ли это проблема. Тестовые случаи будут проходить большую часть времени, и они могут быть даже настроены таким образом, что они проходят всегда, а ошибка возникает только на производстве.

Наличие такого кода со слушателями в пользовательском интерфейсе довольно распространено, и они обычно не дают сбоев из-за единого потока пользовательского интерфейса, который упрощает все, но иногда нет. Если вы создаете объект в потоке пользовательского интерфейса, и этот объект добавляет несколько слушателей в поток пользовательского интерфейса, весьма вероятно, что эти слушатели также вызываются в потоке пользовательского интерфейса, поскольку они реагируют на события пользовательского интерфейса. Если вы уверены в настройке, вы можете просто проигнорировать ошибку.

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

Вот как я бы исправил код:

      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);
    }

}

Предоставьте фабричный метод или создайте отдельный фабричный класс, который может создавать ваши объекты. Эта фабрика отвечает за вызов метода после создания объекта.

Это требует от вас поиска ссылок, в которых используется конструктор, и обновления до нового метода. В качестве промежуточного шага рефакторинга я предлагаю отказаться от конструктора, чтобы вы могли обновить клиентов для использования нового фабричного метода до его изменения, например:

      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);
    }

}

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

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

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

/questions/1232029/kak-podavit-preduprezhdenie-v-fajlah-php-dlya-netbeans/1232044#1232044

Скажем, у вас изначально был такой класс, который использовал себя как 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 на его собственный класс. Конечно, для этого потребуется ссылка на 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 и удаляете ее.

Другие вопросы по тегам