美文网首页
理解Scala中的字段和方法

理解Scala中的字段和方法

作者: tracy_668 | 来源:发表于2021-01-21 08:09 被阅读0次

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

    首先看一段简单的源码:

    class FieldMethodTest{
     
        private var i = 0
        private val j = 0   
        
        def add() : Int = i + j
     
    }
    

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

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

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

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

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

    编译源文件:

    scalac FieldMethodTest.scala
    

    反编译字节码:

    javap -c -v -private -classpath . FieldMethodTest
    

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

    下面是输出结果:

    MD5 checksum 57c9795df9f0e79c3c3bfa9de3f98096
      Compiled from "FieldMethodTest.scala"
    public class FieldMethodTest
      SourceFile: "FieldMethodTest.scala"
      RuntimeVisibleAnnotations:
        0: #6(#7=s#8)
        ScalaSig: length = 0x3
         05 00 00
      minor version: 0
      major version: 50
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Utf8               FieldMethodTest
       #2 = Class              #1             //  FieldMethodTest
       #3 = Utf8               java/lang/Object
       #4 = Class              #3             //  java/lang/Object
       #5 = Utf8               FieldMethodTest.scala
       #6 = Utf8               Lscala/reflect/ScalaSignature;
       #7 = Utf8               bytes
       #8 = Utf8               !......
       #9 = Utf8               i
      #10 = Utf8               I
      #11 = Utf8               j
      #12 = Utf8               ()I
      #13 = NameAndType        #9:#10         //  i:I
      #14 = Fieldref           #2.#13         //  FieldMethodTest.i:I
      #15 = Utf8               this
      #16 = Utf8               LFieldMethodTest;
      #17 = Utf8               i_$eq
      #18 = Utf8               (I)V
      #19 = Utf8               x$1
      #20 = NameAndType        #11:#10        //  j:I
      #21 = Fieldref           #2.#20         //  FieldMethodTest.j:I
      #22 = Utf8               add
      #23 = NameAndType        #9:#12         //  i:()I
      #24 = Methodref          #2.#23         //  FieldMethodTest.i:()I
      #25 = NameAndType        #11:#12        //  j:()I
      #26 = Methodref          #2.#25         //  FieldMethodTest.j:()I
      #27 = Utf8               <init>
      #28 = Utf8               ()V
      #29 = NameAndType        #27:#28        //  "<init>":()V
      #30 = Methodref          #4.#29         //  java/lang/Object."<init>":()V
      #31 = Utf8               Code
      #32 = Utf8               LocalVariableTable
      #33 = Utf8               LineNumberTable
      #34 = Utf8               SourceFile
      #35 = Utf8               RuntimeVisibleAnnotations
      #36 = Utf8               ScalaSig
    {
      private int i;
        flags: ACC_PRIVATE
     
     
      private final int j;
        flags: ACC_PRIVATE, ACC_FINAL
     
     
      private int i();
        flags: ACC_PRIVATE
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: getfield      #14                 // Field i:I
             4: ireturn
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                   0       5     0  this   LFieldMethodTest;
          LineNumberTable:
            line 3: 0
     
      private void i_$eq(int);
        flags: ACC_PRIVATE
        Code:
          stack=2, locals=2, args_size=2
             0: aload_0
             1: iload_1
             2: putfield      #14                 // Field i:I
             5: return
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                   0       6     0  this   LFieldMethodTest;
                   0       6     1   x$1   I
          LineNumberTable:
            line 3: 0
     
      private int j();
        flags: ACC_PRIVATE
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: getfield      #21                 // Field j:I
             4: ireturn
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                   0       5     0  this   LFieldMethodTest;
          LineNumberTable:
            line 4: 0
     
      public int add();
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: invokespecial #24                 // Method i:()I
             4: aload_0
             5: invokespecial #26                 // Method j:()I
             8: iadd
             9: ireturn
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                   0      10     0  this   LFieldMethodTest;
          LineNumberTable:
            line 6: 0
     
      public FieldMethodTest();
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: invokespecial #30                 // Method java/lang/Object."<init>":()V
             4: aload_0
             5: iconst_0
             6: putfield      #14                 // Field i:I
             9: aload_0
            10: iconst_0
            11: putfield      #21                 // Field j:I
            14: return
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                   0      15     0  this   LFieldMethodTest;
          LineNumberTable:
            line 1: 0
            line 3: 4
            line 4: 9
    }
    
    

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

    首先看字段:

     private int i;
        flags: ACC_PRIVATE
     
     
      private final int j;
        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 。 但是看它的字节码指令:

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

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

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

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

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

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

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

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

    class FieldMethodTest{
     
        private int i = 0
        private final j = 0   
        
        private int i(){
            return i;
        }
     
        private void setI(int i){
            this.i = i;
        }
     
        private int j(){
            return j;
        }
     
        int add(){
     
            return i() + j();
        }
     
    }
    

    总结

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

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

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

    相关文章

      网友评论

          本文标题:理解Scala中的字段和方法

          本文链接:https://www.haomeiwen.com/subject/ktkrzktx.html