Java是否具有惰性求值?

35
我知道在这种情况下Java有智能/惰性求值:
public boolean isTrue() {
    boolean a = false;
    boolean b = true;
    return b || (a && b); // (a && b) is not evaluated since b is true
}

但是关于:

public boolean isTrue() {
    boolean a = isATrue();
    boolean b = isBTrue();
    return b || a;
}

isATrue()isBTrue()返回true时是否被调用?


关于你的例子中 && 部分的一个侧记: 如果 a 为 false,则 return a && b 可能会忽略 b,但是 return a & b 也将执行关系的第二部分。 - Simeon
9个回答

36

关于语言的问题 - 是的,这两个函数都被调用了。

如果你将函数重写为以下形式:

public boolean isTrue() {
    return isBTrue() || isATrue();
}

如果第一个函数为真,则不会调用第二个函数。

但这是短路求值,而不是惰性求值。惰性求值的情况看起来会像这样:

public interface LazyBoolean {
    boolean eval();
}

class CostlyComparison implements LazyBoolean {
  private int a, b;

  public CostlyComparison(int a, int b) { 
    this.a=a; 
    this.b=b; 
  }

  @Override 
  public boolean eval() {
    //lots of probably not-always-necessary computation here
    return a > b;
  }
} 

public LazyBoolean isATrue() {
  return new CostlyComparison(10,30);  //just an example
}

public boolean isTrue() {        // so now we only pay for creation of 2 objects
    LazyBoolean a = isATrue();   // but the computation is not performed; 
    LazyBoolean b = isBTrue();   // instead, it's encapsulated in a LazyBoolean
    return b.eval() || a.eval(); // and will be evaluated on demand;
                                 // this is the definition of lazy eval.
}

7
"但这是短路评估,而不是惰性评估。" 实际上,它是:"短路评估只是(非常有限的)惰性评估的一种形式"。 - m0skit0

26
在Java(和其他类C语言)中,这被称为短路求值* 是的,在第二个例子中,isATrue总是被调用。也就是说,除非编译器/JVM能够确定它没有可观察的副作用,否则它可能会选择优化,但在这种情况下,您也不会注意到差异。
*两者有很大不同;前者本质上是一种优化技术,而后者是由语言规定的,可能会影响可观察到的程序行为。
我最初认为这与惰性求值完全不同,但正如@Ingo在下面的评论中指出的那样,这是一个可疑的断言。人们可以将Java中的短路运算符视为惰性求值的非常有限的应用。
然而,当函数式语言要求惰性求值语义时,通常是出于完全不同的原因,即防止无限(或至少是过度)递归。

1
@CaptainFord:没错,在我的回答中我已经提供了这两个页面的维基百科链接 ;) - Oliver Charlesworth
1
@m0skit0:因为它在这种情况下没有优化,你是如何确定它已被调用的? - Oliver Charlesworth
2
我没有给你点踩,但你应该得到它。因为短路求值只是懒惰求值的一个非常有限的情况。 - Ingo
1
从您提供的链接中:在默认使用惰性求值的语言中(例如Haskell),所有函数实际上都是“短路”的,而特殊的短路运算符是不必要的。 如果惰性求值不能代替短路求值,那么这怎么可能实现? - Ingo
1
@Ingo:是的,你说得对。当我写上面的答案时,我在提到优化方面的惰性求值(在这个语境中,我认为它与短路是不同的)。但确实,在某些语言中(尤其是函数式语言),它基本上是必须的,并且会影响程序语义。我会尽力重新措辞我的答案以恢复秩序! - Oliver Charlesworth
显示剩余5条评论

17

不,Java只对用户定义的方法使用急切求值。正如您所指出的那样,Java的一些语言结构实现非严格评估。其他包括if?:while

我曾经学过[1],“有惰性评估”意味着“按需调用”评估。Java根本没有像这样的东西。然而,有一个普遍趋势(我个人不鼓励),将惰性评估的定义放宽到包括“按名称调用”的评估。在按需调用和按名称调用下无法区分诸如&&之类的函数;它们是相同的,这使问题变得模糊。

考虑到这种放宽,一些进一步的反驳声称Java通过以下方式具有惰性评估:

interface Thunk<A> {
  A value();
}

那么,你可以这样编写一个用户定义的 &&

boolean and(boolean p, Thunk<Boolean> q) {
  return p && q();
}

这时候有人声称Java具有惰性求值,但即使在宽泛的意义上也不是。Java类型系统的一个区别在于没有将boolean/BooleanThunk<Boolean>这两种类型统一起来。试图将它们中的一个用作另一个将导致类型错误。在没有静态类型系统的情况下,代码仍然会失败。正是这种缺乏统一性(无论是静态类型还是非静态类型),回答了这个问题;不,Java没有用户定义的惰性求值。当然,可以像上面那样模拟它,但这是一个无聊的观察,它遵循了图灵等效性原理。

