- 简介
本文主要通过反编译工具(jd-gui)查看scala代码文件编译之后的.class文件对应的java代码来理解伴生类和伴生对象之间的关系。 - 伴生类和伴生对象的区别和联系
- 关系
伴生类中主要编写非静态代码,伴生对象中主要编写静态代码,静态代码包括属性和方法。scala中取消了static关键字,因此静态代码只能写到伴生对象中。伴生对象也是单例对象,多次修改其中的内容,后面的访问者获取到的则是最后一次修改之后的内容,而不是初始化内容。 - 使用
伴生类中的属性和方法只能通过创建对象的方式访问;伴生对象中的属性和方法只能通过 类名. 的方式访问,不能创建对象。
- 关系
- scala代码和java代码
说明:由于反编译工具的瑕疵,所有 MODULE.. 的地方都应该是 MODULE$. 。- 伴生类
- scala代码
- java代码
- scala代码
- 伴生对象
- scala代码
- java代码
- scala代码
- 使用
- scala代码
- java代码(两部分,自动创建伴生类)
- scala代码
- 伴生类
- 代码解释
伴生类中有 var name 属性和 sayHi 方法,伴生对象中有 var age 属性和 sayHi 方法,var 类型的属性会在对应的class文件中生成对应的get/set方法,不过方法名和java中的get/set方法名不一样,但是其内容是完全一样的。
伴生类生成的class文件名为 原类名.class ,伴生对象生成的class文件名为 原类名$.class 。- 伴生类对应的 原类名.class 中的代码内容
scala代码:
var name = "wzq" def sayHi(): Unit = { println("class ScalaPerson sayHi~~~") }
java代码:
public String name() { return this.name; } public void name_$eq(String x$1) { this.name = x$1; } private String name = "wzq"; public void sayHi() { Predef..MODULE$.println("class ScalaPerson sayHi~~~"); } public static void age_$eq(int paramInt) { ScalaPerson..MODULE$.age_$eq(paramInt); } public static int age() { return ScalaPerson..MODULE$.age(); }
java代码中包含:
- 伴生类对应的 原类名.class 中的代码内容
① name属性,提供对应的get/set方法:name()/name_$eq()。
② 伴生对象属性对应的get/set方法,并且这两个方法为静态方法,不过这并不是scala代码可以直接通过 类名. 的方式访问伴生对象中内容的原因,具体后面解释。
③ 伴生类中的方法。
④ 生成所有和伴生对象方法同名的静态方法,方法实现为通过伴生对象的 MODULE$ 来方法伴生对象的具体方法,因此伴生类可以访问所有伴生对象中属性和方法,即使是私有的也无所谓。(例子中由于伴生类和伴生对象都有sayHi()方法,问了防止同名方法冲突,伴生类对应的class文件中的sayHi()方法为伴生类自己的实现)
2. 伴生对象对应的 原类名$.class 中的代码内容
scala代码:
object ScalaPerson {
var age = 26
def sayHi(): Unit = {
println("object ScalaPerson sayHi~~~")
}
}
java代码:
public final class ScalaPerson$ {
public static final MODULE$;
private int age;
public int age() {
return this.age;
}
public void age_$eq(int x$1) {
this.age = x$1;
}
public void sayHi() {
Predef..MODULE$.println("object ScalaPerson sayHi~~~");
}
private ScalaPerson$() {
MODULE$ = this;
this.age = 26;
}
static {
new ();
}
}
java代码中包括:
① 伴生对象中的 age 属性,及其对应的 get/set 方法。
② 伴生对象中的方法。
③ 静态不可变的自身对象:MODULE$。
④ 静态代码块:创建自身对象。
⑤ 私有无参构造方法,初始化自身对象(对象由静态代码块创建,在类加载时只执行一次,因此该对象为单例对象),并初始化属性的值。
该类中方法执行顺序:
① 类在加载时,执行静态代码块内容,调用私有无参构造方法。
② 无参构造方法将类本身作为对象赋值给MODULE$,并初始化类的属性。
③ MODULE$为 public static final 类型,此后将作为单例对象对外界提供访问,外界通过该单例对象访问对象的所有方法。
3. 调用方伴生对象对应的 原类名.class 和 原类名$.class 对应的代码。
补充:在scala中,如果你只写了伴生对象,则编译之后会生成伴生类和伴生对象两个class文件,如果只写了伴生类,则只会生成伴生类对应的class文件。
scala代码:
object AccompanyObject {
def main(args: Array[String]): Unit = {
println(ScalaPerson.age)
ScalaPerson.sayHi()
val p = new ScalaPerson
p.sayHi()
}
}
java代码:
public final class AccompanyObject {
public static void main(String[] paramArrayOfString) {
AccompanyObject..MODULE$.main(paramArrayOfString);
}
}
public final class AccompanyObject$ {
public static final MODULE$;
static {
new ();
}
public void main(String[] args) {
Predef..MODULE$.println(BoxesRunTime.boxToInteger(ScalaPerson..MODULE$.age()));
ScalaPerson..MODULE$.sayHi();
ScalaPerson p = new ScalaPerson();
p.sayHi();
}
private AccompanyObject$() {
MODULE$ = this;
}
}
我们具体看反编译之后代码的执行顺序:
① 分析:
对于java执行来说,其入口方法肯定为 main 方法,其方法签名必定为: public static void main(String[] paramArrayOfString) ,因此虚拟机执行代码,肯定是从主类(也就是伴生类:AccompanyObject)对应的class代码开始执行的。
② 执行:
1) AccompanyObject类中的main方法:
执行以下代码:AccompanyObject$.MODULE$.main(paramArrayOfString);;,该代码实际上是通过 AccompanyObject$ 类中的单例对象 MODULE$ 来调用类的方法:
2) AccompanyObject$ 类中的单例对象 MODULE$ 的创建过程,在上面的单例对象反编译的java代码中已经解释过。
调用 AccompanyObject$ 类的 main 方法:
执行以下代码:
Predef..MODULE$.println(BoxesRunTime.boxToInteger(ScalaPerson$.MODULE$.age()));
ScalaPerson$.MODULE$.sayHi();
ScalaPerson p=new ScalaPerson();
p.sayHi();
a. 第一行对应于scala代码中的输出伴生对象的age属性的值,其执行逻辑为:
通过伴生对象(调用方AccompanyObject$)对应的单例对象 MODULE$ 直接调用 println 方法, println 方法参数也是调用方法,其执行顺序为:通过被调用方法伴生对象ScalaPerson$的单例对象 MODULE$ 来调用 age() 方法。
b. 第二行对应于 scala 代码中调用伴生对象的 sayHi() 方法,其执行逻辑为:
通过被调用方法伴生对象ScalaPerson$的单例对象 MODULE$ 来调用 sayHi() 方法。
c. 第三行创建一个伴生类的对象。
d. 第四行通过创建的伴生类的对象调用伴生类的 sayHi() 方法。
5. 执行顺序总结
(1) 入口main方法
scala中的入口方法必须写在伴生对象中,但伴生对象会自动产生伴生类对应的class文件,java虚拟机是将伴生类对应的class文件中的 main方法作为入口来执行代码的,具体原因为:
伴生类中的main方法签名为: public static void main(String[] paramArrayOfString)
伴生对中的main方法签名为:public void main(String[] args)
伴生类中的main方法才是JVM识别的主方法,其实现为调用伴生对象的main方法。伴生对象中的main方法为普通方法,只不过方法名为main而已,其实现为scala伴生对象中main方法的java实现。
入口main方法通过调用伴生对象对应class文件中的单例对象 MODULE$ 来调用main方法,该main方法中编写了scala中真正的代码。
(2) 伴生对象的main方法
该main方法实现为scala中伴生对象的main方法的java具体实现:
如果scala中调用了伴生对象的方法或属性(其实访问或改变属性,也是在执行对应的get/set方法),则是通过伴生对象对应的class中的单例对象MODULE$来调用对应的方法。
如果scala中调用了伴生类的方法,其必定需要先创建伴生类的对象,然后通过创建的对象访问伴生类的方法。调用伴生对象的方法不需要创建对象,主要是因为其java实现为:通过调用伴生对象的class中的单例对象 MODULE$ 来调用方法。因此在scala中并没有真正静态的概念,因为看起来像静态调用访问和调用的地方,实际上是通过伴生对象自己的静态单例对象来访问和调用的。
6. 总结
我们在编写scala代码时,访问伴生类的内容,需要创建对象来访问;访问伴生对象时,将其内部所有内容直接作为静态,通过 类名. 的方式调用即可。