Подтвердить что ты не робот

Как эта `this` ссылка на внешний класс исчезает из-за публикации экземпляра внутреннего класса?

Это было задано несколько иначе раньше но просил дать ответ "да/нет" , но я ищу объяснения, которые отсутствуют в книге (Java Concurrency в Практика), как эта очевидная большая ошибка будет использована злонамеренно или случайно.

Последний механизм, с помощью которого может быть объект или его внутреннее состояние опубликовано, чтобы опубликовать экземпляр внутреннего класса, как показано в ThisEscape в листинге 3.7. Когда ThisEscape публикует EventListener, он неявно публикует прилагаемый ThisEscape экземпляр, так как внутренние экземпляры классов содержат скрытые ссылка на прилагаемый экземпляр.

Листинг 3.7. Неявно разрешить эту ссылку для Escape. не сделайте это.

public class This Escape {
    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e);
                }
            });
    }
}

3.2.1. Безопасная строительная практика

ThisEscape иллюстрирует важный частный случай выхода - когда эта ссылка ускользает во время строительства. Когда внутренний Экземпляр EventListener публикуется, поэтому прилагается ThisEscape пример. Но объект находится в предсказуемом, согласованном состоянии после того, как его конструктор вернется, поэтому публикация объекта изнутри конструктор может публиковать неполностью сконструированный объект. Это true, даже если публикация является последним утверждением в конструкторе. Если эта ссылка исчезает во время построения, объект считается неправильно построенным. [8]

[8] Более конкретно, эта ссылка не должна выходить из до тех пор, пока конструктор не вернется. Эта ссылка может быть хранится где-нибудь конструктором, если он не используется другой нить до после строительства. SafeListener в листинге 3.8 использует эту технику.

Не позволяйте этой ссылке выходить во время построения.

Как кто-то прокомментировал бы это, чтобы добраться до OuterClass до его завершения? Что обозначается hidden inner class reference курсивом в первом абзаце?

4b9b3361

Ответ 1

См. эту статью. Там четко объяснено, что может произойти, если вы отпустите this.

И вот последующие действия с дальнейшими пояснениями.

Это потрясающий информационный бюллетень Хайнца Кабуца, где обсуждаются эта и другие очень интересные темы. Я очень рекомендую.

Вот пример, взятый из ссылок, которые показывают, как справляется ссылка this:

public class ThisEscape {
  private final int num;

  public ThisEscape(EventSource source) {
    source.registerListener(
        new EventListener() {
          public void onEvent(Event e) {
            doSomething(e);
          }
        });
    num = 42;
  }

  private void doSomething(Event e) {
    if (num != 42) {
      System.out.println("Race condition detected at " +
          new Date());
    }
  }
}

Когда он компилируется, javac генерирует два класса. Внешний класс выглядит следующим образом:

public class ThisEscape {
  private final int num;

  public ThisEscape(EventSource source) {
    source.registerListener(new ThisEscape$1(this));
    num = 42;
  }

  private void doSomething(Event e) {
    if (num != 42)
      System.out.println(
          "Race condition detected at " + new Date());
  }

  static void access$000(ThisEscape _this, Event event) {
    _this.doSomething(event);
  }
}

Далее у нас есть анонимный внутренний класс:

class ThisEscape$1 implements EventListener {
  final ThisEscape this$0;

  ThisEscape$1(ThisEscape thisescape) {
    this$0 = thisescape;
    super();
  }

  public void onEvent(Event e) {
    ThisEscape.access$000(this$0, e);
  }
}

Здесь анонимный внутренний класс, созданный в конструкторе внешнего класса, преобразуется в класс доступа к пакету, который получает ссылку на внешний класс (тот, который позволяет this сбежать). Чтобы внутренний класс имел доступ к атрибутам и методам внешнего класса, во внешнем классе создается статический метод доступа к пакетам. Это access$000.

Эти две статьи показывают, как происходит фактическое экранирование и что может произойти.