Scala这样的语言具有按名称调用的求值方式,允许用户定义等价于常规&&函数的and函数(考虑到终止。参见[1])。

// note the => annotation on the second argument
def and(p: Boolean, q: => Boolean) =
  p && q

这使得:

def z: Boolean = z
val r: Boolean = and(false, z)

请注意,这个短的程序片段通过提供一个值来终止。它还将Boolean类型的值统一为按名称调用。因此,如果您认同懒惰求值的松散定义(我不鼓励这样做),您可能会说Scala具有懒惰求值。在这里提供Scala作为很好的对比。我建议看看Haskell以获得真正的懒惰求值(按需调用)。

希望这可以帮到你!

[1] http://blog.tmorris.net/posts/a-fling-with-lazy-evaluation/


13

SE8 (JDK1.8)引入了Lambda表达式,可以使惰性求值更加透明。考虑以下代码主方法中的语句:

@FunctionalInterface
public interface Lazy<T> {
   T value();
}

class Test {
   private String veryLongMethod() {
      //Very long computation
      return "";
   }

   public static <T> T coalesce(T primary, Lazy<T> secondary) {
      return primary != null? primary : secondary.value();
   }

   public static void main(String[] argv) {
      String result = coalesce(argv[0], ()->veryLongMethod());
   }
}

被调用的函数coalesce返回第一个非空值(如SQL中所示)。其调用中的第二个参数是Lambda表达式。只有在argv [0] == null时才会调用veryLongMethod()方法。这种情况下唯一的有效负载是在要惰性按需评估的值之前插入() ->


1
@pagoda_5b 关于“这并没有回答问题”,实际上它已经回答了。JDK8的lambda表达式可以用于惰性函数求值,除此之外还提供了早期JDK版本中提供的任何机制。虽然答案不完整,但是它是正确的,所以说它并没有没有回答问题。 - aem999
我删除了原始评论。你可能是对的。 - pagoda_5b
2
我会使用现有的java.util.function.Supplier作为函数接口,但是也许创建一个名为Lazy的函数接口更清楚地显示出意图。 - Henno Vermeulen

3
为了简化操作,您可以像这样使用Java 8中的Supplier接口:
Supplier<SomeVal> someValSupplier = () -> getSomeValLazily();

然后在代码中,您可以使用以下方式:
if (iAmLazy) 
  someVal = someValSupplier.get(); // lazy getting the value
else 
  someVal = getSomeVal(); // non lazy getting the value

谢谢,但我不是在问如何实现懒惰,而是Java是否默认为懒惰。 - m0skit0
谢谢,这将使一些if语句更易读,您认为会有可衡量的性能损失吗? - wutzebaer

3
只是想在这个问题线程中补充一点,以下是来自Oracle JVM文档的内容:
一个Java虚拟机实现可以选择在使用时逐个解析类或接口中的每个符号引用("懒"或"延迟"解析),或者在验证类时一次性解析所有符号引用("急切"或"静态"解析)。这意味着,在某些实现中,解析过程可能会在类或接口被初始化后继续进行。 reference 作为具有延迟实现的类的示例,可以看到Stream,这是来自Oracle Stream文档的内容:
流是延迟执行的;只有在启动终端操作时才对源数据进行计算,并且只在需要时消耗源元素。 reference 话虽如此,如果你按照以下方式操作,将不会显示任何内容。除非你添加触发器。
Stream.of(1, 2, 3, 4, 5).filter(number -> {
   System.out.println("This is not going to be logged");
   return true;
});

0
如果isBTrue()返回true,isATrue()会被调用吗?
是的,两个方法都会被调用。

0

不,它并不会。无论isATrue()的结果如何,isBTrue()都将被调用。您可以编写这样一个程序,每个方法中都有打印语句,以验证此内容。


2
但是如果我放弃一个打印语句,那么编译器/JVM将感到被迫调用它们,并且不会进行优化 :) - m0skit0
1
Java语言的语义要求它们被调用。如果编译器可以确定地安全地消除调用而不影响程序语义(即更改行为),则可以这样做。但我认为JVM实际上并没有这样做。 - Jeremy Roman

0

是的,isATrue()会被调用,因为你在boolean a = isATrue();这一行中显式地调用了它。

但如果isBTrue()返回true,则不会调用它:

public boolean isTrue() {
    return isBTrue() || isATrue();
}

在真正的惰性语言中(通常是函数式语言,例如Haskell),不会调用isATrue(),因为变量a直到实际使用它之前都不会被计算(实际上从未)。 - m0skit0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接