Lambda表达式

Lambda表达式


本章内容:

Lambda管中窥豹
在那里以及如何使用Lambda
环绕执行模式
函数式接口,类型推断
方法引用
Lambda复合

Lambda管中窥豹

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

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

Lambda来自于学术界开发出来额一套用来描述计算的λ,在java中传递代码十分繁琐和冗长。现在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表达式有三部分

  • 参数列表
  • 箭头
  • Lambda主体
    为进一步说明,下面给出java8中5个有效的Lambda表达式的例子
(String s) -> s.length //具有一个String类型的参数并返回一个int。lambda没有return语句,因为已经隐含了return。
(Apple a) -> a.getWeigth() > 150 
(int x, int y) -> { //lambda表达式可以包含多行语句
    System.out.println("Result:");
    System.out.println(x + y
}
() -> 42
(Apple a1, Apple a2) ->a1.getWeight.compareTo(a2.getWeight())

java之所以设置这样的语法,是因为c#和Scala等语言中的类似功能广受欢迎Lambda的基本语法是
(parameters) -> express

(parameters) -> {statements;}

928bf7955a005147b9888d14ed0e510b.png](en-resource://database/1583:1)![1a2064be71115b17d1821a5c9ff0593f.png

下表提供了Lambda的例子和使用案例

在这里插入图片描述

在哪里以及如何使用Lambda

在上一个例子中将Lambda赋给一个Comparator类型的变量。你也可以在上一章实现filter方法中使用Lambda:
List<Apple> greenApples = filter(inventory, (Apple a) -> "green".equals(e.getColor()));
可以将Lambda作为第二个参数传给filter方法,因为这里需要Predicate,而这是一个函数式接口。

函数式接口

在上一章中,为了参数化filter方法的行为而创建了Predicate接口,他就是一个函数式接口!为什么呢?因为Predi仅仅定义了一个抽象方法。

public interface Predicate<T> {
    boolean test (T t);
}

一言以蔽之,函数式接口就是之定义一个抽象方法的接口。已经知道了java API中的一些其他函数式接口,如Comparator和Runnable

public  interface Comparator<T> {
    int compare(T o1, To2);
}
public interface Runnable {
    void run();
}
public interface ActionListener extends EventListener {
    void actionPerformed(ActionEvent e);
}
public interface Callable<V> {
    V call();
}
public interface PrevilegeAction<V> {
    V run();
}

Lambda表达式允许你直接以内敛的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体来说,是函数式接口一个具体实现的实例)。下面的代码式有效的,因为Runnable式一个之定义了一个抽象方法的run的函数式接口:
Runnable r1 = () -> System.out.println(“Hello World”);

函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫做函数描述符,例如Runnable接口可以看作一个什么也不接受也不返回(void)的函数的签名,因为它只有一个叫做run的抽象方法。

在这里插入图片描述

把Lambda付诸实践:环绕执行模式

资源处理时一个常见的模式式打开一个资源,做一些处理,然后关闭资源。这就是所谓的环绕执行(execute around)模式,下例使用了java7中带资源的try语句,他已经简化了代码因此不需要显示的关闭资源了。

public static String ProcessFile() throws IOExecption {
    try(BufferReader br = new BufferReader(new FIleReader("data.txt"))) {
        return br.readLine();
    }
}

第1步:记得行为参数化

现在这段代码是有局限的。你只能读文件的第一行。如果你想返回头两行,甚至是返回使用最频繁的词。改怎么办?在理想的情况下,要重用执行设置和清理的代码,并告诉processFile方法对文件执行不同的操作,你需要一种方法把行为传递给processFile,以便利用BufferedReader执行不同的行为。
传递行为正是Lambda的拿手好戏,下面就是打印两行的写法:

String result  = processFile((BufferedReader br) -> br.readLine() + br.readLine());

第2步:使用函数接口来传递行为

Lambda进可用于上下文是函数式接口的情况。需要创建一个能匹配BufferedReader -> String还可以抛出异常IOExecption异常的接口。

@FunctionalInterface
public interface BufferedReaderProcessor {
    String process(BufferedReader b) throws IOExecption;
}

现在可以将这个接口作为新的processFile方法的参数

public static String processFile(BufferedReaderProcessor p) throws IOExecption {

}

第3步:执行一个行为

public static String ProcessFile(BufferedReaderProcessor p) throws IOExecption {
    try(BufferReader br = new BufferReader(new FIleReader("data.txt"))) {
        return p.process(br);
    }
}

第4步:传递Lambda

处理一行:
String oneLine = processFile((BufferedReader br) -> br.readLine())
处理两行
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine())
下图终结了使processFile方法更加灵活的四个步骤

在这里插入图片描述

虽然可以利用函数式接口来传递Lambda,但你还得定义自己的接口。下面将会探讨java8中加入的新接口,可以用它来传递不同的Lambda。

使用函数式接口

函数式接口定义且之定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名成为函数描述符。所以为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口JavaAPI已经有了几个函数式接口,比如Comparable、Runnable和Callable
接下来会介绍Predicate、Consumer和Function

Predicate

java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean,当需要设计类型T的布尔表达式时候,就可以使用这个接口。例如:

@FunctionalInterfacepublic interface Predicate<T> {
    boolean test(T t);
}
public static <T> list<T> filter(List<T> list, Predicate<T> p) {
    List<T> results = new ArrayList<>();
    for(T s : list) {
        if(p.test(s){
            results.add(s);
        }
    }
    return results;
}

Predicate<String> nonEmptyStringPredicate  = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

Consumer

java.util.function.COnsumer定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void),如果需要访问类型T的对象,并对其执行某些操作,可以使用这个接口。例如:

