java lambda::_Java中的Lambda:深入分析

java lambda::

通过不收购Sun Microsystems的方式,Oracle可以从事严肃的工作来振兴许多人认为停滞的语言。 对许多人的要求很高的是能够传递独立于类的函数的能力,因此这些函数可用作其他函数或方法调用的参数。 诸如HaskellF#之类的功能语言纯粹是使用这种范式存在的,而功能编程可以追溯到Lambda演算八十年了,导致Lambda一词在许多语言中都用于描述匿名函数。

尽管诸如PythonRuby之类的语言已经有很长时间了,但是基于JVM的语言(特别是Groovy和Scala)最近的流行也重新点燃了Java对lambda的需求。 因此,当宣布提议将其作为OpenJDK项目时 ,即使他们对lambda和闭包之间的混淆也感到非常兴奋。 还有一个InfoQ新闻帖子可供有兴趣了解有关此讨论的更多知识的人员(包括有关Java中lambda语法的辩论)。

Lambda和闭包

匿名函数的正确名称始终是lambda或希腊字母λ。 这个想法是一个函数纯粹是其参数的结果。 在lambda演算中,增量函数类似于λx.x+1 ,它对应于类似于def inc(x): return x+1的python函数def inc(x): return x+1尽管该绑定到'inc'。 (您可以使用lambda x:x+1在Python中执行纯lambda,并将结果分配给变量或存储在另一个数据结构中。)

还可以编写不完整的函数,这些函数依赖于外部状态,而不是完全由其参数定义。 例如, λx. x+y λx. x+y是一个开放函数; 在知道y之前无法评估。 捕获局部(词汇)状态(例如λy. λx. x+y λy. λx. x+y λy. λx. x+y使函数闭合 ; 现在捕获所有变量,因此被称为“封闭”术语。 (闭包这个名字指的是关闭一个开放术语的行为。)因此,尽管所有匿名函数都是lambda,但它们并不总是闭包。

实际上,自Java 1.1以来,Java就有了闭包(以内部类的形式)。 考虑以下代码片段:

public interface IFilter {
  public boolean filter(int x);
}
public class FilterFactory {
  public static IFilter greaterThan(final int i) {
    return new IFilter() {
      public boolean filter(int x) {
        // i is captured from outside lexical scope
        return x > i;
      }
    };
  }
}

在上面的代码示例中, FilterFactory具有工厂方法greaterThan ,在调用时,将返回的封闭过的论点。 用不同的参数调用同一段代码,您将得到不同的闭包,每个闭包都捕获自己的值。 如果我们有一个lambda表达式λi. λx. x > i也是如此λi. λx. x > i λi. λx. x > i λi. λx. x > i作为封闭的lambda(将i作为其参数)将返回一个函数,该函数在求值时会将其参数与封闭时的i值进行比较。 用3和4调用两次,得到不同的闭包,即λx. x > 3 λx. x > 3λx. x > 4 λx. x > 4 。 因此,将匿名函数称为闭包在语义上是错误的。 并非所有的lambda都是闭包,也不是所有的闭包都是lambda。 但是所有匿名函数都是lambda。

兰达·稻草人

最初的建议发布于12月 (基于前往Devoxx时在飞机上的想法), 1月的正式建议为0 .12月的后续建议为0.1.5。 从那时起,邮件列表一直保持安静,直到5月份最近的一连串活动引发了另一场讨论以及一份新的提案翻译文档 。 工作似乎正在幕后进行; 虽然邮件列表主要是外部利益相关方-所有的工作是由Oracle完成闭门造车 。 尽管JCP经历了语言变化,但尚无关于JCP本身要进行哪些更改(如果有)的消息,并且“ 和谐”问题仍未解决。

更大的担忧是时间表之一。 早在4月,尼尔·戈夫特(Neil Gafter)表示, Lambda项目延迟将与当时的JDK时间线不一致。 (其他人已经观察到JDK7总是会下滑 。)我们现在可以确定的是Lambda仍在开发中,并且JDK7尚未达到候选发布状态。 一个是否还会影响另一个仍有待观察。

最近,OpenJDK提供了一个初始实现 ,实现了所提议实现的一部分。 请注意,本文中的语法和说明均基于当前的草帽建议。 当最终规格发布时,文章将进行更新以反映这一点。

Java中的Lambda

Lambda与JSR292中称为“ 方法句柄”的新功能相关,该功能允许在语法上直接引用方法(而不必经历反射间接层)。 除了其他方面,VM级别的方法句柄概念将允许在方法内联方面进行更大的优化,这是JIT执行的最大胜利。

在Java中表示lambda的建议语法是使用#字符,后跟参数,然后是表达式或块。 自最初的提议以来,这没有改变,因此很可能会一直延续到最终的规范,尽管当然可能会改变。 (这也遵循了项目代币决定使用#表示外来方法名称,如先前所述 。)例如,上面的增量lambda可以表示为:

inc = #(int x) (x+1); // single expression
inc2 = #(int x) { return x+1; }; // block

Lambda还将具有从周围的局部范围捕获变量的能力,因此可以通过执行以下操作来编写等效的加法:

int y = ...;
inc = #(int x) (x+y);

可以将其概括为从函数捕获变量以生成函数工厂,例如:

public #int(int) incrementFactory(int y) {
  return #(int x) (x+y);
}

