scala中的字段和成员方法的底层class字节文件分析

本文基于class字节码来分析在Scala语言中, 一个类中的字段和方法是如何实现的, 并且对比和java实现方式的区别。

首先看一段简单的源码:

[java] view
plain
 copy

  1. class FieldMethodTest{
  2. private var i = 0
  3. private val j = 0
  4. def add() : Int = i + j
  5. }

这个类很简单, 其中有两个字段和一个方法:

i字段被声明为var, 是可变的,类似于java中的普通变量;

j字段被声明为val, 是不可变的, 类似于java中的final , 一旦被初始化, 就不能被改变;

add方法没有参数, 并且返回Int, 计算的是i和j的和。

源码很简单。 下面我们编译这个类, 并且反编译class文件, 来看看scala中的字段和方法到底编译成了什么形式。

编译源文件:

[java] view
plain
 copy

  1. scalac FieldMethodTest.scala

反编译字节码:

[java] view
plain
 copy

  1. javap -c -v -private -classpath . FieldMethodTest

之所以加上-private选项, 是因为javap命令默认不会输出私有成员的信息, 加上这个选项就可以输出私有成员的信息。

下面是输出结果:

[java] view
plain
 copy

  1. Classfile /D:/Workspace/scala/scala-test/FunctionTest/FieldMethodTest.class
  2. Last modified 2014-4-2; size 1017 bytes
  3. MD5 checksum 57c9795df9f0e79c3c3bfa9de3f98096
  4. Compiled from "FieldMethodTest.scala"
  5. public class FieldMethodTest
  6. SourceFile: "FieldMethodTest.scala"
  7. RuntimeVisibleAnnotations:
  8. 0: #6(#7=s#8)
  9. ScalaSig: length = 0x3
  10. 05 00 00
  11. minor version: 0
  12. major version: 50
  13. flags: ACC_PUBLIC, ACC_SUPER
  14. Constant pool:
  15. #1 = Utf8               FieldMethodTest
  16. #2 = Class              #1             //  FieldMethodTest
  17. #3 = Utf8               java/lang/Object
  18. #4 = Class              #3             //  java/lang/Object
  19. #5 = Utf8               FieldMethodTest.scala
  20. #6 = Utf8               Lscala/reflect/ScalaSignature;
  21. #7 = Utf8               bytes
  22. #8 = Utf8               !......
  23. #9 = Utf8               i
  24. #10 = Utf8               I
  25. #11 = Utf8               j
  26. #12 = Utf8               ()I
  27. #13 = NameAndType        #9:#10         //  i:I
  28. #14 = Fieldref           #2.#13         //  FieldMethodTest.i:I
  29. #15 = Utf8               this
  30. #16 = Utf8               LFieldMethodTest;
  31. #17 = Utf8               i_$eq
  32. #18 = Utf8               (I)V
  33. #19 = Utf8               x$1
  34. #20 = NameAndType        #11:#10        //  j:I
  35. #21 = Fieldref           #2.#20         //  FieldMethodTest.j:I
  36. #22 = Utf8               add
  37. #23 = NameAndType        #9:#12         //  i:()I
  38. #24 = Methodref          #2.#23         //  FieldMethodTest.i:()I
  39. #25 = NameAndType        #11:#12        //  j:()I
  40. #26 = Methodref          #2.#25         //  FieldMethodTest.j:()I
  41. #27 = Utf8               <init>
  42. #28 = Utf8               ()V
  43. #29 = NameAndType        #27:#28        //  "<init>":()V
  44. #30 = Methodref          #4.#29         //  java/lang/Object."<init>":()V
  45. #31 = Utf8               Code
  46. #32 = Utf8               LocalVariableTable
  47. #33 = Utf8               LineNumberTable
  48. #34 = Utf8               SourceFile
  49. #35 = Utf8               RuntimeVisibleAnnotations
  50. #36 = Utf8               ScalaSig
  51. {
  52. private int i;
  53. flags: ACC_PRIVATE
  54. private final int j;
  55. flags: ACC_PRIVATE, ACC_FINAL
  56. private int i();
  57. flags: ACC_PRIVATE
  58. Code:
  59. stack=1, locals=1, args_size=1
  60. 0: aload_0
  61. 1: getfield      #14                 // Field i:I
  62. 4: ireturn
  63. LocalVariableTable:
  64. Start  Length  Slot  Name   Signature
  65. 0       5     0  this   LFieldMethodTest;
  66. LineNumberTable:
  67. line 3: 0
  68. private void i_$eq(int);
  69. flags: ACC_PRIVATE
  70. Code:
  71. stack=2, locals=2, args_size=2
  72. 0: aload_0
  73. 1: iload_1
  74. 2: putfield      #14                 // Field i:I
  75. 5: return
  76. LocalVariableTable:
  77. Start  Length  Slot  Name   Signature
  78. 0       6     0  this   LFieldMethodTest;
  79. 0       6     1   x$1   I
  80. LineNumberTable:
  81. line 3: 0
  82. private int j();
  83. flags: ACC_PRIVATE
  84. Code:
  85. stack=1, locals=1, args_size=1
  86. 0: aload_0
  87. 1: getfield      #21                 // Field j:I
  88. 4: ireturn
  89. LocalVariableTable:
  90. Start  Length  Slot  Name   Signature
  91. 0       5     0  this   LFieldMethodTest;
  92. LineNumberTable:
  93. line 4: 0
  94. public int add();
  95. flags: ACC_PUBLIC
  96. Code:
  97. stack=2, locals=1, args_size=1
  98. 0: aload_0
  99. 1: invokespecial #24                 // Method i:()I
  100. 4: aload_0
  101. 5: invokespecial #26                 // Method j:()I
  102. 8: iadd
  103. 9: ireturn
  104. LocalVariableTable:
  105. Start  Length  Slot  Name   Signature
  106. 0      10     0  this   LFieldMethodTest;
  107. LineNumberTable:
  108. line 6: 0
  109. public FieldMethodTest();
  110. flags: ACC_PUBLIC
  111. Code:
  112. stack=2, locals=1, args_size=1
  113. 0: aload_0
  114. 1: invokespecial #30                 // Method java/lang/Object."<init>":()V
  115. 4: aload_0
  116. 5: iconst_0
  117. 6: putfield      #14                 // Field i:I
  118. 9: aload_0
  119. 10: iconst_0
  120. 11: putfield      #21                 // Field j:I
  121. 14: return
  122. LocalVariableTable:
  123. Start  Length  Slot  Name   Signature
  124. 0      15     0  this   LFieldMethodTest;
  125. LineNumberTable:
  126. line 1: 0
  127. line 3: 4
  128. line 4: 9
  129. }

