跳至主要內容

Java

LincZero大约 24 分钟

Java

目录

函数传递

“函数指针传递” 的方式(Java)

有几种方法

① 接口方法

例如通过实现接口的方式,本质是传递一个实现了接口的类

举两个例子:

接口实现:事件监听器

class Worker implements ActionsListener
{
    public void actionPerformed(AxtionEvent event)
    {
        // do some work
    }
}

接口实现:比较器 compare方法不是立即调用。实际上,在数组完成排序之前,sort方法会一直调用compare方法

class LengthComparator implements Comparator<String>
{
    public int compar(String first, String second)
    {
        return first.length() - second.length();
    }
}
...
Array.sort(Strings, new LengthComparator());

这两个例子有一些共同点,

都是将一个代码块传递到某个对象(一个定时器,或者一个sort方法)。这个代码块会在将来某个时间调用。

② Lambda表达式

Lambda表达式也可以做到类似的作用,并且方式更加简单

Q:思考:Java为什么引入lambda表达式

A:lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。 先来看看没有Lambda表达式时,Java是怎么传递 “函数指针的”。一般通过类似于接口+观察者模式来实现

③ 传递方法引用

普通方法引用

构造器引用

与C++不同(传递代码段的方式)

  • Java
    • Java很难传递代码段
    • 在Java中传递一个代码段并不容易,不能直接传递代码段。 Java是一种纯面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。
    • 问题已经不是是否增强Java来支持函数式编程,而是要如何做到这一点。 设计者们做了多年的尝试,终于找到一种适合Java的设计 —— Lambda表达式(Java SE 8引入)
  • 其他语言
    • 在其他语言中可以直接处理代码块。
    • Java设计者很长时间以来一直拒绝增加这个特性。Java非常重视其简单性和一致性。 他们认为如果只要一个特性能够让代码稍简洁一些,就把这个特性增加到语言中,这个语言很快就会变得一团糟,无法管理。

Lambda表达式(Java SE 8)

Lambda主要学习目的:

  • 如何生成lambda表达式
  • 如何把lambda表达式传递到需要一个函数式接口的方法
  • 如何编写方法处理lambda表达式

定义方法

定义方法

(String first, String second)
	-> first.length() - second.length()
        
(String first, String second) ->
    {
        if (first.length() < second.length()) return -1;
        else if (first.length() > second.length()) return 1;
        else return 0;
    }

// 写法上有点像js的箭头函数,即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样

类型自动推导

Comparator<String> comp
    = (first, second)  // 相当于 (String first, String second)
    	-> first.length() - second.length();

// 如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。
// 在这里,编译器可以推导出first和second必然是字符串,因为这个lambda表达式将赋给一个字符串比较器。

省略小括号

ActionListener listener = event -> System.out.println("The time is "+new Date());
	// 相当于 (event) -> ... or (ActionEvent event) -> ...

// 如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号

自动推导返回类型

// 无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出
// 例如下面这句可以在需要int类型结果的上下文中使用
(String first, String second) -> first.length() - second.length()

自动推倒返回类型时,要确保返回唯一性

// 如果一个lambda表达式只在某些分支返回一个值,而在另外一些分支不返回值,这是不合法的
// 例如下面这句是不合法的
(int x) -> {if (x>=0) return 1;}

使用举例

如何在一个比较器和一个动作监听器中使用lambda表达式

package lambda;
import java.util.*;
import javax.swing.*;
import javax.swing.Timer;

/**
 * 此程序演示如何使用lambda表达式
 * @version 1.0 2015-05-12
 * @author Cay Horstmann
 */
public class LambdaTest
{
    public static void main(String[] args)
    {
        // Lambda作为比较器参数传入
        String[] planets = new Sting[] {"水星", "金星", "地球", "火星", "木星", "土星", "天王星", "海王星"};
        System.out.println(Attays.toString(Planets));
        
        System.out.println("按字典名称排序:");
        Array.sort(planets);
        System.out.println(Arrays.toString(planets));
        
        System.out.println("按长度排序:");
        Arrays.sort(planets, (first,second) -> first.length() - second.length());		// Lambda的使用1
        System.out.println(Arrays.toString(planets));
        
        // Lambda表达式作为监听器参数传入
        Timer t = new Timer(
            1000,
        	event -> System.out.println("The time is " + new Date())					// Lambda的使用2
        );
        t.start();
        
        // 保持程序运行,直到用户选择 "Ok"
        J0ptionPane. showMessageDialog(nul1,"是否要退出程序?");
        System.exit(O);
    }
}