"Что" в основном является условием гонки, которое может привести к NullPointerException или любому другому исключению при попытке использовать объект еще не полностью инициализированным. В примере, если поток достаточно быстр, может случиться, что он запускает метод doSomething(), а num еще не был правильно инициализирован до 42. В первой ссылке есть тест, который показывает именно это.

EDIT: Несколько строк о том, как закодировать эту проблему/функцию, отсутствовали. Я могу думать только о том, чтобы придерживаться (может быть, неполного) набора правил/принципов, чтобы избежать этой проблемы и других:

  • Вызывать только методы private из конструктора
  • Если вам нравится адреналин и вы хотите вызвать методы protected из конструктора, сделайте это, но объявите эти методы как final, чтобы они не могли быть переопределены подклассами
  • Никогда создавать внутренние классы в конструкторе, анонимные, локальные, статические или нестатические
  • В конструкторе не передавайте this непосредственно в качестве аргумента для чего-либо
  • Избегайте любых переходных комбинаций правил выше, т.е. не создавайте анонимный внутренний класс в методе private или protected final, который вызывается из конструктора
  • Используйте конструктор, чтобы просто построить экземпляр класса, и пусть он только инициализирует атрибуты класса со значениями по умолчанию или с предоставленными аргументами

Если вам нужно делать другие вещи, используйте либо конструктор, либо шаблон factory.

Ответ 2

Я немного изменю пример, чтобы сделать его более понятным. Рассмотрим этот класс:

public class ThisEscape {

    Object someThing;

    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e, someThing);
                }
            });
        someThing = initTheThing();
    }
}

За кулисами анонимный внутренний класс имеет доступ к внешнему экземпляру. Вы можете это сказать, потому что вы можете получить доступ к переменной экземпляра someThing, и, как упоминалось в Shashank, вы можете получить доступ к внешнему экземпляру через ThisEscape.this.

Проблема заключается в том, что, предоставляя анонимный внутренний экземпляр класса внешнему (в данном случае объекту EventSource), он также будет иметь экземпляр ThisEscape с ним.

Что может случиться с этим? Рассмотрим эту реализацию EventSource ниже:

public SomeEventSource implements EventSource {

    EventListener listener;

    public void registerListener(EventListener listener) {
        this.listener = listener;
    }

    public void processEvent(Event e) {
        listener.onEvent(e);
    }

}

В конструкторе ThisEscape мы зарегистрируем EventListener, который будет храниться в переменной экземпляра listener.

Теперь рассмотрим два потока. Один вызывает конструктор ThisEscape, а другой вызывает processEvent с некоторым событием. Также предположим, что JVM решает переключиться с первого потока на второй, сразу после строки source.registerListener и прямо перед someThing = initTheThing(). Второй поток теперь запускается, и он будет вызывать метод onEvent, который, как вы можете видеть, выполняет что-то с someThing. Но что такое someThing? Он null, потому что другой поток не завершил инициализацию объекта, поэтому это (вероятно) вызовет исключение NullPointerException, которое на самом деле не является тем, что вы хотите.

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

Ответ 3

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

Представьте, что EventSource.registerListener сразу вызывает EventLister.doSomething()! Этот doSomething будет вызываться для объекта, родительский this является неполным.

public class ThisEscape {

    public ThisEscape(EventSource source) {
        // Calling a method
        source.registerListener(
                // With a new object
                new EventListener() {
                    // That even does something
                    public void onEvent(Event e) {
                        doSomething(e);
                    }
                });
        // While construction is still in progress.
    }
}

Выполнение этого способа будет подключать отверстие.

public class TheresNoEscape {

    public TheresNoEscape(EventSource source) {
        // Calling a method
        source.registerListener(
                // With a new object - that is static there is no escape.
                new MyEventListener());
    }

    private static class MyEventListener {

        // That even does something
        public void onEvent(Event e) {
            doSomething(e);
        }
    }
}