java函数式接口,Lambda表达式以及方法引用

Lambda表达式

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表

  • 匿名——我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写得少而想得多!
  • 函数——我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
  • 传递——Lambda表达式可以作为参数传递给方法或存储在变量中。
  • 简洁——无需像匿名类那样写很多模板代码。

Lambda来自于学术界开发出来的一套用来描述计算的λ演算法。它可以让你十分简明地传递代码。理论上来说,你在Java 8之前做不了的事情,Lambda也做不了。但是,现在你用不着再用匿名类写一堆笨重的代码,来体验行为参数化的好处了!Lambda表达式鼓励你采用行为参数化风格。最终结果就是你的代码变得更清晰、更灵活。比如,利用Lambda表达式,你可以更为简洁地自定义一个 Comparator 对象。

先前代码:

Comparator<Apple> byWeight = new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
};

之后(用了Lambda表达式):

Comparator<Apple> byWeight =
 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

Lambda表达式有三个部分:

  • 参数列表——这里它采用了 Comparator 中 compare 方法的参数,两个 Apple 。
  • 箭头——箭头 -> 把参数列表与Lambda主体分隔开。
  • Lambda主体——比较两个 Apple 的重量。表达式就是Lambda的返回值了。

Lambda的基本语法是:

(parameters) -> expression
或(请注意语句的花括号)
(parameters) -> { statements; }

下面的例子:

  1.  () -> {}
  2.  () -> "Raoul"
  3.  () -> {return "Mario";}
  4.  (Integer i) -> return "Alan" + i;
  5.  (String s) -> {"IronMan";}

只有4和5是无效的Lambda。
(1) 这个Lambda没有参数,并返回 void 。它类似于主体为空的方法: public void run() {} 。
(2) 这个Lambda没有参数,并返回 String 作为表达式。
(3) 这个Lambda没有参数,并返回 String (利用显式返回语句)。

(4) return 是一个控制流语句。要使此Lambda有效,需要使花括号,如下所示:(Integer i) -> {return "Alan" + i;} 。
(5)“Iron Man”是一个表达式,不是一个语句。要使此Lambda有效,你可以去除花括号和分号,如下所示: (String s) -> "Iron Man" 。或者如果你喜欢,可以使用显式返回语句,如下所示:

(String s)->{return "IronMan";} 。

函数式接口

定义:函数式接口就是只定义一个抽象方法的接口。

注意:接口现在还可以拥有 默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口

下面哪些接口是函数式接口?
public interface Adder{
int add(int a, int b);
}
public interface SmartAdder extends Adder{
int add(double a, double b);
}
public interface Nothing{
}

只有 Adder 是函数式接口。SmartAdder 不是函数式接口,因为它定义了两个叫作 add 的抽象方法(其中一个是从Adder 那里继承来的)。Nothing 也不是函数式接口,因为它没有声明抽象方法。

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。下面的代码是有效的,因为 Runnable 是一个只定义了一个抽象方法 run的函数式接口:

@FunctionalInterface

函数式接口带有 @FunctionalInterface 的标注这个标注用于表示该接口会设计成一个函数式接口。如果你用 @FunctionalInterface 定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。请注意, @FunctionalInterface 不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是 @Override标注表示方法被重写了。

特殊的void兼容规则

如果一个Lambda的主体是一个语句表达式, 它就和一个返回 void 的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管 List 的 add 方法返回了一个boolean ,而不是 Consumer 上下文( T -> void )所要求的 void :

// Predicate返回了一个boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);

使用局部变量

Lambda表达式允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。例如,下面的Lambda捕获了 portNumber 变量:

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为 final ,或事实上是 final 。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量 this 。) 例如,下面的代码无法编译,因为 portNumber变量被赋值两次:

实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。使用final修饰之后,变量值不会被修改,所以这份副本始终有效。

方法引用

方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。

先前:
inventory.sort((Apple a1, Apple a2)

    -> a1.getWeight().compareTo(a2.getWeight()));

之后(使用方法引用和 java.util.Comparator.comparing ):

inventory.sort(comparing(Apple::getWeight));

方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符 :: 前,方法的名称放在后面。例如,Apple::getWeight 就是引用了 Apple 类中定义的方法 getWeight 。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式 (Apple a) -> a.getWeight() 的快捷写法。表3-4给出了Java 8中方法引用的其他一些例子。

如何构建方法引用
方法引用主要有三类。

  1. 指向静态方法的方法引用(例如 Integer 的 parseInt 方法,写作 Integer::parseInt )。
  2. 指向任意类型实例方法的方法引用( 例如String 的 length 方 法 , 写 作String::length )。
  3. 指向现有对象的实例方法的方法引用(假设你有一个局部变量 expensiveTransaction用于存放 Transaction 类型的对象,它支持实例方法 getValue ,那么你就可以写 expensiveTransaction::getValue )。

第二种和第三种方法引用可能乍看起来有点儿晕。类似于 String::length 的第二种方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如,Lambda表达式 (String s) -> s.toUppeCase() 可以写作 String::toUpperCase 。但第三种方法引用指的是,你在Lambda中调用一个已经存在的外部对象中的方法。例如,Lambda表达式()->expensiveTransaction.getValue() 可以写作 expensiveTransaction::getValue 。

类型语法对应的Lambda表达式
静态方法引用类名::staticMethod(args) -> 类名.staticMethod(args)
实例方法引用inst::instMethod(args) -> inst.instMethod(args)
对象方法引用类名::instMethod(inst,args) -> 类名.instMethod(args)
构造方法引用类名::new(args) -> new 类名(args)

总结:

  •  Lambda 表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
  •  Lambda 表达式让你可以简洁地传递代码。
  •  函数式接口就是仅仅声明了一个抽象方法的接口。
  •  只有在接受函数式接口的地方才可以使用 Lambda 表达式。
  •  Lambda 表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
  •  Java 8 自带一些常用的函数式接口,放在 java.util.function 包里,包括 Predicate<T> 、 Function<T,R> 、 Supplier<T> 、 Consumer<T> 和 BinaryOperator<T> ,如表3-2 所述。
  • 为了避免装箱操作,对 Predicate<T> 和 Function<T, R> 等通用函数式接口的原始类型特化: IntPredicate 、 IntToLongFunction 等。
  •  环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合 Lambda 提高灵活性和可重用性。
  •  Lambda 表达式所需要代表的类型称为目标类型。
  •  方法引用让你重复使用现有的方法实现并直接传递它们。
  •  Comparator 、 Predicate 和 Function 等函数式接口都有几个可以用来结合 Lambda 表达式的默认方法。

参考:Java8实战

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值