last modified:2020/10/31

1-06-3-Lambda表达式

6.3.1 为什么引入lambda表达式

  • lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。

    • 将一个代码块传递到某个对象,这个代码块会在将来某个时间调用。

6.3.2 lambda表达式的语法

  • 带参数变量的表达式被称为lambda表达式。

  • 你已经见过Java中的一种lambda表达式形式:参数,箭头(->)以及一个表达式。

    • 如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在中,并包含显式的return语句。例如:
    (String first,String second)->
    {
    if (first.1ength() < second.length()) return -1;
    else if (first.length() > second.length()) return 1;
    else return 0;
    }
    • 即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样:
    () -> { for (int i = 100; i >= 0; i--) System.out.prinln(i); }
    • 如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。例如:
    Comparator<String> comp
    (first,second) // Same as (String first,String second)
    -> first.length() - second.length();
    • 在这里,编译器可以推导出first和second必然是字符串,因为这个lambda表达式将赋给一个字符串比较器。(下一节会更详细地分析这个赋值。)
    • 如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:
    ActionListener listener = event ->
    System.out.println("The time is " + new Date()");
    // Instead of (event) -> .. . or (ActionEvent event) -> ..·
    • 无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。例如,下面的表达式
    (String first, String second)-> first.length() - second.length();
    • 可以在需要int类型结果的上下文中使用。
    • 如果一个lambda表达式只在某些分支返回一个值,而在另外一些分支不返回值,这是不合法的。例如,(int x)->{ if(x>= 0) return1; }就不合法。
//程序显示了如何在一个比较器和一个动作监听器中使用lambda表达式
public class LambdaTest
{
public static void main(String[]args)
{
String[] planets = new String[]{ "Mercury","Venus","Earth”,
"Mars","Jupiter","Saturn","Uranus","Neptune"};
System.out.println(Arrays.toString(planets));
system.out.println("Sorted in dictionary order:");
Arrays.sort(planets);
System.out.println(Arrays.toString(planets));
system.out.println("Sorted by length:");
Arrays.sort(planets,(first,second)->first.length()-second.length());
System.out.print1n(Arrays.toString(planets));
Timer t = new Timer(1000, event->
System.out.println("The time is" +new Date());
t.start();
// keep program running until user selects "ok"
optionPane.showMessageDialog(null,"Quit program?");
System.exit(O);
}
}

6.3.3 函数式接口

  • Java中已经又很多封装代码块的接口,lambda表达式与这些接口是兼容的。

  • 对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口(functional interface)

    注释:你可能想知道为什么函数式接口必须有一个抽象方法。不是接口中的所有方法都是抽象的吗?实际上,接口完全有可能重新声明Object类的方法,如toString 或clone,这些声明有可能会让方法不再是抽象的。(Java API中的一些接口会重新声明Object方法来附加javadoc注释。Comparator API就是这样一个例子。)更重要的是,正如6.1.5节所述,在Java SE 8中,接口可以声明非抽象方法

  • 为了展示如何转换为函数式接口,下面考虑Arrays.sort方法。

    • 它的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:

      Arrays.sort(words,
      (first, second)->first.length()- second.length();

      在底层,Arrays.sort方法会接收实现了Comparator<String>的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口。

  • 实际上,在java中,对lambda表达式所能做的也只是能转换为函数式接口。

  • Java API在java.util.function包中定义了很多非常通用的函数式接口

    • 其中一个接口BiFunction<T,U,R>描述了参数类型为T和U而且返回类型为R的函数。可以把我们的字符串比较lambda表达式保存在这个类型的变量中:

      BiFunction<String,String,Integer> comp
      = (first,second)-> first.length() - second.length();

      不过,这对于排序并没有帮助。没有哪个Arrays.sort方法想要接收一个BiFunction。如果你之前用过某种函数式程序设计语言,可能会发现这很奇怪。不过,对于Java程序员而言,这非常自然。类似Comparator的接口往往有一个特定的用途,而不只是提供一个有指定参数和返回类型的方法。Java SE8沿袭了这种思路。想要用lambda表达式做某些处理,还是要谨记表达式的用途,为它建立一个特定的函数式接口。

    • java.util.function包中有一个尤其有用的接口Predicate:

      public interface Predicate<T>
      {
      boolean test(T t);
      //Additional default and static methods
      }

      ArrayList类有一个removelf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式。例如,下面的语句将从一个数组列表删除所有null值:

      list.removeIf(e -> e == null);

6.3.4 方法引用

  • 有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。

    • 例如,假设你希望只要出现一个定时器事件就打印这个事件对象。当然,为此也可以调用:

      Timer t = new Timer(1000,event -> System.out.println(event));

      但是,如果直接把printIn方法传递到Timer构造器就更好了。具体做法如下:

      Timer t = new Timer(1000,System.out::println);

      表达式System.out::printIn是一个方法引用(method reference),它等价于lambda表达式

      x->System.out.println(x)
    • 再来看一个例子,假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式:

      Arrays.sort(strings,String::compareToIgnoreCase)

      从这些例子可以看出,要用::操作符分隔方法名与对象或类名。主要有3种情况:

      • object::instanceMethod
      • Class::staticMethod
      • Class::instanceMethod

      在前2种情况中,方法引用等价于提供方法参数的lambda表达式。前面已经提到,

      System.out::println等价于x->System.out.printIn(x)。类似地,Math:pow等价于(x,y)->Math.pow(x,y)。

      对于第3种情况,第1个参数会成为方法的目标。例如String::compareTolgnoreCase等同于(x,y)-> x.compareTolgnoreCase(y)。

  • 如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的那一个方法

    • 例如,Math.max方法有两个版本,一个用于整数,另一个用于double值。选择哪一个版本取决于Math::max转换为哪个函数式接口的方法参数。类似于lambda表达式,方法引用不能独立存在,总是会转换为函数式接口的实例。
  • 可以在方法引用中使用this参数。

    • 例如,this::equals 等同于x -> this.equals(x)。使用super也是合法的。下面的方法表达式super::instanceMethod使用this 作为目标,会调用给定方法的超类版本。

6.3.5 构造器引用

  • 构造器引用与方法引用很类似,只不过方法名为new

    • 例如,Person::new 是Person构造器的一个引用。哪个构造器呢?这取决于上下文

    • 假设你有一个字符串列表。可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器,调用如下:

      Arraylist<String> names= ... ;
      // map方法会为各个列表元素调用Person(String)构造器
      Strean<Person> strean = names.stream().map(Person::new) ;
      List<Person> people = strean.collect(Collectors . tolist());

      如果有多个Person构造器,编译器会选择有一个String参数的构造器,因为它从上下文推导出这是在对一个字符串调用构造器。

    • 可以用数组类型建立构造器引用

      • 例如,int[]::new 是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x -> new int[x]。
    • Java有一个限制,无法构造泛型类型T的数组

      • 数组构造器引用对于克服这个限制很有用。表达式new T[n]会产生错误,因为这会改为new Object[n]。对于开发类库的人来说,这是一个问题。例如,假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:

        0bject[] people = stream.toArray();

        不过,这并不让人满意。用户希望得到一个Person引用数组,而不是Object引用数组。流库利用构造器引用解决了这个问题。可以把Person[]:new传入toArray方法:

        Person[] people = stream.toArray(Person[]::new);

        toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。

6.3.6 变量作用域

  • lambda 表达式有3个部分:

    • 1)一个代码块;
    • 2)参数;
    • 3)自由变量的值,这是指非参数而且不在代码中定义的变量。
  • public static void repeatMessage(String text, int delay)
    {
    Actionlistener listener = event ->
    {
    System.out.println(text);
    Toolkit.getDefaultToolkit().beep();
    };
    new Timer(delay,listener).start();
    }
    //来看这样一个调用:
    repeatMessage("Hel1o",1000); // Prints Hel1o every 1,000 milliseconds
  • 在我们的例子中,这个lambda表达式有1个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串"Hello"。我们说它被lambda表达式捕获

    ( captured)
    。(下面来看具体的实现细节。例如,可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。)

    注释:关于代码块以及自由变量值有一个术语: 闭包( closure)。 如果有人吹噓他们的语言有闭包,现在你也可以自信地说Java也有闭包。在Java中,lambda表达式就是闭包。

  • 可以看到,lambda表达式可以捕获外围作用域中变量的值

  • 在Java中,要确保所捕获的值是明确定义的,这里有一个重要的限制。

    • 在lambda表达式中,只能引用值不会改变的变量。例如,下面的做法是不合法的:

      public static void countDown(int start,int delay)
      {
      ActionListener listener = event ->{
      start--; // Error: Can't mutate captured variable
      System.out.println(start);
      };
      new Timer(delay,listener) .start();
      }
    • 另外如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。

      例如,下面就是不合法的:

      public static void repeat(String text,int count)
      {
      for (int i = 1; i <= count; i++)
      {
      ActionListener listener = event -> {
      System.out.println(i + "; " + text);
      // Error: Cannot refer to changing i
      };
      new Timer(1000 ,listener).start();
      }
      }

      这里有一条规则: lambda 表达式中捕获的变量必须实际上是最终变量( effectively final)。实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。在这里,text 总是指示同一个String 对象,所以捕获这个变量是合法的。不过, i的值会改变,因此不能捕获i。

  • lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。

    • 在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
    Path first = Paths.get("/usr/bin");
    Comparator<String> comp =
    (first, second) -> first.length() - second.length();
    // Error: Variable first already defined
    • 在方法中,不能有两个同名的局部变量,因此,lambda表达式中同样也不能有同名的局部变量。

    • 在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如,考虑下面的代码:

      public class Application()
      {
      public void init()
      {
      ActionListener listener = event ->
      {
      //表达式this.toString()会调用Application对象的 //toString方法,而不是ActionListener实例的方法。
      System.out.print1n(this. toString());
      ...
      }
      ...
      }
      }

      在lambda表达式中,this 的使用并没有任何特殊之处lambda 表达式的作用域嵌套在init方法中,与出现在这个方法中的其他位置一样,lambda表达式中this的含义并没有变化。

6.3.7 处理lambda表达式

  • 使用lambda表达式的重点是延迟执行( deferred execution)。

  • 之所以希望以后再执行代码,这有很多原因,如:

    • 在一个单独的线程中运行代码;
    • 多次运行代码;
    • 在算法的适当位置运行代码(例如,排序中的比较操作);
    • 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);
    • 只在必要时才运行代码。
  • 下面来看一个简单的例子。假设你想要重复一个动作n次。将这个动作和重复次数传递

    到一个repeat方法:

    repeat(10, () -> System.out.println("Hello, World!"));

    要接受这个lambda表达式,需要选择(偶尔可能需要提供)一个函数式接口

    表6-1列出了Java API中提供的最重要的函数式接口。

    在这里,我们可以使用Runnable接口:

    public static void repeat(int n, Runnable action)
    {
    for (int i =0; i < n; i++) action.run();
    }

    需要说明,调用action.run()时会执行这个lambda表达式的主体。

  • 现在让这个例子更复杂一些。我们希望告诉这个动作它出现在哪一次迭代中。 为此,需

    要选择一个合适的函数式接口,其中要包含一个方法,这个方法有一个int参数而且返回类

    型为void。处理int值的标准接口如下:

    public interface IntConsumer{
    void accept(int value);
    }

    下面给出repeat方法的改进版本:

    public static void repeat(int n, IntConsumer action){
    for (int i =0; i < n; i++) action.accept(i);
    }

    可以如下调用它:

    repeat(10, i -> Sysem.out.pritnln("Countdown: "+ (9 - i)));

    表6-2列出了基本类型int、long和double的34个可能的规范。最好使用这些特殊

    化规范来减少自动装箱。出于这个原因,上一个例子中使用了IntConsumer 而不是

    Consumer<Integer>。

    • 最好使用6-1或6-2中的接口
    • 大多数标准函数式接口都提供了非抽象方法来生成或合并函数。
      • 如,Predicate.isEqual(a)等同于a::equal,不过如果a为null也能正常工作。
    • 如果设计你自己的接口,其中只有一个抽象方法,可以用@FunctionalInterface注

      **解来标记这个接口。这样做有两个优点。

      • 如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息。
      • 另外javadoc页里会指出你的接口是一个函数式接口。
    • 并不是必须使用注解。根据定义,任何有一个抽象方法的接口都是函数式接口。不

      过使用@FunctionalInterface 注解确实是一个很好的做法。

6.3.8 再谈Comparator(!!!)

  • Comparator接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda表达式或方法引用。

  • 静态comparing方法取一个“键提取器”函数,它将类型T映射为一个可比较的类型

    (如String)。对要比较的对象应用这个函数,然后对返回的键完成比较。

    例如,假设有一个Person对象数组,可以如下按名字对这些对象排序:

    Arrays.sort(people,Comparator.comparing(Person:getName));

    与手动实现一个Comparator相比,这当然要容易得多。另外,代码也更为清晰,因为显然我们都希望按人名来进行比较。

    可以把比较器与thenComparing方法串起来。例如,

    Arrays.sort(people,Comparator.comparing(Person::getLastName)
    .thenComparing(Person::getFirstNane));

    如果两个人的姓相同,就会使用第二个比较器。

    这些方法有很多变体形式。可以为comparing和thenComparing方法提取的键指定一个比较器。例如,可以如下根据人名长度完成排序:

    Arrays.sort(people, Comparator.comparing(Person:getName,
    (s, t) -> Integer.compare(s.length(), t.length())));

    另外,comparing 和thenComparing方法都有变体形式,可以避免int、long或double值的装箱。要完成前一个操作,还有一种更容易的做法:

    Arrays. sort(people, Comparator . comparingInt(p -> p.getName(). length));

    如果键函数可以返回null,可能就要用到nullFirst和nullsLast适配器这些静态方

    法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或

    大于正常值。例如,假设一个人没有中名时getMiddleName会返回一个nul,就可以使用

    Comparator.comparing(Person:.getMiddleName(),        Comparator.nullsFirst(...))

    nullsFirst方法需要一个比较器, 在这里就是比较两个字符串的比较器。

    naturalOrder 方法可以为任何实现了Comparable的类建立一个比较器。在这里,Comparator.<String> naturalOrder()正是我们需要的。下面是一个完整的调用,可以按可能为null 的中名进行排序。这里使用了一个静态导入java.util.Comparator.*,以便理解这个表达式。注意naturalOrder的类型可以推导得出。

    Arrays.sort(people,comparing(Person::getMiddleName,
    nullsFirst(naturalOrder())));

    静态reverseOrder方法会提供自然顺序的逆序。要让比较器逆序比较,可以使用reversed实例方法。例如naturalOrder.(reversed)等同于reverseOrder()。

1-06-2 Lambda表达式的更多相关文章

  1. 委托、Lambda表达式、事件系列06,使用Action实现观察者模式,体验委托和事件的区别

    在"实现观察者模式(Observer Pattern)的2种方式"中,曾经通过接口的方式.委托与事件的方式实现过观察者模式.本篇体验使用Action实现此模式,并从中体验委托与事件 ...

  2. 背后的故事之 - 快乐的Lambda表达式(一)

    快乐的Lambda表达式(二) 自从Lambda随.NET Framework3.5出现在.NET开发者眼前以来,它已经给我们带来了太多的欣喜.它优雅,对开发者更友好,能提高开发效率,天啊!它还有可能 ...

  3. JDK8 的 Lambda 表达式原理

    JDK8 使用一行 Lambda 表达式可以代替先前用匿名类五六行代码所做的事情,那么它是怎么实现的呢?从所周知,匿名类会在编译的时候生成与宿主类带上 $1, $2 的类文件,如写在 TestLamb ...

  4. Lambda表达式动态拼接(备忘)

    EntityFramework动态组合Lambda表达式作为数据筛选条件,代替拼接SQL语句 分类: C# Lambda/Linq Entity Framework 2013-05-24 06:58 ...

  5. 十分钟学会Java8的lambda表达式和Stream API

    01:前言一直在用JDK8 ,却从未用过Stream,为了对数组或集合进行一些排序.过滤或数据处理,只会写for循环或者foreach,这就是我曾经的一个写照. 刚开始写写是打基础,但写的多了,各种乏 ...

  6. 【转】背后的故事之 - 快乐的Lambda表达式(一)

    快乐的Lambda表达式(二) 自从Lambda随.NET Framework3.5出现在.NET开发者眼前以来,它已经给我们带来了太多的欣喜.它优雅,对开发者更友好,能提高开发效率,天啊!它还有可能 ...

  7. 快乐的Lambda表达式(一)

    转载:http://www.cnblogs.com/jesse2013/p/happylambda.html 原文出处: Florian Rappl   译文出处:Jesse Liu 自从Lambda ...

  8. 委托、Lambda表达式、事件系列07,使用EventHandler委托

    谈到事件注册,EventHandler是最常用的. EventHandler是一个委托,接收2个形参.sender是指事件的发起者,e代表事件参数. □ 使用EventHandler实现猜拳游戏 使用 ...

  9. 委托、Lambda表达式、事件系列05,Action委托与闭包

    来看使用Action委托的一个实例: static void Main(string[] args) { int i = 0; Action a = () => i++; a(); a(); C ...

  10. 委托、Lambda表达式、事件系列04,委托链是怎样形成的, 多播委托, 调用委托链方法,委托链异常处理

    委托是多播委托,我们可以通过"+="把多个方法赋给委托变量,这样就形成了一个委托链.本篇的话题包括:委托链是怎样形成的,如何调用委托链方法,以及委托链异常处理. □ 调用返回类型为 ...

随机推荐

  1. Nginx常见错误解决办法

    报错: nginx: [error] CreateFile() "C:\mytools\nginx-1.8.1/logs/nginx.pid" failed (2: The sys ...

  2. C# 微信access_token缓存和过期刷新

    摘自:http://blog.csdn.net/hechurui/article/details/22398849 首先建立一个Access_token类 /// <summary> // ...

  3. CC2530定时器模模式最大值计算

    首先假设 频率: f 分频系数: n 间隔定时: s 周期: T 模模式最大值: N 因为 T = 1 / f 所以 s = ( n / f ) * N  =  n * N / f 由此可得 计算模模 ...

  4. Java8新特性--Base64转换

    1.简介 在Java8中,Base64编码已经成为Java类库的标准.Java 8 内置了 Base64 编码的编码器和解码器. Base64工具类提供了一套静态方法获取下面三种BASE64编解码器: ...

  5. java默认值

    注意:此处默认值是在类成员时才可以被初始化有默认值 如果时在局部变量中,必须先自己初始化才可以使用,否则编译失败

  6. mysql 改变表结构 alter

    总结:alter添加栏位时,只需记住添加新栏位为第一列,用first;添加其他用,after 前一个栏位字段,如下例 1.需求:将新的栏位添加为第二列 添加前: 添加后: 参考:http://www. ...

  7. docker容器学习资料

    现在说起docker容器,你应该不会太陌生了吧?如果你是真的不懂或者是太久没有用到已经忘记的差不多了,那么你需要这一波的干货了,这波的干货刚刚好可以满足你的需要! 话不多说,直接上干货

  8. 给大家分享一下java数据库操作步骤

    获取驱动程序Jar文件,并放置到项目的类路径中: 注册驱动器类: 获取数据库连接: 获取Statement对象来执行相关SQL操作: 关闭各种资源;

  9. 21个写SQL的好习惯,你值得拥有

    前言 每一个好习惯都是一笔财富,本文分SQL后悔药, SQL性能优化,SQL规范优雅三个方向,分享写SQL的21个好习惯,谢谢阅读,加油哈~ 公众号:捡田螺的小男孩 1. 写完SQL先explain查 ...

  10. STM32入门系列-创建寄存器模板

    介绍如何使用 KEIL5 软件创建寄存器模板, 方便之后使用寄存器方式来操作STM32开发板上的LED,让大家创建属于自己的寄存器工程模板. 获取工程模板的基础文件 首先我们在电脑任意位置创建一个文件 ...