#int(int)类型是lambda本身的类型,在这种情况下,意味着“采用int并返回int”。

要调用lambda,理想情况下,我们想执行以下操作:

inc(3);

不幸的是,这并不完全有效。 问题在于Java具有用于字段和方法的不同名称空间(与C ++不同,后者共享一个公共名称空间)。 因此,在C ++中,您不可能有一个名为foo的字段和一个名为foo()的方法,而在Java中,这是完全可能的:

public class FooMeister {
  private int foo;
  public int foo() {
    return foo;
  }
}

这与lambdas有什么关系? 好吧,当您将lambda分配给类中的实例字段时,就会出现问题。

public class Incrementer {
  private static #int(int) inc = #(int x) (x+1);
  private static int inc(int x) {
    return x+2;
  }
}

如果调用Incrementer.inc(3) ,我们将获得什么价值? 如果绑定到lambda,则得到4,而如果绑定到方法,则得到5。结果,lambdas无法对方法调用使用相同的语法,因为在某些情况下会导致歧义。

当前的建议是使用句点和括号来调用lambda。 因此,为了从上方调用lambda,我们将改为执行Incrementer.inc.(3) 。 请注意上一个示例的额外时间; 这就是使lambda调用与常规方法调用不同的原因。 实际上,您甚至不需要将其分配给变量。 您可以执行#(int x) (x+1).(3)

Lambda和数组

涵盖了lambda的基础知识之后,您需要注意一些陷阱。 考虑下面的代码示例

#String(String)[] arrayOfFunction = new #String(String)[10];
Object[] array = arrayOfFunction;
array[0] = #void() {throw new Exception()};

这里的问题是,如果第一个赋值是允许的,那么第二个也是允许的(因为任何引用类型都可以转换为Object ),然后这允许我们为数组分配不同签名的函数类型。 结果,编译为调用arrayOfFunction[0].("Foo")将失败,因为数组元素0处的lambda无法接受String作为参数。

不幸的是,这对于特定的lambda实现不是问题。 相反,这是Java类型系统中的一个弱点,它允许将任何内容都强制转换为Object (以及扩展为Object的数组)。 但是,尽管泛型将在运行时导致ClassCastException ,但这实际上会泄漏具有不兼容签名的函数,这些签名未表示为数组类型的一部分。

结果,Java中将不允许使用lambda数组。 尽管将允许使用通用List

共享的可变状态并有效地最终

Java对于内部类的约定是要求内部类使用final关键字捕获所有变量。 尽管这样做可以防止在捕获后对变量进行后续修改(从而大大简化了内部类的实现),但它迫使开发人员在将其包装为内部类时在代码中键入其他字符,或者让IDE自动执行此操作。

为了简化预期将lambda放入的用例,已创建了有效final的新概念。 这允许捕获非最终值,前提是编译器可以通过方法的主体来验证它们是否实际上未更改。 实际上,这促进了IDE在编译器阶段可以做的事情。

但是,如果捕获的变量是可修改的,那么这可能会在捕获lambda时带来大量额外开销。 这样,如果变量被lambda捕获并被其修改(或之后在lambda捕获范围内),则必须使用shared关键字显式标记该变量。 实际上,这默认使lambdas捕获的局部变量为final,但是需要一个附加关键字来指示可变性。

public int total(List list) {
  shared int total = 0;
  list.forEach(#void(Integer i) {
    total += i;
  });
  return total;
}

可变状态的问题在于,它需要与纯功能方法不同的实现方式。 例如,如果lambda在不同的线程上执行或退出该方法,则可能有必要在单独的堆分配对象中表示绑定。 例如,这具有相同的效果(用于内部类):

public int total(List list) {
  final int total = new int[1];
  list.forEach(#void(Integer i) {
    total[0] += i;
  });
  return total;
}