与C++不同(概念上)

Lambda表达式【区别还挺大的】

  • Java
    • lambda表达式所能做的也只是能转换为函数式接口。函数式接口这个概念其他语言也似乎是没有的
  • 其他语言
    • (这里指其他支持函数字面量的程序设计语言)
    • 可以声明函数类型(如(String, String) -> int)、声明这些类型的变量,还可以使用变量保存函数表达式。 不过,Java设计者还是决定保持我们熟悉的接口概念,没有为Java语言增加函数类型

与C++不同(写法上)

  • Java(区别极大)

    // 这里用了类型自动推导。另外,最重要的一点是:
    // 这里表示可以用来代替函数式接口来使用,而不是真的赋值、保存Lambda表达式
    Comparator<String> comp = (first, second) ->
    {
        first.length() - second.length();
    }
    
    // 用BiFunction倒是可以保存一个Lambda表达式到BiFunction变量中,但这没有什么用
    // 例如:没有哪个Arrays.sort方法想要接收一个BiFunction
    BiFunction<String, Sting, Integer> comp
        = (first, second) -> first.length() - second.length();
    
    // Predicate接口才是专门用来传递lambda表达式
    
  • C++

    // 语法
    [捕获列表] (形参列表) mutable 异常列表-> 返回类型
    {
        函数体
    }
    
    /*
        捕获列表:捕获外部变量,捕获的变量可以在函数体中使用,可以省略,即不捕获外部变量。
        形参列表:和普通函数的形参列表一样。可省略,即无参数列表
        mutable:mutable 关键字,如果有,则表示在函数体中可以修改捕获变量,根据具体需求决定是否需要省略。
        异常列表:noexcept / throw(...),和普通函数的异常列表一样,可省略,即代表可能抛出任何类型的异常。
        返回类型:和函数的返回类型一样。可省略,如省略,编译器将自动推导返回类型。
        函数体:代码实现。可省略,但是没意义。
    */
    
    // 举例
    auto l = [](int x) -> bool {
    	// ...  
    };
    
    auto lambda = [a, b](int x, int y)mutable throw() -> bool
    {
        return a + b > x + y;
    };
    
  • Python

    self.aboutAct = QAction("关于", self, statusTip="关于界面"
                            , triggered=lambda: QMessageBox.about(self, "About MDI", "WWWWWWWWWWWWW")
                            )  # def about(self);
    
  • JavaScript

    let max = (a,b) => a>b?a:b;
    // 也可用es6的箭头写
    let max = function (a,b){
    	return a>b?a:b;
    }
    

函数式接口 (Functional Interface)

概念

函数式接口 (Functional Interface)

  • 概念
    • 就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口(Runoob的解释)
  • 特点
    • 函数式接口可以被隐式转换为 lambda 表达式
  • 举例
    • Java中已经有很多封装代码块的接口,如ActionListener或Comparator。 lambda表达式与这些接口是兼容的。
    • 不能把lambda表达式赋给类型为Object的变量,Object不是一个函数式接口

几个概念的区别

三者区别(接口、函数式接口、Lambda表达式、函数、内联函数)

  • 函数式接口 & 接口 & Lambda
    • 函数式接口是一种特殊的接口
    • 函数式接口可以隐式地转换为Lambda表达式(普通接口则不行)
  • Lambda & 函数
    • 其他语言:Lambda是一种特殊的函数、类似于匿名闭包函数的概念
    • Java:没有函数这个概念,Lambda是一种可以用来代替函数式接口的东西
  • 原理上
    • C++:lambda表达式可以理解为一个匿名的内联函数

使用举例

展示如何转换为函数式接口