源码虽然很简短, 但是字节码却很长。 这不得不让我们怀疑, scalac编译器在编译源码的时候做了什么手脚。下面我们仔细分析反编译结果。

首先看字段:

[java] view
plain
 copy

  1. private int i;
  2. flags: ACC_PRIVATE
  3. private final int j;
  4. flags: ACC_PRIVATE, ACC_FINAL

源文件中的i使用var声明, 编译后是普通的私有变量, j在源文件中用val声明, 编译后被加上了ACC_FINAL标志, 可以认为是不可变的, 与java中的final关键字的语义是一样的。

然后看方法信息:

我们在源文件中只定义了一个方法add, 字节码中却出现了5个方法!!!这确实有点让人抓狂。不用多说, 肯定有4个是scala编译器自动生成的。下面我们逐一分析:

1) 自动生成构造方法 public FieldMethodTest();

这个现象很正常, 即使是在java中, 如果你不定义构造方法的话, javac编译器也会自动生成一个无参数构造方法。根据该方法的字节码我们可以看到, 构造方法的逻辑是先使用invokespecial指令调用父类Object的构造方法, 然后用putfield指令初始化字段i和字段j 。

2)自动生成方法 private int i();

这个方法让人很费解, 它的方法体中的逻辑是使用getfield指令获取字段i的值, 并返回字段i的值。 类似于java中的getter方法。

3)自动生成方法 private void i_$eq(int);

它的方法体中的逻辑是, 使用传入的参数, 为变量i赋值。类似于java中的setter方法。

4)自动生成方法 private int j();

它的方法体中的逻辑是使用getfield指令获取字段j的值, 并返回字段j的值。 类似于java中的getter方法。

之所以没有生成和j字段相对的 private void j_$eq(int); 方法, 是因为j是不可变的, 在初始化后, 就不能通过setter改变它的值。

下面分析源码中的add方法对应的class中的方法。

add方法, 编译到class文件中之后, 生成了方法 public int add();  , 在源码中, 该方法的逻辑很简单, 直接将i 和 j相加, 然后返回相加后的和。 既然是将两个变量相加, 那么我们猜想,在方法体中必然存在访问这两个字段的指令getfield 。 但是看它的字节码指令:

[java] view
plain
 copy

  1. public int add();
  2. flags: ACC_PUBLIC
  3. Code:
  4. stack=2, locals=1, args_size=1
  5. 0: aload_0
  6. 1: invokespecial #24                 // Method i:()I
  7. 4: aload_0
  8. 5: invokespecial #26                 // Method j:()I
  9. 8: iadd
  10. 9: ireturn
  11. LocalVariableTable:
  12. Start  Length  Slot  Name   Signature
  13. 0      10     0  this   LFieldMethodTest;
  14. LineNumberTable:
  15. line 6: 0

其中并没有getfield指令, 对这i字段的访问, 是通过调用自动生成的方法private int i();   , 而对字段j的访问, 是通过调用自动生成的方法 private int j(); 。

这说明, 在源码中, 所有对字段的显示访问, 都会在class中编译成通过getter和setter方法来访问。这也说名了为什么下面的代码不能通过编译:

[java] view
plain
 copy

  1. class FieldMethodTest{
  2. private var abc = 0
  3. def abc() : Int = {1 + 2 + 3}
  4. }

编译这个类, 会得到如下错误提示:

[java] view
plain
 copy

  1. scalac FieldMethodTest.scala
  2. FieldMethodTest.scala:5: error: method abc is defined twice
  3. conflicting symbols both originated in file ‘D:\Workspace\scala\scala-test\Fun
  4. ctionTest\FieldMethodTest.scala‘
  5. def abc() : Int = {1 + 2 + 3}
  6. ^
  7. one error found

提示的大概意思是, abc方法重复定义了。 也就是说, 编译器为abc字段自动生成一个abc方法, 然后源文件中也定义了一个abc方法, 所以方法冲突。

至于为什么会编译成这样, 应该是想通过这种方式, 让字段和方法位于相同的层次上, 也就是让字段和方法位于相同的命名空间中。如何用java来实现的话, 有点像这样:

[java] view
plain
 copy

  1. class FieldMethodTest{
  2. private int i = 0
  3. private final j = 0
  4. private int i(){
  5. return i;
  6. }
  7. private void setI(int i){
  8. this.i = i;
  9. }
  10. private int j(){
  11. return j;
  12. }
  13. int add(){
  14. return i() + j();
  15. }
  16. }

总结

scalac编译器会为类中的var字段自动添加setter和getter方法, 会为类中的val字段自动添加getter方法。 其中的getter方法名和字段名相同。

源文件中所有对字段的显式访问, 都会编译成通过getter和setter方法对字段进行访问。

由此可见, 编译器会我们做了大量的工作, 这正是scala代码会比java代码简洁的原因, 听说实现相同的项目, scala能比java少些一半的代码。 让我们记住这条规则: 在写scala程序时, 你不是一个人在编码, 而是在和scalac一同工作 。

时间: 2024-08-09 19:48:02

scala中的字段和成员方法的底层class字节文件分析的相关文章

scala中的伴生对象实现原理

孤立对象是只有一个object关键字修饰的对象. 该对象会编译成两个class文件, 一个是以孤立对象的名字命名的class,  一个是以孤立对象的名字后面加上一个$字符命名的class, 这个class又叫做虚构类. 源码中的孤立对象中的字段和方法, 都被编译成以孤立对象的名字命名的class中的静态方法, 这些静态方法都会访问单例的虚构类对象. 虚构了是传统意义上的单例模式, 并且在类初始化的时候有, 就会创建唯一的对象. 源码中的所有字段和方法都会在虚构类中有相对应的成员. 如果不明白的可

scala中的孤立对象实现原理

