惰性求值/延时求值并非什么新技术,大概三四十年前就有了,SICP还专门有一节来讲述如何给语言解释器添加惰性求值的功能,不过这门技术在工程领域并没有大范围使用,大部分流行语言,例如C、C++、Java、JavaScript等,仍然使用严格求值。相比于严格求值,惰性求值在变量被访问的时候才进行求值,这可以避免一部分不会被访问的变量的计算。另外,可以将计算延迟到合适的时候,也给代码的组织和表达提供了更多的空间。
近几年,有些语言开始为惰性求值提供支持,例如Kotlin,可以编写如下代码来实现p
在第一次访问时才求值,而非定义的时候,也不会在访问的时候每次都求值:
val p: String by lazy {
// 计算字符串的值
"str value"
}
Java语言并不支持惰性求值,但是可以利用一些函数式的特性来实现惰性求值的效果。本文就用简短的代码通过一个Lazy
类来实现惰性求值的效果。
1. 示例
首先来看一个简单例子
public void testLazy(){
Supplier<Integer> value = Lazy.from(() -> {
System.out.println("eval value");
return 1;
});
System.out.println("start...");
System.out.println("value:" + value.get());
System.out.println("value:" + value.get());
}
打印结果如下,可以看到代表求值的eval value
信息只有在第一次访问的时候才打印:
start...
eval value
value:1
value:1
在来看另一种场景,在Android项目中,由于视图组件只有在onCreate
后才能通过findViewById
取到,因此有些字段不能表示为final
,例如:
public class TestActivity extends Activity {
private TextView tvName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
tvName = findViewById(R.id.tv_name)
tvName.get().setText("test");
}
}
这样可能导致其他人对这些字段重新赋值。我们也可以这样写来使这个字段仍然可以被表示为final
:
public class TestActivity extends Activity {
private final Supplier<TextView> tvName = Lazy.from(() -> findViewById(R.id.tv_name));
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
tvName.get().setText("test");
}
}
2. 使用说明
这个类的使用非常简单,一共两个方法:
from
静态方法:这个方法接受一个Supplier
并返回新的只求值一次的Supplier
对象,用于构建惰性求值的对象。get
方法:这个方法实现了Supplier
的get
方法,不过只进行一次求值,用于获取变量的值。
3. 实现
完整代码如下
import java.util.function.Supplier;
public class Lazy<T> implements Supplier<T> {
private final Supplier<T> supplier;
private T value;
private boolean isEvaluated = false;
public static <T> Supplier<T> from(Supplier<T> supplier) {
return new Lazy<>(supplier);
}
private Lazy(Supplier<T> supplier) {
this.supplier = supplier;
}
@Override
public T get() {
if (!isEvaluated) {
value = supplier.get();
isEvaluated = true;
}
return value;
}
}
4. 注意
这个类中的get
方法并非线程安全,在多线程场景下,如果多个线程一起调用get
方法可能导致重复求值。如果需要在多线程场景下使用,建议用synchronized
修饰get
方法,或使用其他同步手段。如果提供的Supplier
参数可以看作纯函数且不耗时,则不需要考虑多线程问题。