Fork me on GitHub

scala笔记之惰性赋值(lazy)

一、lazy关键字简介

lazy是scala中用来实现惰性赋值的关键字,被lazy修饰的变量初始化的时机是在第一次使用此变量的时候才会赋值,并且仅在第一次调用时计算值,即值只会被计算一次,赋值一次,再之后不会被更改了,这个特性有点熟悉哎?没错,所以lazy修饰的变量必须同时是val修饰的不可变变量。

 

下面是一个惰性赋值的例子:

package cc11001100.scala.lazyStudy

class FooBar() {

  println("foo before")
  lazy val foo: String = initFoo()
  println("foo after")

  println("bar before")
  val bar: String = initBar()
  println("bar after")

  def initFoo(): String = {
    println("initFoo exec")
    "foo"
  }

  def initBar(): String = {
    println("initBar exec")
    "bar"
  }

}

object LazyStudy {
  
  def main(args: Array[String]): Unit = {

    val foobar = new FooBar()

    println(foobar.foo)
    println(foobar.foo)

    println(foobar.bar)
    println(foobar.bar)

  }

}

输出:

foo before
foo after
bar before
initBar exec
bar after
initFoo exec
foo
foo
bar
bar

需要注意的是lazy修饰的变量后面只需要是个表达式就可以,一般是调用个方法计算值,也可以是字面值常量,但字面值常量的话又有什么意义呢?

 

二、原理探究

scala也是编译成字节码跑在jvm上的,而jvm的字节码指令并没有提供对lazy这种语义的支持,所以由此可以推断,lazy只是一个语法糖,scala编译器在编译时期对其做一些包装转换,但究竟是如何转换的呢,可以写一段代码编译然后反编译看一下。

编写一段scala代码,有两个变量,一个使用lazy修饰,一个不使用lazy修饰:

package cc11001100.scala.lazyStudy

class LazyInitDemoForDecompilation {
  lazy val foo = "foo"
  val bar = "bar"
}

object LazyInitDemoForDecompilation {

  def main(args: Array[String]): Unit = {
    val o = new LazyInitDemoForDecompilation()
    println(o.foo)
    println(o.bar)
  }

}

然后编译为字节码文件,再使用jd-gui等工具将其反编译:

package cc11001100.scala.lazyStudy;

import scala.reflect.ScalaSignature;

@ScalaSignature(bytes="\006\001}2A!\003\006\001#!)q\003\001C\0011!A1\004\001EC\002\023\005A\004C\004&\001\t\007I\021\001\017\t\r\031\002\001\025!\003\036\017\0259#\002#\001)\r\025I!\002#\001*\021\0259b\001\"\001+\021\025Yc\001\"\001-\005qa\025M_=J]&$H)Z7p\r>\024H)Z2p[BLG.\031;j_:T!a\003\007\002\0231\f'0_*uk\022L(BA\007\017\003\025\0318-\0317b\025\005y\021AC2dcE\002\004'M\0311a\r\0011C\001\001\023!\t\031R#D\001\025\025\005i\021B\001\f\025\005\031\te.\037*fM\0061A(\0338jiz\"\022!\007\t\0035\001i\021AC\001\004M>|W#A\017\021\005y\031S\"A\020\013\005\001\n\023\001\0027b]\036T\021AI\001\005U\0064\030-\003\002%?\t11\013\036:j]\036\f1AY1s\003\021\021\027M\035\021\00291\013'0_%oSR$U-\\8G_J$UmY8na&d\027\r^5p]B\021!DB\n\003\rI!\022\001K\001\005[\006Lg\016\006\002.aA\0211CL\005\003_Q\021A!\0268ji\")\021\007\003a\001e\005!\021M]4t!\r\0312'N\005\003iQ\021Q!\021:sCf\004\"AN\037\017\005]Z\004C\001\035\025\033\005I$B\001\036\021\003\031a$o\\8u}%\021A\bF\001\007!J,G-\0324\n\005\021r$B\001\037\025\001")
public class LazyInitDemoForDecompilation
{
  private String foo;
  
  private String foo$lzycompute()
  {
    // 因为在调用此方法之前已经判断过一次标志位的值了,
    // 所以可以看做是一种被拆散了的DCL
    synchronized (this)
    {
      if (!this.bitmap$0)
      {
        this.foo = "foo";
        this.bitmap$0 = true;
      }
    }
    return this.foo;
  }
  
  public String foo()
  {
    // 每次获取foo的值的时候,先判断是否已经初始化过了,
    // 如果还没有初始化就将其初始化,否则直接将已经计算出的值返回
    return !this.bitmap$0 ? foo$lzycompute() : this.foo;
  }
  
  public String bar()
  {
    return this.bar;
  }
  
  // bar变量直接为其赋值的
  private final String bar = "bar";
  // 这个变量是一个标志位,用来记录foo变量是否已经被初始化过了
  private volatile boolean bitmap$0;
  
  public static void main(String[] paramArrayOfString)
  {
    LazyInitDemoForDecompilation..MODULE$.main(paramArrayOfString);
  }
}

在object中执行:

package cc11001100.scala.lazyStudy;

import scala.Predef.;

public final class LazyInitDemoForDecompilation$
{
  public static  MODULE$;
  
  static
  {
    new ();
  }
  
  public void main(String[] args)
  {
    LazyInitDemoForDecompilation o = new LazyInitDemoForDecompilation();
    // 会将对变量的访问替换成调用访问器,
    // 这样的话编译器就可以很鸡贼的在访问器方法中插入各种处理以提供N多的语法糖,挺机智的
    Predef..MODULE$.println(o.foo());
    Predef..MODULE$.println(o.bar());
  }
  
  private LazyInitDemoForDecompilation$()
  {
    MODULE$ = this;
  }
}

综上源码,得出结论,scala的lazy关键字就是编译器在编译期将变量的初始化过程替换为Double Check Lock,类似于Java中的懒汉式单例模式初始化。

 

.

posted @ 2019-01-09 12:35  CC11001100  阅读(1502)  评论(0编辑  收藏  举报