举例:Arrays.sort()方法

Array.sort(
    words,
    (first, second) -> first.length() - second.length()
);
// Arrays.sort 简明原理
// 原本它的第二个参数需要一个Comparator实例,方法会接收实现Comparator<String>的某个类的对象
// 而Comparator只有一个方法的接口,属于函数式借口,所以可以提供一个lambda表达式
// 在这个对象上调用compare方法会自动执行这个lambda表达式。这也是为什么函数式接口要求有且只有一个抽象方法

// 最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口。

举例:Timer定时器

Timer t = new Timer(1000, event ->
	{
		System.out.println("At the tone, the time is " + new Date());
        Toolkit.getDefaultToolkit().beep();
	}
);
// 原本它的第二个参数要传入一个实现了ActionListener接口的类
// 而这个类只有一个方法的接口

这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。???(这句话不太懂)

Java库举例

后面 “处理Lambda” 表达式一章会详细讲

BiFunction,保存Lambda表达式到变量

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

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

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

不过,这对于排序并没有帮助。没有哪个Arrays.sort方法想要接收一个BiFunction

这与其他函数式程序语言不同。如果你之前用过某种函数式程序设计语言,可能会发现这很奇怪

Predicate

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

public interface Predicate<T>
{
    boolean test(T t);
    // 附加默认项和静态方法
}

ArrayList类有一个removeIf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式

例如,下面的语句将从一个数组列表删除所有null值:

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

方法引用(Method Reference)

  • 普通方法引用(Method Reference)
  • 构造器引用(Construct Reference)

使用举例

举例:Timer定时器

例如,假设你希望只要出现一个定时器事件就打印这个事件对象

// 方案一:传入Lambda表达式
Timer t = new Timer(1000, event -> System.out.println(event));

// 方案二:直接把println方法传递到Timer构造器
Timer t = new Timer(1000, System.out::println);

举例:Arrays.sort()方法

假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式

Arrays.sort(strings, String::compareToIgnoreCase)

其他补充

等价的Lambda表达式

上面的 “举例:Timer定时器“ 中

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

函数重载问题

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

例如,Math.max方法有两个版本,一个用于整数,另一个用于double值。选择哪一个版本取决于 Math::max 转换为哪个函数式接口的方法参数。

::操作符小总结

要用::操作符分隔方法名与对象或类名。主要有3种情况

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

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

// 情况一。例如:
System.out::println			/*等价于*/		x->System.out.println(x)

// 情况二。例如:
Math::pow					/*等价于*/		(x,y)->Math.pow(x,y)

// 情况三。例如:
String::compareToIgnoreCase	/*等同于*/		(x,y)->x.compareToIgnoreCase(y)  // 其中第1个参数会成为方法的实例

结合this/super参数

可以在方法引用中使用this参数。例如,this::equals等同于x->this.equals(x)

super::instanceMethod

例子

class Greeter
{
    public void greet()
    {
        System.out.println("Hello, world!");
    }
}

class TimedGreeter extends Greeter
{
    public void greet()
    {
        // TimedGreeter.greet方法开始执行时,会构造一个Timer,它会在每次定时器滴答时执行super::greet方法
        Timer t = new Timer(1000, super::greet); 
        t.start();
    }
}

构造器引用(Construct Reference)

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

例如:Person::new是Person构造器的一个引用

使用举例

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

ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);	// 传入构造器引用。
List<Person> people = stream.collect(Collectors.toList());

// map方法会为各个列表元素调用Person(String)构造器

其他补充

等价的Lambda表达式

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

例如,int[]::new是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x->new int[x]

函数重载问题

例如在上面的例子中

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

妙用:构造泛型类型T的数组

Java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很有用。表达式new T[n]会产生错误,因为这会改为new Object[n]

例如:

// 假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:
Object[] people = stream.toArray();
// 不过,这并不让人满意。用户希望得到一个Person引用数组,而不是Object引用数组

// 流库利用构造器引用解决了这个问题。可以把Person[]::new传入toArray方法
Person[] people = steam.toArray(Person[]::new);
// toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。

闭包问题(Lambda变量作用域)

写法 - 访问外部方法或变量

通常,你可能希望能够在lambda表达式中访问外围方法或类中的变量 其他编程语言的Lambda基本都有这个功能

public static void repeatMessage(String text, int delay)
{
    ActionListener listener = event ->
    {
        // 注意这里的text变量
        //     不是在这个lambda表达式中定义的,是repeatMessage方法的一个参数变量
        // 这里会有问题:
        //     lambda表达式的代码可能会在repeatMessage调用返回很久以后才运行,而那时这个参数变量已经不存在了。
        // 如何保留text变量呢?
        System.out.println(text);
        Toolkit.getDefaultToolkit().beep();
    };
    new Timer(delay, listener).start();
}

// 调用
repeatMessage("Hello", 1000); // 打印:Hello every 1,000 milliseconds

原理 - Lambda如何保留变量?

下面来巩固我们对lambda表达式的理解。lambda表达式有3个部分:

  1. 一个代码块
  2. 参数
  3. 自由变量的值。这是指非参数而且不在代码中定义的变量

我们说它被lambda表达式捕获(captured)

_

关于代码块以及自由变量值有一个术语:闭包(closure)。在Java中,lambda表达式就是闭包。

写法限制

lambda表达式可以捕获外围作用域中变量的值,但是会有一些限制

① 只能引用值不会改变的变量

原因:因为如果在lambda表达式中改变变量,并发执行多个动作时就会不安全

// 例如,下面的写法是不合法的
public static void countDown(int start, int delay)
{
    ActionListener listener = event ->
    {
        start--;  // 错误: 不能修改捕获的变量
        System.out.println(start);
    };
    new Timer(delay, lestener).start();
}
② 不能引用可能在外部被改变的变量

如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的

lambda表达式中捕获的变量必须实际上是最终变量(effectively final)。最终变量是指:这个变量初始化之后就不会再为它赋新值

// 例如,下面的写法是不合法的
public static void repeat(String text, int count)
{
    for(int i=1; i<=count; i++)
    {
        ActionListener listener = event ->
        {
            System.out.println(i+": "+text);  // 报错:不能引用可能被在外部改变的变量
        };
        new Timer(1000, listener).start();
    }
}
③ 不能声明同名变量

在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的

这里同样适用命名冲突和遮蔽的有关规则

// 例如,下面的写法是不合法的
Path first = Paths.get("/usr/bin");
Comparator<String> comp = 
    (first, second) -> first.length() - second.length(); // 报错:变量“first”已经被定义过了
④ this的含义

在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数 这点有点像 js 箭头函数中 this 的使用

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

与C++不同(闭包与变量捕获)

首先,**闭包 (closure)**是什么?

参考:

百度百科解释:

**(功能上.)**闭包就是能够读取其他函数内部变量的函数。

**(使用上.)**例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。

**(本质上.)**在本质上,闭包是将函数内部和函数外部连接起来的桥梁。


mozilla解释:

**(组成上.)**闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。

**(功能上.)**换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。

**(使用上.)**在 JavaScript 中,闭包会随着函数的创建而被同时创建。


Wiki:

(概念上.)在计算机科学中,闭包(Closure),又称词法闭包(Lexical Closure)函数闭包(function closures)。 是在支持头等函数的编程语言中实现词法绑定的一种技术。

(组成/实现上.)闭包是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。 环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。

**(语言上.)**最早实现闭包的程序语言是Scheme。 之后,闭包被广泛使用于函数式编程语言 (如ML语言和LISP),很多命令式程序语言也开始支持闭包。

**(区分闭包和 函数 .)**它们最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。 捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。

**(区分闭包和匿名函数.)**它们经常被用作同义词。但严格来说,匿名函数就是字面意义上没有被赋予名称的函数,而闭包则实际上是一个函数的实例,也就是说它是存在于内存里的某个结构体。

各种语言中(类似)闭包的结构

  • C语言

    • C语言的回调函数

      在C语言中,支持回调函数的库有时在注册时需要两个参数:一个函数指针,一个独立的void*指针用以保存用户数据。 这样的做法允许回调函数恢复其调用时的状态。这样的惯用法在功能上类似于闭包,但语法上有所不同。

    • gcc对C语言的扩展

      gcc编译器对C语言实现了一种闭包的程序特性。

    • C语言扩展:Blocks

  • C++函数对象

    • 早期标准

      早期标准允许通过重载operator()来定义函数对象。这种对象的行为在某种程度上与函数式编程语言中的函数类似。

      它们可以在运行时动态创建、保存状态,但是不能如闭包一般方便地隐式获取局部变量,并且有“专物专用”的繁琐问题——对于每一段闭包代码都要单独写一个函数对象类。

    • C++11

      // C++11标准已经支持了闭包,这是一种特殊的函数对象,由特殊的语言结构——lambda表达式自动构建
      void foo(string myname) {
      	typedef vector<string> names;
      	int y;
      	names n;
      	// ...
      	names::iterator i =
      	 find_if(n.begin(), n.end(), [&](const string& s){return s != myname && s.size() > y;});	
      	// 'i' 现在是'n.end()'或指向'n'中第一个
      	// 不等于'myname'且长度大于'y'的字符串
      }
      
  • Java

    • Java SE 8 引入Lambda,可以用Lambda来实现闭包

    • 注意事项

      • ① 只能引用值不会改变的变量
      • ② 不能引用可能在外部被改变的变量
      • ③ 不能声明同名变量
      • ④ this的含义
    • 代码举例

      public static void repeatMessage(String text, int delay)
      {
          ActionListener listener = event ->
          {
              // 这里捕获了text变量。不怕函数结束时该变量被销毁
              System.out.println(text);
              Toolkit.getDefaultToolkit().beep();
          };
          new Timer(delay, listener).start();
      }
      
      // 调用
      repeatMessage("Hello", 1000); // 打印:Hello every 1,000 milliseconds
      
  • JavaScript

    • JavaScript 在闭包上要简单许多 Javascript 语言的特殊之处,就在于函数内部可以直接读取全局变量 JavaScript 变量可以是局部变量或全局变量。私有变量可以用到闭包。

      // 实例1,a 是一个 局部 变量
      function myFunction() {
          var a = 4;
          return a * a;
      }
      
      // 实例2,a 是一个 全局 变量
      var a = 4;
      function myFunction() {
          return a * a;
      }
      

处理Lambda表达式

概述

使用lambda表达式的重点是延迟执行(deferred execution) 毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个lambda表达式中

写法

① 写法(使用默认的函数式接口:Runnable)

// 【目的】
// 假设你想要重复一个动作n次

// 【方法定义】
// 要接受这个lambda表达式,需要选择(偶尔可能需要提供)一个函数式接口
public static void repeat(int n, Runnable action)
{
    for(int i = 0; i<n; i++) action.run();  // 调用 action.run() 时会执行这个lambda
}

// 【调用】
// 将这个动作和重复次数传递到一个repeat方法:
repeat(10, () -> System.out.println("Hello, World!"));

② 改进版本(使用自定义的函数式接口:IntConsumer)

// 【目的】
// 现在让这个例子更复杂一些。我们希望告诉这个动作它出现在哪一次迭代中。

// 【函数式接口定义】
public interface IntConsumer
{
    void accept(int value);
}

// 【方法定义】
// 这里使用函数式接口:IntConsumer
public static void repeat(int n, IntConsumer action)
{
    for (int i=0; i<0; i++) action.accept(i);
}

// 【调用】
repeat(10, i -> System.out.println("Countdown: " + (9-i)));

常用函数式接口

常用函数式接口(表一)

函数式接口参数类型返回类型抽象方法名描述其他方法
Runnablenonevoidrun作为无参数或返回值的动作运行
Supplier<T>noneTget提供一个T类型的值
Consumer<T>Tvoidaccept处理一个T类型的值andThen
BiConsumer<T, U>T, Uvoidaccept处理T和U类型的值andThen
Function<T, R>TRapply有一个T类型参数的函数compose, andThen, identity
BiFunction<T, U, R>T, URapply有T和U类型参数的函数andThen
UnaryOperator<T>TTapply类型T上的一元操作符compose, andThen, identity
BinaryOperator<T>T, TTapply类型T上的二元操作符andThen, maxBy, minBy
Predicate<T>Tbooleantest布尔值函数and, or ,negate, isEqual
BiPredicate<T, U>T, Ubooleantest有两个参数的布尔值函数and, or, negate

