1-06-2 Lambda表达式

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表达式的主体。

    Java核心技术读书笔记_06 接口、lambda表达式与内部类_1

  • 现在让这个例子更复杂一些。我们希望告诉这个动作它出现在哪一次迭代中。 为此,需
    要选择一个合适的函数式接口,其中要包含一个方法,这个方法有一个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>。

    Java核心技术读书笔记_06 接口、lambda表达式与内部类_2

    • 最好使用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()。

原文地址:https://www.cnblogs.com/nojacky/p/13909180.html