Так что кажется, что это плохая идея передать this
из конструктора в Java.
class Foo {
Foo() {
Never.Do(this);
}
}
Мой простой вопрос: почему?
В Stackoverflow есть некоторые связанные вопросы, но ни один из них не дает исчерпывающего списка проблем, которые могут возникнуть.
Например, в этом вопросе, который просит обходной путь к этой проблеме, один из ответов указывает:
Например, если ваш класс имеет конечное (нестатическое) поле, вы обычно можете зависеть от того, что оно установлено в значение и никогда не меняется.
Когда объект, на который вы смотрите, в настоящее время выполняет свой конструктор, эта гарантия больше не выполняется.
Как это?
Кроме того, я понимаю, что подклассификация является большой проблемой, потому что конструктор суперкласса всегда вызывается перед конструктором подкласса и может вызвать проблемы.
Кроме того, я читал, что могут возникать проблемы Java Memory Model (JMM), такие как различия в видимости по потокам и переупорядочение доступа к памяти. никаких подробностей об этом.
Какие еще могут возникнуть проблемы и вы можете подробнее остановиться на вышеупомянутых проблемах?
В принципе, вы уже перечисляете плохие вещи, которые могут произойти, поэтому вы частично отвечаете на свой собственный вопрос. Я расскажу подробности о том, что вы упомянули:
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
, поскольку они не могут быть переопределены невиновным производным классом.
Заключительные поля в модели памяти 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 в основном позволяет изменять порядок вещей по желанию в этом случае. Вместо того, чтобы помнить подробные сведения о различных конкретных проблемах, которые могут произойти, просто помните о двух концептуальных вещах, которые могут произойти:
this
, который выходит из конструктора, представляет собой частично построенный объект, и обычно вы никогда не хотите иметь частично построенные объекты, потому что их трудно рассуждать (см. Мой первый пример). Особенно, когда наследование вступает в игру, все становится еще сложнее. Просто помните: утечка this
+ inheritance = Вот драконы.this
в конструкторе, поэтому безумные переупорядочения могут привести к очень странным исполнениям, которые почти невозможно отладить. Просто помните: утечка this
+ concurreny = Вот драконы.
this
может потребовать что-то от нее до полной инициализации ...