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. day22 函数整理

    # 1.计算 年月日时分秒 于现在之间差了多少 格式化时间 # 现在 # 某一个年月日时分秒 参数 # import time # def get_time(old_t,fmt = '%Y-%m-%d ...

  2. ES6之数组

    数组新增方法 map(可以理解为是映射,以一定规则修改数组每一项并返回全新数组) reduce(可以理解为是汇总,一堆出来一个) filter(可以理解为过滤,筛选的意思,以一定规则拿到符合的项并返回 ...

  3. spring boot:用redis+lua限制短信验证码的发送频率(spring boot 2.3.2)

    一,为什么要限制短信验证码的发送频率? 1,短信验证码每条短信都有成本制约, 肯定不能被刷接口的乱发 而且接口被刷会影响到用户的体验, 影响服务端的正常访问, 所以既使有图形验证码等的保护, 我们仍然 ...

  4. spring boot:使用redis cluster集群作为分布式session(redis 6.0.5/spring boot 2.3.1)

    一,为什么要使用分布式session? HpptSession默认使用内存来管理Session,如果将应用横向扩展将会出现Session共享问题, 所以我们在创建web集群时,把session保存到r ...

  5. centos8平台使用nethogs基于进程监控网络流量

    一,nethogs的作用: 按进程或程序实时统计网络带宽使用率 我们查看流量的占用时,知道来源的ip.访问的端口,还不足以帮我们确认到进程, 而nethogs则可以让我们查看每个进程所占用的流量带宽 ...

  6. C++ 设置软件激活不息屏

    SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED);

  7. Vue-cli3以上安装jquery

    vue-cli3以上就没有webpack.config.js这个文件了,所以在安装jquery时 终端执行  npm install jquery --save 之后查看package.json 安装 ...

  8. Oracle一些常用操作语句

    --创建oracle登录用户 create user CHECKDATAUSER   identified by "bsoft"   default tablespace PBPG ...

  9. E. Xenia and Tree 解析(思維、重心剖分)

    Codeforce 342 E. Xenia and Tree 解析(思維.重心剖分) 今天我們來看看CF342E 題目連結 題目 給你一棵樹,有兩種操作,把某點標成紅色或者查詢離某點最近的紅點有多遠 ...

  10. 什么是SOAP?SOAP有什么用?什么时候会用到SOAP?

    什么是SOAP SOAP(Simple Object Access Protocol)一般指简单对象访问协议,简单对象访问协议是交换数据的一种协议规范,是一种轻量的.简单的.基于XML(标准通用标记语 ...