值类型的一些细节

看了老赵的这篇blog: 针对struct对象使用using关键字是否会引起装箱?

对比一下scala里的value class(值类型)来看看scala里对一个value class调用其方法时,是怎么做的。
其实,在之前的这篇 值类型的一些限制 里已经提到过AnyVal文档里的这句话:

by replacing virtual method invocations with static method invocations.(value class用静态方法调用替代了虚方法调用)

不过是否只是在值类型不被装箱的情况下方法调用才会调用静态方法?如果我们对值类型进行装箱,然后对装箱后的对象再调用内部方法,是否还是静态方法调用呢?我们来验证一下。先看看值类型编译后的信息:

// B.scala
class B(val s:String) extends AnyVal {
    def say() { println("hi") }
}

//反编译上面编译后的class,
hongjiang@whj-mbp /tmp % javap B 
Compiled from "B.scala"
public final class B {
  public static boolean equals$extension(java.lang.String, java.lang.Object);
  public static int hashCode$extension(java.lang.String);
  public static void say$extension(java.lang.String);
  public java.lang.String s();
  public void say();
  public int hashCode();
  public boolean equals(java.lang.Object);
  public B(java.lang.String);
}

// 反编译B的伴生对象
hongjiang@whj-mbp /tmp % javap B$
Compiled from "B.scala"
public final class B$ {
  public static final B$ MODULE$;
  public static {};
  public final void say$extension(java.lang.String);
  public final int hashCode$extension(java.lang.String);
  public final boolean equals$extension(java.lang.String, java.lang.Object);
}

在伴生对象里存在几个 $extension 方法,比如say$extension 与伴生类中的say方法是什么关系呢?
(在伴生类中还有几个静态的$extension方法,这几个是伴生对象方法里的静态代理,不用关注)

通过反编译信息可以看到 say 方法其实是调用的半生对象中的say$extension方法:

public void say();
  flags: ACC_PUBLIC
  Code:
  stack=2, locals=1, args_size=1
     0: getstatic     #16                 // Field B$.MODULE$:LB$;
     3: aload_0
     4: invokevirtual #37                 // Method s:()Ljava/lang/String;
     7: invokevirtual #26                 // Method B$.say$extension:(Ljava/lang/String;)V
    10: return
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
           0      11     0  this   LB;
  LineNumberTable:
    line 2: 0

编译时的语法树也很明确的表示say方法调用的是伴生对象里的say$extension :

def say(): Unit = B.say$extension(B.this.s());

equals和hashCode方法也是同理调用伴生对象中的$extension方法,不过这2个方法较特殊,在值类型中是不允许自己实现equals和hashCode的。

现在来看看值类型中的方法在被调用时,是怎么回事:

class B(val s:String) extends AnyVal {
    def say() { println(s) }
}

object Main extends App {
    def boot(b:B) {
        b.say  // 调用值类型的方法 ----> 这行
    }

    boot(new B("hello")) 
}

通过 scalac -Xprint:all 来分析,发现在erasure阶段前,boot方法的定义:

def boot(b: B): Unit = b.say();

posterasure阶段,编译器把上面的方法翻译成了下面的:

def boot(b: String): Unit = B.this.say$extension(b);

注意,编译器在这一步处理后boot传入的是String,而不是值类型B,等于剥掉了值类型这层wrapper,直接用的B内部的数据;并且方法体被替代成了B.this.say$extension(b) 原本的虚方法调用也变成了对final方法的调用。

所以因为编译器在编译时就对方法体做了替换,使得运行时压根就不会出现虚方法调用,也不会因为方法调用而产生装箱的情况。

再看 boot(new B("hello")) 这一句,因为boot方法最后被翻译为接受String类型的方法,所以这一句在编译时也被翻译成了下面的语句:

boot("hello")  // new B("hello") 被编译器剥掉值类型的壳,直接传入"hello"

那在什么情况下值类型会被装箱呢?

1) 赋给另一个类型的变量

$ cat A.scala
class A(val s:String) extends AnyVal
class Main {
  val a:Any = new A("hello") // 变量a指定为Any类型
  val b = new A("world")     // 类型推导
  val c:Any = b               // 把b赋给Any类型的c
}

$ scalac -Xprint:jvm A.scala
...
Main.this.a = new A("hello");        // 引用类型
Main.this.b = "world";               // unboxing
Main.this.c = new A(Main.this.b()); // boxing
...

2) 赋给数组

val arr = new Array[A](1)
val a = new A("aaa")
arr(0) = a

// 上面的语句翻译后,实际调用的是update方法:
arr.update(0, new A(Main.this.a())) // boxing

3) 运行时的类型测试

b.isInstanceOf[A]

//或进行类型模式匹配,都会引起装箱
b match { case x:A => println("ok") }

值类型的一些细节》上有2条评论

  1. pythonee

    或者说,值类型避免装箱的真正原因是编译期的替换呢

    回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注