伴生类和伴生对象之间的关系及其执行顺序(反编译)

  1. 简介
    本文主要通过反编译工具(jd-gui)查看scala代码文件编译之后的.class文件对应的java代码来理解伴生类和伴生对象之间的关系。
  2. 伴生类和伴生对象的区别和联系
    1. 关系
      伴生类中主要编写非静态代码,伴生对象中主要编写静态代码,静态代码包括属性和方法。scala中取消了static关键字,因此静态代码只能写到伴生对象中。伴生对象也是单例对象,多次修改其中的内容,后面的访问者获取到的则是最后一次修改之后的内容,而不是初始化内容。
    2. 使用
      伴生类中的属性和方法只能通过创建对象的方式访问;伴生对象中的属性和方法只能通过 类名. 的方式访问,不能创建对象。
  3. scala代码和java代码
    说明:由于反编译工具的瑕疵,所有 MODULE.. 的地方都应该是 MODULE$. 。
    1. 伴生类
      • scala代码
      • java代码
    2. 伴生对象
      • scala代码
      • java代码
    3. 使用
      • scala代码
      • java代码(两部分,自动创建伴生类)

  4. 代码解释
    伴生类中有 var name 属性和 sayHi 方法,伴生对象中有 var age 属性和 sayHi 方法,var 类型的属性会在对应的class文件中生成对应的get/set方法,不过方法名和java中的get/set方法名不一样,但是其内容是完全一样的。
    伴生类生成的class文件名为 原类名.class ,伴生对象生成的class文件名为 原类名$.class 。
    1. 伴生类对应的 原类名.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代码中包含:

          ① 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代码时,访问伴生类的内容,需要创建对象来访问;访问伴生对象时,将其内部所有内容直接作为静态,通过 类名. 的方式调用即可。

发布了20 篇原创文章 · 获赞 47 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/u012443641/article/details/89921243