В чем конкретно заключается опасность передачи 'this' из конструктора Java?

7

Так что кажется, что это плохая идея передать this из конструктора в Java.

class Foo {
    Foo() {
        Never.Do(this);
    }
}

Мой простой вопрос: почему?

В Stackoverflow есть некоторые связанные вопросы, но ни один из них не дает исчерпывающего списка проблем, которые могут возникнуть.

Например, в этом вопросе, который просит обходной путь к этой проблеме, один из ответов указывает:

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

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

Как это?

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

Кроме того, я читал, что могут возникать проблемы Java Memory Model (JMM), такие как различия в видимости по потокам и переупорядочение доступа к памяти. никаких подробностей об этом.

Какие еще могут возникнуть проблемы и вы можете подробнее остановиться на вышеупомянутых проблемах?

  • 0
    Как всегда, немного кода стоит 1024 слова ...
  • 3
    Вызывающая сторона, использующая ссылку на this может потребовать что-то от нее до полной инициализации ...
Показать ещё 4 комментария
Теги:

1 ответ

17
Лучший ответ

В принципе, вы уже перечисляете плохие вещи, которые могут произойти, поэтому вы частично отвечаете на свой собственный вопрос. Я расскажу подробности о том, что вы упомянули:

Выдача this перед инициализацией окончательных полей

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

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

Как это?

Довольно просто: если вы передадите this перед установкой поля final, то он еще не будет установлен:

class X{
    final int i;
    X(){
        new Y(this); // ouch, don't do this!
        i = 5;
    }
}

class Y{
    Y(X x){
        assert(x.i == 5);//This assert should be true, since i is final field, but it fails here
    }
}

Довольно просто, не так ли? Класс Y видит a X с неинициализированным полем final. Это большой нет-нет!

Java обычно гарантирует, что поле final инициализируется ровно один раз и не считывается до его инициализации. Эта гарантия исчезает после утечки this.

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

подклассов

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

class A{
    static void doFoo(X x){
        x.foo();
    }
}

class X{
    X(){
        A.doFoo(this); // ouch, don't do this!
    }

    void foo(){
        System.out.println("Leaking this seems to work!");
    }

}

class Y extends X {
     PrintStream stream;

     Y(){
         this.stream = System.out;
     }

     @Overload // Polymorphism ruins everything!
     void foo(){
         // NullPointerException; stream not yet initialized 
         stream.println("Leaking + Polymorphism == NPE");      
     }

}

Итак, как вы видите, существует класс X с методом foo. X просочится в A в свой конструктор, а A вызывает foo. Для классов X это работает отлично. Но для классов Y выбрано a NullPointerException. Причина в том, что Y переопределяет foo и использует одно из своих полей (stream) в нем. Поскольку stream еще не инициализируется, когда A вызывает foo, вы получаете исключение.

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

Утечка this себе

В этом разделе точно не говорится о проблеме своего типа, но нужно иметь в виду: даже вызов одного из ваших собственных методов можно рассматривать как утечку this, поскольку он приносит схожие проблемы, как ссылка утечка в другой класс. Например, рассмотрим предыдущий пример с другим конструктором X:

    X(){
        // A.doFoo();
        foo(); // ouch, don't do this!
    }

Теперь мы не просачиваем this в A, но мы просачиваем его себе, вызывая foo. Опять же, происходят те же самые плохие вещи: класс Y, который переопределяет foo() и использует одно из своих полей, приведет к хаосу.

Теперь рассмотрим наш первый пример с полем final. Опять же, утечка себя с помощью метода может позволить найти поле final неинициализированным:

class X{
   final int i;
   X(){ 
      foo();
      i = 5;
   }

   void foo(){
      assert(i == 5); // Fails, of course
   }
}

Конечно, этот пример вполне построен. Каждый программист заметит, что первый вызов foo, а затем установка i неверна. Но теперь рассмотрим наследование снова: ваш метод X.foo() может даже не использовать i, поэтому он должен быть вызван до инициализации i. Однако подкласс может переопределить foo() и использовать в нем i, снова разбивая все.

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

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

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

Concurrency

Заключительные поля в модели памяти Java обладают хорошим свойством: их можно читать одновременно без блокировки. JVM должен гарантировать, что даже одновременный разблокированный доступ всегда будет видеть полностью инициализированное поле final. Это можно сделать, например, путем добавления барьера памяти к концу конструктора, который назначает конечные поля. Однако эта гарантия исчезает, как только вы раздаете this слишком рано. Снова пример:

class X{
    final int i;
    X(Y y){
        i = 5;
        y.x = this; // ouch, don't do this!
    }
}

class Y{
    public static Y y;
    public X x;
    Y(){
        new X(this);
    }
}

//... Some code in one thread
{
    Y.y = new Y();
}

//... Some code in another thread
{
    assert(Y.y.x.i == 5); // May fail!
}

Как вы видите, мы снова раздаем this слишком рано, но только послеинициализация i. Таким образом, в одном потоковом окружении все в порядке. Но теперь введите concurrency: мы создаем статический Y (который получает испорченный экземпляр X) в одном потоке и обращаются к нему во втором потоке. Теперь утверждение может снова потерпеть неудачу, так как компилятор или процессор, не выполняющий выполнение заказа, теперь разрешено переупорядочить назначение i = 5 и назначение Y.y = new Y().

Чтобы сделать все более понятным, предположим, что JVM выполнит все вызовы, таким образом, код

{
        Y.y = new Y();
}

будет сначала вложен в (rX - локальные регистры):

{
        r1 = 'allocate memory for Y' // Constructor of Y
        r1.x = new X(r1);            // Constructor of Y
        Y.y = r1;
}

теперь мы также включили бы вызов new X():

{
        r1 = 'allocate memory for Y' // constructor of Y
        r2 = 'allocate memory for X' // constructor of X
        r2.i = 5;                    // constructor of X
        r1.x = r2;                   // constructor of X
        Y.y = r1;
}

До сих пор все в порядке. Но теперь переупорядочение разрешено. Мы (т.е. JVM или CPU) переупорядочиваем r2.i = 5 до конца:

{
        r1 = 'allocate memory for Y' // 1.
        r2 = 'allocate memory for X' // 2.
        r1.x = r2;                   // 3.
        Y.y = r1;                    // 4.
        r2.i = 5;                    // 5.
}

Теперь мы можем наблюдать неправильное поведение: рассмотрим, что thread 1 выполняет все шаги до 4., а затем прерван (перед установкой поля final!). Теперь поток 2 выполняет весь код и, следовательно, его assert(Y.y.x == 5); терпит неудачу.

Другие проблемы могут возникать

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

  • Конструктор обычно сначала тщательно конструирует объект, а затем передает его вызывающему. A this, который выходит из конструктора, представляет собой частично построенный объект, и обычно вы никогда не хотите иметь частично построенные объекты, потому что их трудно рассуждать (см. Мой первый пример). Особенно, когда наследование вступает в игру, все становится еще сложнее. Просто помните: утечка this + inheritance = Вот драконы.
  • Модель памяти отказывается от большинства гарантий после того, как вы передадите this в конструкторе, поэтому безумные переупорядочения могут привести к очень странным исполнениям, которые почти невозможно отладить. Просто помните: утечка this + concurreny = Вот драконы.
  • 0
    Хорошо, почему-то я думал, что конечные поля могут быть инициализированы даже после создания. Излишне говорить, что я новичок в Java;)
  • 0
    Это отличный ответ, но не могли бы вы добавить пример проблемы с подклассами?
Показать ещё 3 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню