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

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


首先看一段简单的源码:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class FieldMethodTest{  
  2.   
  3.     private var i = 0  
  4.     private val j = 0     
  5.       
  6.     def add() : Int = i + j  
  7.   
  8. }  


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


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

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

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


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


编译源文件:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. scalac FieldMethodTest.scala  


反编译字节码:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. javap -c -v -private -classpath . FieldMethodTest  


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


下面是输出结果:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  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.   
  55.   
  56.   private final int j;  
  57.     flags: ACC_PRIVATE, ACC_FINAL  
  58.   
  59.   
  60.   private int i();  
  61.     flags: ACC_PRIVATE  
  62.     Code:  
  63.       stack=1, locals=1, args_size=1  
  64.          0: aload_0  
  65.          1: getfield      #14                 // Field i:I  
  66.          4: ireturn  
  67.       LocalVariableTable:  
  68.         Start  Length  Slot  Name   Signature  
  69.                0       5     0  this   LFieldMethodTest;  
  70.       LineNumberTable:  
  71.         line 30  
  72.   
  73.   private void i_$eq(int);  
  74.     flags: ACC_PRIVATE  
  75.     Code:  
  76.       stack=2, locals=2, args_size=2  
  77.          0: aload_0  
  78.          1: iload_1  
  79.          2: putfield      #14                 // Field i:I  
  80.          5return  
  81.       LocalVariableTable:  
  82.         Start  Length  Slot  Name   Signature  
  83.                0       6     0  this   LFieldMethodTest;  
  84.                0       6     1   x$1   I  
  85.       LineNumberTable:  
  86.         line 30  
  87.   
  88.   private int j();  
  89.     flags: ACC_PRIVATE  
  90.     Code:  
  91.       stack=1, locals=1, args_size=1  
  92.          0: aload_0  
  93.          1: getfield      #21                 // Field j:I  
  94.          4: ireturn  
  95.       LocalVariableTable:  
  96.         Start  Length  Slot  Name   Signature  
  97.                0       5     0  this   LFieldMethodTest;  
  98.       LineNumberTable:  
  99.         line 40  
  100.   
  101.   public int add();  
  102.     flags: ACC_PUBLIC  
  103.     Code:  
  104.       stack=2, locals=1, args_size=1  
  105.          0: aload_0  
  106.          1: invokespecial #24                 // Method i:()I  
  107.          4: aload_0  
  108.          5: invokespecial #26                 // Method j:()I  
  109.          8: iadd  
  110.          9: ireturn  
  111.       LocalVariableTable:  
  112.         Start  Length  Slot  Name   Signature  
  113.                0      10     0  this   LFieldMethodTest;  
  114.       LineNumberTable:  
  115.         line 60  
  116.   
  117.   public FieldMethodTest();  
  118.     flags: ACC_PUBLIC  
  119.     Code:  
  120.       stack=2, locals=1, args_size=1  
  121.          0: aload_0  
  122.          1: invokespecial #30                 // Method java/lang/Object."<init>":()V  
  123.          4: aload_0  
  124.          5: iconst_0  
  125.          6: putfield      #14                 // Field i:I  
  126.          9: aload_0  
  127.         10: iconst_0  
  128.         11: putfield      #21                 // Field j:I  
  129.         14return  
  130.       LocalVariableTable:  
  131.         Start  Length  Slot  Name   Signature  
  132.                0      15     0  this   LFieldMethodTest;  
  133.       LineNumberTable:  
  134.         line 10  
  135.         line 34  
  136.         line 49  
  137. }  



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


首先看字段:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. private int i;  
  2.    flags: ACC_PRIVATE  
  3.   
  4.   
  5.  private final int j;  
  6.    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
  在CODE上查看代码片 派生到我的代码片
  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 60  


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


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

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class FieldMethodTest{  
  2.   
  3.     private var abc = 0   
  4.       
  5.     def abc() : Int = {1 + 2 + 3}  
  6.   
  7. }  


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

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. scalac FieldMethodTest.scala  
  2.   
  3. FieldMethodTest.scala:5: error: method abc is defined twice  
  4.   conflicting symbols both originated in file 'D:\Workspace\scala\scala-test\Fun  
  5. ctionTest\FieldMethodTest.scala'  
  6.         def abc() : Int = {1 + 2 + 3}  
  7.             ^  
  8. one error found  


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


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

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class FieldMethodTest{  
  2.   
  3.     private int i = 0  
  4.     private final j = 0     
  5.       
  6.     private int i(){  
  7.         return i;  
  8.     }  
  9.   
  10.     private void setI(int i){  
  11.         this.i = i;  
  12.     }  
  13.   
  14.     private int j(){  
  15.         return j;  
  16.     }  
  17.   
  18.     int add(){  
  19.   
  20.         return i() + j();  
  21.     }  
  22.   
  23. }  


总结

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


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


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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值