@functionalInterface
public interface COnsumer<T> {
    void accept(T t);
}
public static <T> void forEach(List<T> list , Consumer<T> c) {
    for(T t : list) {
        c.accept(i);
    }
}
foreach(Attays.asList(1,2,3,4,5), (Integer i) -> System.out.prinln(i))

Function

java.util.function.Function<T, R>接口定义了一个叫做apply的方法,它接受一个泛型T对象,并返回一个泛型R对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,姐可以使用这个接口

@FunctionalInterface
public interface Function<T, R> {
    R.apply(T t);
}
public static <T, R> List<R> map(List<T>, Function<T, R> f) {
    List<R> result = new ArrayList<>();
    for(T s : list) {
        result.add(f.apply(s));
    }
    return result;
}
List<Integer> l = map(Arrays.asList("lambda", "in", "action"), (String s) -> s.length);

原始类型特化
java类型要么式引用类型,要么式原始类型。但是泛型只能绑定到引用类型。这是由泛型内部的实现方式造成的。因此有了自动装箱拆箱操作,例如

List<Integer> list = new ArrayList<>();
for(int i = 0; i < 400; i++) {
    list.add(i)
}

但这在性能方面要符出代价的。装箱的值的本质就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹得原始值,java8为我们之前所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱操作。

public interface intPredicate {
    boolean test(int t);
}
IntPredicate evenNumbers = (int i) -> i %2 == 0;
evenNumbers.test(1000);  //无装箱
Predicate<Interger> oddNumbers = (integer i) -> i % 2 ==1;
evenNumbers.test(1000);  //false装箱

一般来说,针对专门的输入参数类型的函数式接口名称都要加上对应的原始类型前缀,比如DoublePredicate、IntCOnsumer、LongBinaryOperatator、IntFunction等。Function接口还有针对输出类型参数类型的变种:ToIntFunction、IntToDoubleFunction等
下表提供了Java API宠用的函数式接口及其函数描述符。
在这里插入图片描述
在这里插入图片描述
一些例子:
在这里插入图片描述
在这里插入图片描述

类型检查、类型推断以及限制

当第一次提到Lambda表达式时,说它可以为函数式接口生成一个实例。然而,Lambda表达式本身并不包含实现哪个函数式接口的信息。为了全面了解Lambda表达式,应该知道Lambda的实际类型是什么。

类型检查

Lambda的类型式从Lambda的上下文类型推断出来的。下图概括了类型检查过程。
在这里插入图片描述
类型检查过程可以分解为如下所示

  • 首先,需要找出filter方法的声明。
  • 第二,要求他是Predicate(目标类型)对象的第二个正式参数。
  • 第三,Predicate是一个函数式接口,定义了一个叫做test的抽象方法。
  • 第四,test方法描述了一个函数描述符,他可以接受一个Apple,并返回一个boolean。
  • 最后,filter的任何实际参数都必须匹配这个请求。
    请注意,如果Lambda表达式抛出了一个异常,那么抽象方法所声明的throws语句也必须与之匹配

同样的Lambda,不同的函数式接口

有了目标函数这个概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。下面的这两个赋值式等效的

Callable<Interger> c = () -> 42;
PrivilegedAction<Interger> p = () -> 42;

下例展示了一个类似的例子;同一个Lambda可用于多个而不同的函数式接口;
Comparator c1 = (Apple a1, Apple a2) -> a1.getWeight().comparaTo(a2.getWeight());
ToIntBiFunction <Apple, Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().comparaTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c1 = (Apple a1, Apple a2) -> a1.getWeight().comparaTo(a2.getWeight());

特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容

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

类型推断

可以进一步的简化代码,java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,例:

List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor()));

Lambda表达式有多个参数,代码可读性的好处就更为明显,例:
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeigt().compareTO(a2.getWeight());
Comparator<Apple> c = (a1, a2) -> a1.getWeigt().compareTO(a2.getWeight());
但是有时显式写出类型更易读,有时去掉更易读,没什么准则。

使用局部变量

Lambda表达式允许使用自由变量(外层作用域中定义的变量),它们被称为捕获Lambda;

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

Lambda可以没有限制的捕获实例变量和静态变量。但是局部变量必须显式声明为final。或事实上是fina,换句话说Lambda只能捕获指派给它们的局部变量一次,下面的代码无法编译

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

对局部变量的限制
java在访问自由局部变量的时候,实际上访问的是它的副本,而不是原始变量,如果局部变量仅仅赋值一次那就没有什么区别了。

方法引用

先前:
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compateTo(a2.getWeight()))
之后:
inventory.sory(comparing(Apple::getWeight))

管中窥豹

方法引用可以看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是如果一个Lambda代表的是“直接调用这个歌方法”,那么最好还是用名称来调用它,而不是描述如何调用它,目标引用放在分割符::前,方法名称放在后面,例如Apple::getWeight就是引用了Apple类中定义的方法getWeight

在这里插入图片描述

如何构建方法引用
(1)指向静态方法的方法引用(例如Interger::parseInt)
(2)指向任意类型实例方法的方法引用(String::length)
(3)指向现有对象实例方法的方法引用

在这里插入图片描述
还有针对构造函数、数组构造函数和父类调用的一些特殊形式的写法

List<String> str = Arrays.asList("a", "b", "A", "B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));

改写
str.sort(String::compateTOIgnoreCase)

25149f65ca46e604da69a4e6e1751ac5.png

构造函数引用

对于一个构造函数,可以利用它的名称和关键字new创建它的一个引用:ClassName::new

Supper<Apple> c1 = Apple::new;
Apple a1 = c1.get()

等价于

Supper<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();

如果构造函数的签名是Apple(Integer weight),就可以这样写

Function<Interger, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);

等价于

Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);

摘自java8 in action 未完!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值