学习Scala:Scala中的字段和方法
2014-04-02 22:36
375 查看
本文基于class字节码来分析在Scala语言中, 一个类中的字段和方法是如何实现的, 并且对比和java实现方式的区别。
首先看一段简单的源码:
这个类很简单, 其中有两个字段和一个方法:
i字段被声明为var, 是可变的,类似于java中的普通变量;
j字段被声明为val, 是不可变的, 类似于java中的final , 一旦被初始化, 就不能被改变;
add方法没有参数, 并且返回Int, 计算的是i和j的和。
源码很简单。 下面我们编译这个类, 并且反编译class文件, 来看看scala中的字段和方法到底编译成了什么形式。
编译源文件:
反编译字节码:
之所以加上-private选项, 是因为javap命令默认不会输出私有成员的信息, 加上这个选项就可以输出私有成员的信息。
下面是输出结果:
源码虽然很简短, 但是字节码却很长。 这不得不让我们怀疑, scalac编译器在编译源码的时候做了什么手脚。下面我们仔细分析反编译结果。
首先看字段:
源文件中的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 。 但是看它的字节码指令:
其中并没有getfield指令, 对这i字段的访问, 是通过调用自动生成的方法private int i(); , 而对字段j的访问, 是通过调用自动生成的方法 private int j(); 。
这说明, 在源码中, 所有对字段的显示访问, 都会在class中编译成通过getter和setter方法来访问。这也说名了为什么下面的代码不能通过编译:
编译这个类, 会得到如下错误提示:
提示的大概意思是, abc方法重复定义了。 也就是说, 编译器为abc字段自动生成一个abc方法, 然后源文件中也定义了一个abc方法, 所以方法冲突。
至于为什么会编译成这样, 应该是想通过这种方式, 让字段和方法位于相同的层次上, 也就是让字段和方法位于相同的命名空间中。如何用java来实现的话, 有点像这样:
源文件中所有对字段的显式访问, 都会编译成通过getter和setter方法对字段进行访问。
由此可见, 编译器会我们做了大量的工作, 这正是scala代码会比java代码简洁的原因, 听说实现相同的项目, scala能比java少些一半的代码。 让我们记住这条规则: 在写scala程序时, 你不是一个人在编码, 而是在和scalac一同工作 。
本文基于分析class字节码来分析scala, 对class文件格式不熟悉的同学, 可以参考我的专栏: 深入理解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命令默认不会输出私有成员的信息, 加上这个选项就可以输出私有成员的信息。
下面是输出结果:
Classfile /D:/Workspace/scala/scala-test/FunctionTest/FieldMethodTest.class Last modified 2014-4-2; size 1017 bytes 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一同工作 。
本文基于分析class字节码来分析scala, 对class文件格式不熟悉的同学, 可以参考我的专栏: 深入理解Java语言 。
相关文章推荐
- scala学习之路:10. Abstract抽象类抽象字段抽象方法
- scala学习手记12 - 字段、方法和构造函数
- Scala学习回顾(七)---- 抽象类、抽象字段、抽象方法
- Scala学习第十二天 Scala中的继承:超类的构造、重写字段、重写方法代码实战
- Scala学习第十三天 抽象类、抽象字段、抽象方法
- scala学习之路:9.Override重写字段或者方法
- scala中的字段和成员方法的底层class字节文件分析
- MyBatis学习笔记:表字段名与实体类属性名不一致的解决方法
- scala编程笔记(三)类,字段和方法
- Scala学习 day01 Scala的类/方法/对象/单例对象
- Dynamic CRM 2013学习笔记(二十三)CRM JS智能提示(CRM 相关的方法、属性以及页面字段),及发布前调试
- scala学习笔记4(apply方法)
- Delphi语言学习8-类成员(字段和方法)
- scala 学习之:List fold, foldLeft方法
- scala学习-Scala class的构造方法与继承
- JMeter学习-019-JMeter 监听器之【聚合报告】界面字段解析及计算方法概要说明
- Scala学习笔记27【泛型类、泛型方法、Bounds入门实战】
- scala学习笔记4(apply方法)
- scala 学习笔记(04) OOP(上)主从构造器/私有属性/伴生对象(单例静态类)/apply方法/嵌套类
- Scala学习第七天 Scala类的属性和对象私有字段实战详解