【问题标题】:What's the point of nonfinal singleton objects in scala?scala中非最终单例对象的意义何在?
【发布时间】:2015-05-15 17:06:33
【问题描述】:

我一直认为 Scala 中的 object 声明会被编译成 final 类,因为它们是由有效的匿名类实现的。由于final 类比非final 类更容易被JVM 优化,我认为finality 有好处且没有成本,所以所有object 实现都是final。

但我一定错过了什么。默认情况下,object 实现类是非最终的。必须明确声明final object 才能获得final 实现类:

Welcome to Scala version 2.11.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_31).
Type in expressions to have them evaluated.
Type :help for more information.

scala> object Nonfinal;
defined object Nonfinal

scala> final object Final;
defined object Final

scala> :javap Nonfinal
  Size 518 bytes
  MD5 checksum f27390a538ccc6e45764beaeb888478c
  Compiled from "<console>"
public class Nonfinal$
  minor version: 0
  major version: 50
  flags: ACC_PUBLIC, ACC_SUPER
// snip!

scala> :javap Final
  Size 509 bytes
  MD5 checksum 2db90a8bab027857524debbe5cf8ef29
  Compiled from "<console>"
public final class Final$
  minor version: 0
  major version: 50
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER

非最终的object 的目的是什么?为什么有人不想要标记object 决赛?

更新请注意,similar question 已被询问和回答(感谢 Travis Brown!)一个答案是权威的,但必须是过时的,SLS version 2.9 声称“final 对于对象来说是多余的定义”,但显然这不是真的,从 Scala 2.11 final 开始,显然会影响为对象生成的字节码。

另一个(已接受!)答案指出存在一个很少使用的功能,用户可以通过该功能覆盖子类或特征中的对象。我认为这是正确的,所以我的问题确实是重复的,但我花了几分钟才说服自己。乍一看,重写一个对象就像重写一个 val,在这种情况下,没有理由不能在字节码中将每个实现标记为 final。但是经过更多的思考和一些搜索,如果我有的话一定是真的

trait Base { 
  object Poop { 
    def stink = "Yuk!"
  }

  def poopSmell = this.Poop.stink
}

trait Derived extends Base {
  override object Poop {
    def stink = "Roses"
  }
}

那么 Derived 的 Poop 必须继承 Base 的 Poop 的类型,所以 Derived.Poop 的实现必须是 Base 的 Poop 实现的子类,所以 Base 的 poop 不能在字节码中标记为 final。

所以我认为这是重复的,尽管我花了几分钟才解决问题。

但请注意,此功能实际上似乎并不能很好地工作。要编译上述代码,必须运行 scala -Yoverride-objects(或者可能是在非 REPL 上下文中运行 scalac -Yoverride-objects)。然后上面的代码确实在 REPL 中编译,但尝试实例化 Derived 的细化或具体扩展失败。

scala> :paste
// Entering paste mode (ctrl-D to finish)

trait Base { 
  object Poop { 
    def stink = "Yuk!"
  }

  def poopSmell = this.Poop.stink
}

trait Derived extends Base {
  override object Poop {
    def stink = "Roses"
  }
}

// Exiting paste mode, now interpreting.

defined trait Base
defined trait Derived

scala> (new Derived{}).poopSmell
java.lang.ClassFormatError: Duplicate method name&signature in class file $anon$1
  at java.lang.ClassLoader.defineClass1(Native Method)
  ...

scala> class Concrete extends Derived;
defined class Concrete

scala> new Concrete
java.lang.ClassFormatError: Duplicate method name&signature in class file Concrete
  at java.lang.ClassLoader.defineClass1(Native Method)

不管这个特性有多糟糕,它的存在确实解释了为什么有时(我认为在实践中很少)有一个非最终的object 是可取的,所以之前接受的答案是正确的,这是重复的。

不过,我会养成在我的objects 决赛中标记的习惯。

更新 2 请注意,重要的是,将对象标记为 final 不会影响对象的惰性初始化语义:

scala> object PrintNonFinal{ println("PrintNonFinal") }
defined object PrintNonFinal

scala> final object PrintFinal{ println("PrintFinal") }
defined object PrintFinal

scala> identity( PrintNonFinal )
PrintNonFinal
res3: PrintNonFinal.type = PrintNonFinal$@2038ae61

scala> identity( PrintFinal )
PrintFinal
res4: PrintFinal.type = PrintFinal$@3dd4520b

更新 3 请注意,根据下面的 som-snytt,对象覆盖只能应用于实例相关类型,即 objects 嵌套(直接或间接)在类或特征中。顶级对象不能被覆盖,因此嵌套在顶级对象中的“静态可达”对象也不能没有任何干预类或特征。

正如 som-snytt 在下面的答案中所展示的,顶级对象实际上已编译为最终类。

不幸的是,很容易证明嵌套在顶级对象中的静态可访问对象在默认情况下确实会编译为非最终类,因此最好保持将除顶级对象之外的所有对象声明为最终对象的习惯,甚至是嵌入的对象在顶级对象中,除非它们嵌套在类或特征中,并且您实际上是要启用对象覆盖。请参阅下面的(已编辑)REPL 会话,展示了顶级对象的最终确定性(再次感谢 som-snytt!)和嵌套在对象声明中的不可覆盖对象声明的非确定性,除非嵌套声明显式声明为 final。

Welcome to Scala version 2.11.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_31).

scala> :paste -raw
// Entering paste mode (ctrl-D to finish)

package Foo {
  object Outer {
     object Inner {
     }
  }
}

