gpt4 book ai didi

java - 从 Java 构造函数传递 'this' 到底有什么危险?

转载 作者:行者123 更新时间:2023-12-02 04:21:56 25 4
gpt4 key购买 nike

所以似乎通过this是个坏主意来自 Java 中的构造函数。

class Foo {
Foo() {
Never.Do(this);
}
}
我的简单问题是:为什么?
Stackoverflow 上有一些相关的问题,但没有一个给出可能出现的问题的完整列表。
例如, in this question ,这是要求解决此问题的方法, one of the answers状态:

For example if your class has a final (non-static) field, then you can usually depend on it being set to a value and never changing.

When the object you look at is currently executing its constructor, then that guarantee no longer holds true.


那个怎么样?
另外,我知道子类化是一个大问题,因为总是在子类构造函数之前调用父类(super class)构造函数,这可能会导致问题。
此外,我读到了 Java Memory Model (JMM) issues such as differences in visibility across threads and memory access reordering可能会出现,但没有详细说明。
还可能出现哪些问题,您能否详细说明上述问题?

最佳答案

基本上,您已经列举了可能发生的坏事,因此您已经部分回答了您自己的问题。我将提供您提到的内容的详细信息:

分发 this在初始化最终字段之前

For example if your class has a final (non-static) field, then you can usually depend on it being set to a value and never changing.

When the object you look at is currently executing its constructor, then that guarantee no longer holds true.

How is that?



很简单:如果你分发 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
}
}

很简单吧?类(class) Y看到一个 X带有未初始化的 final field 。这是一个很大的禁忌!

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

}

所以如你所见,有一个类 Xfoo方法。 X泄露给 A在其构造函数和 A 中电话 foo .对于 X类,这很好用。但是对于 Y类,一个 NullPointerException被抛出。原因是 Y覆盖 foo并在其中使用其字段之一 ( stream )。自 streamA 时尚未初始化电话 foo ,您收到异常。

这个例子展示了泄漏这个的下一个问题:即使你的基类在泄漏时可能工作得很好 this ,从你的基类继承的类(它可能不是你写的,而是其他不知道泄露 this 的人)可能会炸毁一切。

泄漏 this给自己

本节不完全讲自己的那种问题,而是要记住的一点:即使调用自己的一个方法也可以认为是泄漏的 this ,因为它带来了与泄漏到另一个类的引用类似的问题。例如,考虑前面带有不同 X 的示例。构造函数:
    X(){
// A.doFoo();
foo(); // ouch, don't do this!
}

现在,我们不泄漏 thisA但我们通过拨打 foo 将其泄露给自己.同样,同样的坏事发生了:A 类 Y覆盖 foo()并使用它自己的领域之一会造成严重破坏。

现在考虑我们的第一个例子 final field 。同样,通过某种方法泄露给自己可能允许找到 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 .

如果你真的需要在构造函数中调用你自己的方法,那么要么只使用 finalstatic方法,因为这些方法不能被无辜的派生类覆盖。

并发

Java 内存模型中的最终字段具有一个很好的特性:它们可以在不加锁的情况下并发读取。 JVM 必须确保即使是并发解锁访问也总是会看到一个完全初始化的 final field 。例如,这可以通过在分配 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 .所以在单线程环境中,一切都很好。但是现在进入并发:我们创建了一个静态 Y (它接收受污染的 X 实例)并在第二个线程中访问它。现在,断言可能再次失败,因为现在编译器或 CPU 乱序执行 允许重新订购 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.
}

现在,我们可以观察到错误的行为:考虑线程 1 执行直到 4. 的所有步骤。然后被中断(在设置 final 字段之前!)。现在,线程 2 执行所有代码,因此它的 assert(Y.y.x == 5);失败。

还可能出现哪些问题

基本上,你提到的和我上面解释的三个问题是最糟糕的。当然,这些问题可能发生在许多不同的方面,因此可以构建数千个示例。只要您的程序是单线程的,那么早点发布可能没问题(但无论如何都不要这样做!)。一旦并发发挥作用,永远不要这样做,你会得到奇怪的行为,因为在这种情况下,JVM 基本上被允许根据需要重新排序。不要记住可能发生的各种具体问题的血腥细节,只需记住可能发生的两个概念性事情:
  • 构造函数通常首先彻底构造一个对象,然后将其交给调用者。 A this逃避构造函数的那个​​代表一个部分构造的对象,通常你永远不想拥有部分构造的对象,因为它们很难推理(见我的第一个例子)。尤其是当继承发挥作用时,事情变得更加复杂。只需记住:泄漏 this + 继承 = 这里是龙。
  • 一旦你分发 this 内存模型放弃大部分保证在构造函数中,如此疯狂的重新排序可能会产生几乎不可能调试的非常奇怪的执行。只需记住:泄漏 this + concurreny = 这里是龙。
  • 关于java - 从 Java 构造函数传递 'this' 到底有什么危险?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25281301/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com