基本类型的函数式接口(表二)

函数式接口参数类型返回类型抽象方法名描述-------------------------------------------------------------------------------------
BooleanSuppliernonebooleangetAsBoolean
PSuppliernonepgetAsP
PConsumerpvoidaccept
ObjPConsumer<T>T, pvoidaccept
PFunction<T>pTapply
PToQFunctionpqapplyAsQ
ToPFunction<T>TpapplyAsP
ToPBiFunction<T, U>T, UPapplyAsP
PUnaryOperatorpPapplyAsP
PBinaryOperatorp, pPapplyAsP
PPredicatepbooleantest

注:p,q为int,long,double;P,Q为Int,Long,Double

常用函数式接口的注意要点

使用优先级

① 最好优先使用上面表中的接口

例如:假设要编写一个方法来处理满足某个特定条件的文件。 虽然有一个遗留接口java.io.FileFilter,不过最好使用标准的 Predicate<File>。 只有一种情况下可以不这么做,那就是你已经有很多有用的方法可以生成FileFilter实例

② 最好优先使用表二的接口

使用特殊化规范来减少自动装箱(即使用下面的表二而不是表一) 出于这个原因,前面的例子中使用了IntConsumer而不是Consumer<Integer>

非抽象方法来生成或合并函数

大多数标准函数式接口都提供了非抽象方法来生成或合并函数

生成函数,例如:

Predicate.isEqual(a)
// 等同于
a::equals
// 不过如果a为null也能正常工作。

合并函数,例如:

// 提供了默认方法 and、or和negate 来合并谓词
Predicate.isEqual(a).or(Predicate.isEqual(b))
// 等同于
x->a.equals(x)||b.equals(x)
@FunctionalInterface 注解

如果设计你自己的接口,其中只有一个抽象方法,可以用@FunctionalInterface注解来标记这个接口。

这样做有两个优点

① 如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息

② 另外javadoc页里会指出你的接口是一个函数式接口。

并不是必须使用注解。根据定义,任何有一个抽象方法的接口都是函数式接口。不过使用@FunctionalInterface注解确实是一个很好的做法

实例 - 再谈Comparator

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

静态comparing方法

可以取一个“键提取器”函数,它将类型T映射为一个可比较的类型(如String)。

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

代码举例

假设有一个Person对象数组

可以按名字对这些对象排序:

Arrays.sort(
    people,
    Comparator.comparing(Person::getName)	// 与手动实现一个Comparator相比,这当然要容易得多。
											// 另外,代码也更为清晰,因为显然我们都希望按人名来进行比较。
);

可以先比较姓再比较名:(把比较器与thenComparing方法串起来实现)

Array.sort
(
    people,
    Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName)
);
// 如果两个人的姓相同,就会使用第二个比较器

可以根据人名长度完成排序:

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

上面的代码还有一种更容易的做法:

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

按可能为null的中名进行排序:

// 如果键函数可以返回null,可能就要用到nullsFirst和nullsLast适配器
// 原理:这些静态方法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或大于正常值。
// 例如,假设一个人没有中名时getMiddleName会返回一个null,就可以使用
Comparator.comparing(Person::getMiddleName(), Comparator.nullsFirst(...))
    
// 下面是一个完整的调用,可以按可能为null的中名进行排序。
// 这里使用了一个静态导入java.util.Comparator.*,以便理解这个表达式
Array.sort
(
    people,
    Comparator.comparing(Person::getMiddleName, nullsFirst(naturalOrder()))
);

其他

静态reverseOrder方法会提供自然顺序的逆序。要让比较器逆序比较,可以使用reversed实例方法。

例如 naturalOrder().reversed() 等同于 reverseOrder()