// Exiting paste mode, now interpreting.

scala> :javap -sysinfo Foo.Outer$
  Size 343 bytes
  MD5 checksum 5502ef3151c41ab5c20ada0f0d386288
  Compiled from "<pastie>"
public final class Foo.Outer$ {
  public static final Foo.Outer$ MODULE$;
  public static {};
}

scala> :javap -sysinfo Foo.Outer$Inner$
  Size 410 bytes
  MD5 checksum fa4749f47d8f6b432841a4f9947831b1
  Compiled from "<pastie>"
public class Foo.Outer$Inner$ {
  public static final Foo.Outer$Inner$ MODULE$;
  public static {};
  public Foo.Outer$Inner$();
}

scala> :paste -raw
// Entering paste mode (ctrl-D to finish)

package bar {
  object Outer {
    final object Inner {
    }
  }
}

// Exiting paste mode, now interpreting.

scala> :javap -sysinfo bar.Outer$
  Size 343 bytes
  MD5 checksum 138a1e48b7ea4c2d6097c552c9cac440
  Compiled from "<pastie>"
public final class bar.Outer$ {
  public static final bar.Outer$ MODULE$;
  public static {};
}

scala> :javap -sysinfo bar.Outer$Inner$
  Size 410 bytes
  MD5 checksum 96c11a97df2d9630369f8f1c97db7089
  Compiled from "<pastie>"
public final class bar.Outer$Inner$ {
  public static final bar.Outer$Inner$ MODULE$;
  public static {};
  public bar.Outer$Inner$();
}

【问题讨论】:

  • @travis-brown 您能否发布一个链接,指向您认为重复的问题?我发现了与对象内 val 的最终性有关的问题,但与实现类本身的最终性无关。
  • 链接在黄色方框中的问题上方
  • 哦,谢谢。我现在明白了。我会稍微修改一下问题。
  • @SteveWaldman 我忘了我可以在scala 标签中单方面关闭——通常我认为重复投票是一种建议。如果您对链接的问题(及其接受的答案)不满意,请告诉我。
  • @TravisBrown 谢谢。让我补充一点问题,让我知道您是否认为还有更多需要考虑的地方。

标签: scala


【解决方案1】:

这是你的最终答案吗?

由于 REPL 定义被包装,你想paste -raw 编译为粘贴。

看起来顶级 Foo 和 Bar 是最终类:

scala> :pa -raw
// Entering paste mode (ctrl-D to finish)

package foo { object Foo { object Fooz } }
package bar { final object Bar { object Baz} }

// Exiting paste mode, now interpreting.


scala> :javap -sysinfo foo.Foo$
  Size 339 bytes
  MD5 checksum 628ac559ca72294bd342a7813ced5d4c
  Compiled from "<pastie>"
public final class foo.Foo$ {
  public static final foo.Foo$ MODULE$;
  public static {};
}

scala> :javap -sysinfo bar.Bar$
  Size 339 bytes
  MD5 checksum 23a123e9827f1a0a6b033253e0613545
  Compiled from "<pastie>"
public final class bar.Bar$ {
  public static final bar.Bar$ MODULE$;
  public static {};
}

scala> :javap -sysinfo foo.Foo$Fooz$
  Size 401 bytes
  MD5 checksum a081412707f1fbf42f6fa68f2c7f46e9
  Compiled from "<pastie>"
public class foo.Foo$Fooz$ {
  public static final foo.Foo$Fooz$ MODULE$;
  public static {};
  public foo.Foo$Fooz$();
}

此外,成员对象的假设覆盖是红鲱鱼或嵌合体。即使对象实际上只是惰性 val,也不清楚得到什么类型关系。

【讨论】:

  • 我不认为对象覆盖是一个嵌合体。相反,这是迄今为止为非最终对象类提供的唯一解释。很好的捕捉,重新顶级对象!但这只是强化了对象覆盖是非最终、非“静态可达”依赖对象的动机的情况。没有有意义的方式来覆盖顶级对象,也没有嵌入顶级对象的对象,因此它们的默认终结性与对象覆盖解释一致。
  • 对象是可以被覆盖的类或特征的子类,并且根据可替换性原则,覆盖对象必须是实现父对象并符合父对象的类的子类对象类型。因此,为了支持对象覆盖,依赖于实例的对象必须是非最终的。我认为这是迄今为止最好的解释。您对依赖对象默认非最终性有其他建议吗?我很相信它支持(有一天会正确实施)覆盖。
  • 尽管如此,您对顶级对象的默认终结性的观察让我有点安慰。我认为我观察到的非最终性是一个非常糟糕的权衡:Scala 似乎放弃了许多 JVM 可优化性来支持一个几乎从未使用过的特性。但是大多数对象是顶级的或静态可访问的,因此大多数对象默认情况下应该是最终的。现在只有依赖于实例的对象需要考虑是否附加final。非常感谢您指出这一点!
  • 所以,我终于开始检查,嵌套在顶级对象声明中的对象声明默认编译为非最终类。对象覆盖不能解释这一点——没有办法覆盖顶级对象,至少在包继承被发明之前是这样。我仍然怀疑对象覆盖是非顶级对象非最终性的原因,并且静态可访问的嵌套对象是非最终性的疏忽,编译器实现者只是使用嵌套作为可覆盖性的启发式。再次感谢您,您的回答真的很有帮助。
  • (哦,我已经为已经冗长的编辑问题添加了第三个详细更新,详细解释了这一点,当然要归功于你。)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-20
  • 2012-11-18
相关资源
最近更新 更多