需要注意的关键是,尽管前者的意图可能更清晰,但两者都将导致构造一个堆对象,而该堆对象可能会超出方法调用的寿命。 (无法保证传递给forEach的lambda不会存储在内部变量中,例如,该变量会泄漏一个元素的整数数组。)

共享的可变状态可能成为Java Lambda中的反模式; 特别是,这些求和过程通常比采用强制性求和更好,可以采用某种倍数或归约法。

特别是,使用共享可变状态的Lambda不太可能在多线程环境中正常工作,除非使用同步访问器来保护对累加器的访问。

扩展Collections类

尽管Java语言中的lambda的可用性与其在Collections类中的采用无关,但是显然希望能够对collection类执行功能性操作。 (以上示例假定List上存在尚未存在的forEach方法。)

但是,许多客户端可能已经实现了List接口,因此向该接口添加新方法将破坏与前几代的向后兼容性。 为了解决这个问题,已经提出了针对防御者方法的单独建议。 如果接口不存在,它将为接口定义一个“默认”方法,该方法绑定到帮助程序类的静态方法。 例如,我们可能有:

public interface List {
  ...
  extension public void forEach(#void(Object))
    default Collections.forEach;
}
public class Collections {
  public void forEach(Collection c, #void(object) f) {
    for(i:c)
      f.(i);
  }
}

结果,任何调用List.forEach(#)调用方都将在Collections.forEach处调用实现,该实现将具有附加参数以采用原始目标。 但是,实现者可以自由地实现此默认设置,并获得更优化的性能:

public class ArrayList implements List {
  private Object elements[];
  public void forEach(#void(Object o) f) {
    for(int i=0;i<elements.length;i++)
      f.(elements[i]);
  }
}

但是,有一些担忧。 例如,为了适应与lambda类相同的时间范围,对Collections类的改装很匆忙。 可以扩展Java语言以支持lambda,而无需在java.util集合类上支持lambda操作,然后将其作为后续JDK的一部分使用。

SAM,再玩一次

当前功能性Java实现的常见模式是通过单一访问方法或SAM广泛使用接口。 许多过滤器操作将具有谓词函数之类的东西,它们当前表示为实现给定接口的子类。 例如,可以使用以下方法过滤一个Integers列表:

Iterables.filter(list,new Predicate() {
  public boolean apply(Integer i) {
    return(i > 5 );
  }
});

但是,如果允许在接口上使用SAM方法,则可以简化实现:

Iterables.filter(list,#(Integer i) (i > 5));

另外,这将具有更高的性能,因为每次评估此代码时都不必创建Predicate的新实例; 并且,如果JSR292的改进导致扩展了动态方法以支持asSam()调用,那么JVM可以理解接口到方法的实例,以优化所有与对象相关的调用。 但是,这需要lambda组范围之外的支持,并且可能无法在给定的时间范围内提供支持。

this收益和非本地收益的含义

关于在lambda中this的处理还有另一个未解决的问题。 它是指lambda本身还是封闭对象实例? 由于lambda是引用类型,因此能够在内部获取它将允许实现递归函数。 例如,一个人可能有:

fib = #int(int i) { 
  if(i == 0 || i == 1 ) 
    return 1 
  else
    return this.(i-1) + this.(i-1);
}

如果没有this ,就必须提取对其分配变量的引用(即RHS取决于分配的LHS)。 相反,这使得引用封闭对象变得更加困难。 必须在类名之前加上类名,就像内部类一样:

public class EnclosingScope {
  #Object() { return EnclosingScope.this; } 
}

在初始版本的lambda内可能不允许this做,尽管在任何情况下都可以在this前缀之前加上封闭范围。

另一个考虑因素是控制流运算符的含义,例如returnbreak 。 某些语言(例如Self)允许在块中进行非本地返回 ,其中块本身可以触发封闭方法的返回。 同样,控制流(带有breakcontinue等的循环)可能会触发立即lambda之外的事件。

此刻, return的语义似乎与封闭的lambda的返回紧密相关,而不是与任何外部构造紧密相关。 但是,异常按预期工作; 从lambda内部引发的异常将传播到封闭方法,并从那里传播到方法调用链。 这类似于BGGA提案中处理异常的方式。

结论

尽管不在最初提议的JDK 7发布计划的时间范围内,但是lambda草案正在取得进展。 加上最近添加的防御方法,不太可能在今年夏天准备完整的实现。 JDK 7是否将不带lambda(或没有改装的Collections API)一起发货,还是JDK 7是否会延迟以容纳lambda扩展而待观察。

翻译自: https://www.infoq.com/articles/lambdas-java-analysis/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

java lambda::

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值