6

我曾经认为,直观地说,Java 中的构造函数是生成对象的东西,在构造函数返回之前,没有任何东西可以触及该对象。但是,我一次又一次地被证明是错误的:

  1. 未初始化的对象可以通过共享泄露this
  2. 未初始化的对象可能会被从终结器访问它的子类泄漏
  3. 未初始化的对象可以在它们完全构造之前泄漏到另一个线程

所有这些事实都违反了我对构造函数的直觉。

我不再有信心说出构造函数在 Java 中的实际作用,或者它的用途。如果我正在使用所有 final 字段制作一个简单的 DTO ,那么我可以理解构造函数的用途,因为这与 C 中的 struct 完全相同,只是它不能被修改。除此之外,我不知道在 Java 中可以可靠地使用哪些构造函数。它们只是约定/语法糖吗?(即如果只有工厂为您初始化对象,那么您将只有X x = new X(), 然后修改其中的每个字段x以使它们具有非默认值 - 鉴于上述 3 个事实,这几乎等同于 Java 的实际情况)

我可以命名构造函数实际上保证的两个属性:如果我这样做X x = new X(),那么我知道它x是 的实例X但不是 的子类X,并且它的最终字段已完全初始化。你可能想说你知道Xfinished的构造函数并且你有一个有效的对象,但如果你传递X给另一个线程,这是不正确的——另一个线程可能会看到未初始化的版本(即你刚才说的与保证调用工厂)。构造函数实际上保证了哪些其他属性?

4

4 回答 4

6

所有这些事实都违反了我对构造函数的直觉。

他们不应该。构造函数完全按照您的想法执行。

1:未初始化的对象可以通过共享这个来泄露

3:未初始化的对象可以在完全构造之前泄漏到另一个线程

泄漏this、在构造函数中启动线程以及存储新构造的对象(其中多个线程在不同步的情况下访问它)的问题都是围绕非最终(和非易失性)字段的初始化重新排序的问题。但是初始化代码还是由构造函数完成的。构造对象的线程完全看到对象。这是关于当这些更改在其他线程中可见时,语言定义不能保证。

您可能想说您知道 X 的构造函数已完成并且您有一个有效的对象,但如果您将 X 传递给另一个线程,这是不正确的 - 另一个线程可能会看到未初始化的版本(即您刚才所说的没有什么不同而不是打电话给工厂的保证)。

这是对的。同样正确的是,如果您有一个未同步的对象并在一个线程中对其进行变异,则其他线程可能会或可能不会看到该变异。这就是线程编程的本质。即使是构造函数也不能避免需要正确同步对象。

2:未初始化的对象可能被从终结器访问它的子类泄漏

本文档正在讨论终结器以及在对象被垃圾回收后无法正确访问该对象。通过 hack 子类和终结器,您可以生成一个构造不正确的对象,但这样做是一个主要的 hack。对我来说,这并不会以某种方式挑战构造函数的功能。相反,它展示了现代、成熟的 JVM 的复杂性。该文档还展示了如何编写代码来解决这个问题。

Java中的构造函数保证了哪些属性?

根据定义,构造函数:

  1. 为对象分配空间。
  2. 将对象中的所有实例变量设置为其默认值。这包括对象超类中的实例变量。
  3. 为对象分配参数变量。
  4. 处理任何显式或隐式构造函数调用(在构造函数中调用 this() 或 super())。
  5. 初始化类中的变量。
  6. 执行构造函数的其余部分。

就您的 3 个问题而言,#1 和 #3 同样是关于非最终和非易失性字段的初始化何时被构造对象的线程以外的线程看到。不保证这种不同步的可见性。

#2 问题展示了一种机制,如果在执行构造函数时抛出异常,您可以覆盖 finalize 方法以获取和构造不正确的对象。构造函数点 1-5 已经出现。通过 hack,您可以绕过 6 的一部分。我想如果这挑战了构造函数的身份,那是在旁观者的眼中。

于 2013-05-20T15:35:34.113 回答
1

JLS 第 12.5 节

12.5。创建新的类实例

就在对新创建对象的引用作为结果返回之前,使用以下过程处理指示的构造函数以初始化新对象:

将构造函数的参数分配给此构造函数调用的新创建的参数变量。

如果此构造函数以同一类中另一个构造函数的显式构造函数调用(第 8.8.7.1 节)开始(使用 this),则评估参数并使用这五个相同的步骤递归地处理该构造函数调用。如果该构造函数调用突然完成,则此过程出于相同原因而突然完成;否则,继续执行步骤 5。

此构造函数不以显式构造函数调用同一类中的另一个构造函数开始(使用 this)。如果此构造函数用于 Object 以外的类,则此构造函数将以显式或隐式调用超类构造函数开始(使用 super)。使用这五个相同的步骤递归地评估超类构造函数调用的参数和过程。如果该构造函数调用突然完成,则此过程出于相同的原因突然完成。否则,继续执行步骤 4。

Execute the instance initializers and instance variable initializers for this class, assigning the values of instance variable initializers to the corresponding instance variables, in the left-to-right order in which they appear textually in the source code for the class. If execution of any of these initializers results in an exception, then no further initializers are processed and this procedure completes abruptly with that same exception. Otherwise, continue with step 5.

Execute the rest of the body of this constructor. If that execution completes abruptly, then this procedure completes abruptly for the same reason. Otherwise, this procedure completes normally.

**

与 C++ 不同,Java 编程语言在创建新类实例期间没有为方法 >dispatch 指定更改的规则。如果调用了在正在初始化的对象的子类中被覆盖的方法,那么即使在新对象完全初始化之前,也会使用这些覆盖方法。

JLS 16.9开始:

请注意,没有任何规则可以让我们得出结论 V 在实例变量初始化程序之前肯定是未分配的。我们可以非正式地得出结论,在 C 的任何实例变量初始化器之前,V 并不是绝对未赋值的,但没有必要明确说明这样的规则。

发生在 17.4.5 之前

线程 17.5.2

在构造该对象的线程中读取对象的最终字段是按照通常的发生前规则相对于构造函数中该字段的初始化进行排序的。如果读取发生在构造函数中设置字段之后,它会看到最终字段分配的值,否则会看到默认值。

于 2013-05-20T15:40:38.570 回答
0

一个类包含被调用以从类蓝图创建对象的构造函数。

就是 Oracle 关于构造函数的说法。

现在说到你的意思。

直观地说,Java 中的构造函数是生成对象的东西,在构造函数返回之前,没有任何东西可以触及该对象。

所以根据官方文档,你的假设是不正确的。重点是滥用 Java 的规则12行为,除非你有意识地想要泄露你的对象!由于也与 无关Constructor,我将跳过讨论这些要点。

现在,如果我们谈论您的3rd观点,在多线程环境中,没有什么可以保证您的代码的一致性,除非“正确同步块”“原子指令”。由于对象创建既不是指令synchronized也不是atomic指令,因此无法保证一致!没有什么Constructor可以做的。换句话说,它不是Constructor创建对象的责任atomic

现在,回答你的问题,“构造函数实际上保证了哪些其他属性?” 有点容易。Constructors只不过是特殊类型的方法,在对象创建期间从类的蓝图调用。所以它不能保证什么,除非你给它一个机会像任何其他方法一样一致地执行。在持续执行之后,它可以向您保证,您的对象会按照您的需要和指示创建和初始化。

于 2013-05-20T16:55:12.480 回答
-1

构造函数是java仅用于初始化创建的对象的状态..仅此而已。

于 2013-05-20T15:34:51.187 回答