【问题标题】:What properties are guaranteed by constructors in Java?Java中的构造函数保证了哪些属性?
【发布时间】:2013-05-20 15:31:48
【问题描述】:

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

  1. 共享this会泄露未初始化的对象
  2. uninitialized objects can be leaked by a subclass accessing it from the finalizer
  3. uninitialized objects can be leaked to another thread before they're fully constructed

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

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

我可以命名两个实际上由构造函数保证的属性:如果我这样做X x = new X(),那么我知道xX 的一个实例,但不是X 的子类,它的最终字段是完全初始化。您可能想说您知道 X 的构造函数已完成并且您拥有一个有效对象,但如果您将 X 传递给另一个线程 - the other thread may see the uninitialized version(即您刚才所说的与调用工厂的保证)。构造函数实际上还保证了哪些其他属性?

【问题讨论】:

  • @Dog - 你能在java中创建一个使用对象构造函数的工厂吗? (使用 sun.misc.Unsafe 不算在内)。
  • @jtahlborn 这不公平。
  • 您观察到的构造函数可能构造不正确。
  • @jtahlborn 对不起队友,这是灰色区域,你可以避免构造函数。反射和克隆就是几个例子
  • @Raza - 反射并不能避免构造函数。克隆和序列化避免了构造函数,但这并没有使它成为“灰色区域”。您只能克隆和序列化现有实例,因此,您仍然需要一个构造函数(用于现有实例)。

标签: java security inheritance concurrency constructor


【解决方案1】:

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

他们不应该。构造函数完全按照你的想法做。

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

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

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

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

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

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

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

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

根据定义,构造函数:

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

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

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

【讨论】:

  • 什么?这些优化使代码的行为方式符合我的预期,前 2 个项目符号也与此无关,例如数字 2 表示我不能依赖构造函数来执行我在 SecurityManager 下的预期。
  • 多线程程序中存在大量复杂性,使其“表现出 [我们] 期望它不会表现的方式”。指令的重新排序就是其中之一。你可以有x=1;y=2;,另一个线程会看到y分配,但看不到x。我不明白这如何威胁构造函数@Dog 的定义。
  • SecurityManager 示例与并发无关。此外,假设您获得x 的实例,构造函数的完成happens-before 发布x 的引用也是常识,但在Java 中并非如此。问题是我不知道“构造函数的定义”(构造函数的目的和属性)是什么,这就是我问这个问题的原因。
  • 终结器泄漏一个问题,因为如果您使用SecurityManager,那么您不能再仅依赖于完全构造的对象在野外,因为恶意代码可以使用我的问题中第 2 点中描述的技术来获取未初始化的版本。
  • 您是正确的,提到的终结器黑客可能会导致对象初始化不正确,除非您在底部使用它们的私有初始化方法。但对我来说,这不会威胁到我对构造函数的定义。它(再次)展示了现代、成熟的多线程语言的复杂性,但对我来说仅此而已。
【解决方案2】:

来自JLS section 12.5

12.5。创建新的类实例

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

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

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

此构造函数不是以显式构造函数调用同一类中的另一个构造函数开始的(使用 this)。如果 this 构造函数用于 Object 以外的类,然后 this 构造函数将以显式或隐式调用 超类构造函数(使用 super)。评估论据和 使用这些递归处理超类构造函数调用 同样的五个步骤。如果该构造函数调用突然完成, 然后这个过程出于同样的原因突然完成。除此以外, 继续第 4 步。

为这个类执行实例初始化器和实例变量初始化器,分配实例变量的值 对应实例变量的初始化器,在 它们在源代码中以文本形式出现的从左到右的顺序 为班级。如果执行这些初始化程序中的任何一个导致 异常,则不再处理任何初始化程序,这 过程突然完成,但出现同样的异常。除此以外, 继续第 5 步。

执行此构造函数的其余部分。如果该执行突然完成,则此过程突然完成 出于同样的原因。否则,此过程正常完成。

**

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

来自JLS 16.9

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

Happens before 17.4.5

Threading 17.5.2

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

【讨论】:

    【解决方案3】:

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

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

    现在进入你的重点。

    直观地说,Java 中的构造函数是创建对象的东西,在构造函数返回之前,任何东西都不能触及该对象。

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

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

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

    【讨论】:

      【解决方案4】:

      constructors is java只是用来初始化创建的对象的状态..仅此而已。

      【讨论】:

      • @Dog - 工厂仍然使用构造函数。
      • 就像你说的,工厂使用构造函数。构造函数在生活中的唯一目的是初始化对象的状态..仅此而已..
      • 是的,工厂构造对象,并且它们本身也是构造的。甚至静态类也有 static 块,这有点类似于构造函数。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-20
      • 2016-09-08
      • 1970-01-01
      • 2022-12-16
      • 2023-04-02
      • 1970-01-01
      相关资源
      最近更新 更多