<Scala编程>这本书中, 把孤立对象和伴生对象都叫做单例对象.孤立对象指的是只有一个使用object关键字定义的对象, 伴生对象是指有一个使用object关键字定义的对象, 除此之外还有一个使用class关键字定义的同名类, 这个同名的类叫做伴生类.在Scala中单例对象这个概念多少都会让人迷惑, 按<Scala编程>这本书中的说法, 使用object关键字修饰的对象就叫做单例对象.其实这里的单例和设计模式中的单例模式的概念并不尽相同.在Scala中没有静态的概念, 所有的东西

Scala学习(五)---Scala中的类

Scala中的类 摘要: 在本篇中,你将会学习如何用Scala实现类.如果你了解Java或C++中的类,你不会觉得这有多难,并且你会很享受Scala更加精简的表示法带来的便利.本篇的要点包括: 1. 类中的字段自动带有getter方法和setter方法 2. 你可以用定制的getter/setter方法替换掉字段的定义,而不必修改使用类的客户端,这就是所谓的"统一访问原则" 3. 用@BeanProperty注解来生成JavaBeans的getXxx/setXxx()方法 4. 每个类

Scala中的类

Scala中的类 摘要: 在本篇中,你将会学习如何用Scala实现类.如果你了解Java或C++中的类,你不会觉得这有多难,并且你会很享受Scala更加精简的表示法带来的便利.本篇的要点包括: 1. 类中的字段自动带有getter方法和setter方法 2. 你可以用定制的getter/setter方法替换掉字段的定义,而不必修改使用类的客户端,这就是所谓的"统一访问原则" 3. 用@BeanProperty注解来生成JavaBeans的getXxx/setXxx()方法 4. 每个类

Scala中class和object的区别

1.class scala的类和C#中的类有点不一样,诸如: 声明一个未用priavate修饰的字段 var age,scala编译器会字段帮我们生产一个私有字段和2个公有方法get和set ,这和C#的简易属性类似:若使用了private修饰,则它的方法也将会是私有的.这就是所谓的统一访问原则. [java] view plain copy print? //类默认是public级别的 class Person{ var age=18  //字段必须得初始化() def Age=age //这

scala中object和class的理解---apply方法是初始化方法

1.class Scala的类和C#中的类有点不一样,诸如: 声明一个未用priavate修饰的字段 var age,scala编译器会字段帮我们生产一个私有字段和2个公有方法get和set ,这和C#的简易属性类似:若使用了private修饰,则它的方法也将会是私有的.这就是所谓的统一访问原则. 细节的东西太多,还是上代码在注释里面细讲吧 [java] view plain copy //类默认是public级别的 class Person{ var age=18  //字段必须得初始化()

scala学习手记16 &ndash; scala中的static

前面两节学了scala的对象和伴生对象,这两个在使用的时候很有些java的静态成员的意思. scala中没有静态字段和静态方法.静态成员会破坏scala所支持的完整的面向对象模型.不过可以通过伴生对象实现对scala的类一级的操作. 回过头来再看一遍那个Marker的例子,略做了一些调整: class Marker private(val color: String) { println("Creating " + this) override def toString(): Stri

scala入门教程:scala中的面向对象定义类,构造函数,继承

我们知道scala中一切皆为对象,函数也是对象,数字也是对象,它是一个比java还要面向对象的语言. 定义scala的简单类 class Point (val x:Int, val y:Int) 上面一行代码就是一个scala类的定义: 首先是关键字class 其后是类名 Point 类名之后的括号中是构造函数的参数列表,这里相当于定义了对象的两个常量,其名称分别为x,y,类型都是Int 上面的类和下面的类是一致的,不过更精简了. class Point (xArg:Int, yArg:Int)

Scala中的特质详解

Scala中的特质与Java中的接口是比较类似的,但是Scala中的特质可以同时拥有抽象方法和具体方法,而类可以实现多个特质.下面我们详细讲解Scala中的特质这个强大的功能. 1. 把特质当作接口使用 我们定义一个trait,如下所示: 1 trait Logger { 2 def log(msg: String) 3 } 需要注意的是trait中未被实现的方法默认是抽象方法,因此不需要在方法前加abstract. 子类ConsoleLogger对Logger的实现,如下所示: 1 class