2018-10-23

java学习笔记(四)

简单地讲讲Lambda表达式

Lambda管中窥豹

在前一篇文章中,已经看见了Lambda表达式的效果。那什么是Lambda表达式呢?Lambda基于数学中的λ演算得名,在 Java 中你可以把Lambda表达式理解为一种简洁地表示可传递的匿名函数的方式。它没有名称,但它有参数列表,有函数主体,又返回值,可能还可以抛出一个异常列表。

在Java8 之前的Java 传递代码十分繁琐和冗长,Java8 之后可以使用Lambda 就可以解决这个问题,不必要在为匿名类写一堆笨重的代码。使用Lambda 的结果是你的代码变得清晰、灵活。我们可以看看两个Apple比较重量的例子:

Java8 之前

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

使用Lambda之后

Conparator<Apple> byWeith = (Apple a1, Apple a2) -> a1.getWeith().compareTo(a2.getWeith());

不得不承认,代码看起来更清晰了。要是你觉得Lambda表达式看起来似懂非懂也没关系,我们慢慢的来了解它。

现在,我们来看看几个Java8中有效的Lambda表达式加深对Lambda表达式的理解:

// 这个表达式具有一个String类型的参数并返回一个int,Lambda并没有return语句,因为已经隐含了return。
(String s) -> s.length() 
// 这个表达式有一个Apple类型的参数并返回一个boolean(苹果重来是否大于150克)
(Apple a) -> a.getWeight() > 150
// 这个表达式具有两个int类型二的参数并且没有返回值。注意Lambda表达式可以包含多行代码。
(int x, int y) -> {
    System.out.println("Result:");
    System.out.println(x + y);
}
// 这个表达式没有参数类型,返回一个int。
() -> 88
// 显式的指定为Apple类型,并对重量进行比较返回int
(Apple a2, Apple a2) -> a1.getWeight.compareTo(a2.getWeight())

Java语言设计者选选择了这样的语法,是因为C#和Scala等语言中的类似功能广受欢迎。Lambda的基本语法是:

(parameters) -> expression

或者(请注意语句花括号):

(parameters) -> {statements;}

是的,Lambda表达式的语法看起来就是那么简单。

在哪里以及如何使用Lambda

现在你可能在想哪里适合使用 Lambda ,以前是在 filter 方法中使用 Lambda :

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

在 filter 方法中有一个 Predicate<T> 函数式接口,我们才能把 Lambda 表达式作为参数传入方法中。下面就简单介绍一下函数式接口。

函数式接口

参数化的 filter 方法需要一个函数接口,而 Predicate<T> 接口就是一个函数式接口。满足函数接口的条件其实很简单,只要接口中有且只有一个抽象方法(可以有其他的默认方法)即可。而 Predicate<T> 就满足这个条件。Java API 中提供了很多其他函数接口:

public interface Comparator<T> {
    int compare(T o1, T o2);
}

public interface ActionListener extends EventListener {
    void actionPerformed(ActionEvent e);
}

public interface Collable<V> {
    V call();
}

public interface PrivilegedAction<V> {
    V run();
}

这只是一小部分。Lambda 表达式允许直接以内敛的形式为函数是接口的抽象方法提供实现,并把整个表达式作为函数是接口的实例。这就是 Lambda 表达式与函数式接口的关系。

函数描述符

函数式接口的抽象方法的签名基本上就是 Lambda 表达式的签名。这种抽象方法叫做函数描述符。例如:Comparator 接口可以看作一个接受两个参数返回一个整数的函数签名,因为它有一个 compare 方法,这个方法接受两个参数返回一个整数。为了简单会使用形如 () -> void 的表示接受为空,返回为空的 Lambda 和函数式接口的签名。

把 Lambda 付诸实践:环绕执行

看一个简单的例子,在资源处理时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。设置和清理阶段总是类似的,并且会围绕执行处理的那些重要代码。这就是环绕执行模式,在下面的 Java7 代码中其实已经有了很大简化。

public String processFile() throws IOExceptoin {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return br.readLine();
    }
}

现在代码还是有局限的,只能读取一行。如果改变需求,如返回两行,或返回频繁词汇等等。就需要改变行为了。是不是很熟悉,没错就是行为参数化。

第一步:记得行为参数化

传递行为是 Lambda 的拿手好戏。你需要把 processFile 的行为参数化,然后需要一种方法把行为传递给 processFile 。下面是新的 processFile 方法:

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

第二步:使用函数式接口传递行为

Lambda 仅可用于上下文是函数式接口的情况,你需要一个能匹配 BufferedReader -> String ,还能抛出 IOException 异常的接口。

@FunctionalInterface
public interface BufferedReaderProcess {
    String process(BufferedReader br) throws IOException;
}

这样你就可以把这个接口作为 processFile 方法的参数了:

public String processFile(BufferedReaderProcess p) throws IOException{
    ...
}

第三步:执行一个行为

现在我们需要在 processFile 方法里执行 Lambda 所代表的代码。Lambda 表达式允许直接内联,为函数是接口的抽象方法提供实现,并将整个表达式作为一个接口的实例。因此,我们能在 processFile 方法里直接调用 process 方法执行处理:

public String processFile(BufferedReaderProcess p) throws IOException{
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return br.process(br);
    }
}

第四步:传递 Lambda

现在我们可以通过传递 Lambda 重用 processFile 方法,并以不同的方式处理文件。

处理一行:

String oneLine = processFile((BifferedReader br) -> br.readLine());

处理两行:

String oneLine = processFile((BifferedReader br) -> br.readLine() + br.readLine());

简单的展示了如何利用函数式接口来传递 Lambda ,但是我们还需要自己定义接口。下面我们来了解一下 Java8 新加入的接口。

使用函数式接口

函数是接口定义却只定义了一个抽象方法。抽象方法的签名可以描述 Lambda 表达式的签名,也称为函数描述符。为了应用不同的 Lambda 表达式,我们需要一套能够描述常见函数描述符的函数式接口。Java API 已经有了几个函数式接口,他们在 java.util.function 包中,下面简单介绍几个常见的接口。

Predicate 接口

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

这个接口有一个 test 的抽象方法,它接受一个泛型 T 对象,返回一个 boolean 。其实这个和前面创建的是一样的,现在可以直接使用这个接口。

使用 Predicate

public List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();
    for (T t : list) {
        if (p.test(t)) {
            result.add(t);
        }
    }
    return result;
}


List<String> nonEmpty = filter(listOfString, (String s) -> !s.isEmpty())

Consumer 接口

@FunctionInterface
public interface Consumer<T> {
    void accept(T t);
}

这个接口有一个 accept 的抽象方法,它接受一个泛型 T 对象,没有返回(void)。如果你需要访问类型 T 的对象,并对其执行某些操作,就可以使用这个接口。比如,在 forEach 方法中使用它执行某些操作:

public void forEach(List<T> list, Consumer<T> c) {
    for (T t : list) {
        c.accept(t);
    }
}


forEach(Array.asList(1, 2, 3, 4, 5), (Integer i) -> System.out.println(i));

Function 接口

@FunctionInterface
public interface Function<T, R> {
    R apply(T t);
}

这个接口有一个 apply 的抽象方法,它接受一个泛型 T 对象,返回一个泛型 R 对象。如果你想定义一个 Lambda ,将输入对象的信息映射到输出,就可以使用这个接口。例如下面这个例子:

public List<R> map(List<T> list, Function<T, R> f) {
    List<R> result = new ArrayList<>();
    for (T t : list) {
        result.add(f.apply(t));
    }
    return result;
}


List<Integer> list = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length());

就简单介绍这三个接口,其他接口你可以去查阅 Java API 的相关说明。

Java 中有两种类型,一个是原始类型,一个是引用类型。前面三个接口都是为引用类型而设计的。原始类型可以通过自动拆装箱和引用类型转换,但是性能方面需付出代价。Java8 为此提供了原始类型的特化接口。就是在函数式接口名称上加上对应的原始类型前缀,比如 DoublePredicate、IntConsumer 等等。

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

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

类型检查

Lambda 的类型是从使用 Lambda 的上下文推断出来的,上下文中的 Lambda 表达式需要的类型指的是目标类型。通过下例代码简单的介绍一下类型检查的过程。

List<Apple> heavierThan150g = filter(inventory, (Apple a) -> getWeight() > 150)
    

第一:找出 filter 方法的声明

filter(List<Apple> inventory, Predicate<Apple> p)

第二:找出要求的目标类型

Predicate<Apple> 

第三:确定 Predicate<Apple> 是函数式接口,找出抽象方法

boolean test(Apple a)

第四:确定 test 方法的函数描述符

Apple -> boolean

最后:filter 方法的任何实际类型参数都需要匹配,如果有异常抛出也必须匹配。

同样的 Lambda ,不同的接口

有了目标类型的概念,同一个 ​Lambda 就可以和不同的函数是接口联系起来了,只要抽象方法能兼容。例如:

Callable<Integer> c = () -> 75;
PrivilegeAction<Integer> p = () -> 75;

应为他们都是什么都不接受,返回一个泛型 T 。

菱形运算符

熟悉 Java 演变的人会记得,Java7 中引入菱形运算符(< >),利用泛型推断从上下文推断类型的思想。一个实例表达式可以出现在多个不同的上下文中,并会像下面这样推断出合适的类型。

List<String> ListOfString = new ArrayList<>();
List<Integer> ListOfIntegers = new ArrayList<>();

现在你应该能很好理解什么时候使用 Lambda 表达式了。使用类型检查可以知道 Lambda 是否合适某个特定的上下文。其实,我们可以推断 Lambda 的参数类型。

类型推断

还可以进一步简化所写代码。Java 编译器会从上下文中推断函数的目标类型,这表明可以推断出适合的 Lambda 签名,应为函数描述符可以通过目标类型得到。这样我们可以在 Lambda 语法中省去参数注明。例如:

//参数 a 没有类型
List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor()));

Lambda 表达是由多个参数,代码可读性的好处就更为明显。例如:你可以这样创建一个 Comparator 对象:

//没有类型推断
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
//有类型推断
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

有时候显示写出类型更易读,有时候去掉更易读。没有哪种说法更好,由程序员喜好决定。

使用局部变量

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

int portNumber = 6666;
Runner r - () -> System.out.println(portNumber);

但有时候有一点小麻烦,局部变量必须是显示声明为 final 或者事实上是 final 。如果是可变的,就不能通过编译。例如:

int portNumber = 6666;
Runner r - () -> System.out.println(portNumber);
int portNumber = 8888;
//这是不能通过编译的

你可能会想,为什么要有这种限制呢?第一:实例变量和局部变量有一个关键不同,实例变量在堆中储存,局部变量在栈中储存。如果 Lambda 直接使用局部变量,而且是在一个线程中使用的,则使用 Lambda 的线程,可能会在分配那个变量的线程回收该变量后使用。因此,Java 访问局部变量时,实际是访问它的副本。如果局部变量只赋值一次就能没什么区别了。第二:这一限制不鼓励你使用改变外部变量的典型命令式编程模式。

方法引用

方法引用让你可以重复使用现有方法定义,并像 Lambda 一样传递它们。下面我们借助更新的 Java8 API ,用方法引用写一个排序的例子:

先前:

inventory.sort((Apple a1, Apple a2) -> a1.compareTo(a2.getWeigth()));

以后

//第一个方法引用实例
inventory.sort(comparing(Apple::getWeigth()));

管中窥豹

方法引用可以看作调用特定方法的 Lambda 的一种快捷写法。基本思想是,调用一个方法最好是直接用名称调用它,而不是去描述如何调用它。当时需要使用方法引用时,目标引用放在分隔符 :: 前,方法名称放在后面。例如:Apple::getWeigth 就是引用了 Apple 类中定义的方法 getWeigth。

方法引用主要有三类:

  • 指向静态的方法引用(例如 Integer::parseInt)

  • 指向任意类型实例方法的方法引用(例如 String::length )

  • 指向现有对象的实例方法的方法引用

    下面是上述方法引用和 Lambda 表达式之间的等价:

    // Lambda 
    (args) -> ClassName.staticMethod(args)
    //方法引用
    ClassName::staticMethod
    
    // Lambda 
    (args, rest) -> args.instance(args)
    //方法引用
    ClassName::instanceMethod
    
    // Lambda 
    (args) -> expr.instanceMethod(args)
    //方法引用
    expr::instanceMethod
    

    还有针对构造函数、数组构造函数和父类调用等一些特殊形式的方法引用。如果你感兴趣,可以查阅相关文档。

    构造函数引用

    对于现有构造函数,你可以利用它的名称和关键字 new 来创建一个引用:ClassName::new 。它的功能和指向静态方法的引用类似。

    Supplier<Apple> c = Apple::new;
    Apple a = c.get();
    

    等价于

    Supplier<Apple> c = () -> new Apple();
    Apple a = c.get();
    

    这里就不一一举例了。

    总结

    • Lambda 可以理解为一种匿名函数:它没有名称,但是有参数列表、函数主体、返回类型,可能还会抛出一个异常。
    • Lambda 表达式让你可以简洁的传递代码。
    • 函数是接口就是仅仅声明了一个抽象方法的接口。
    • 只有在接收函数式接口的地方才可以使用 Lambda 表达式。
    • Lambda 表达式允许你直接内联,为函数式接口的抽象方法提供实现,并将整个表达式作为函数式接口的一个实例。
    • Java8 自带了一些常用的函数式接口,放在 java.util.function 包中,包括 Predicate<T>、Function<T, R> 、Supplier<T>、Consumer<T> 等等。
    • 为了避免装箱操作,对Predicate<T> 和 Function<T, R> 等常用接口的原始类型特化:IntPredicate<T>、IntToLongFunction<T, R> 等。
    • 环绕执行模式可以配合 Lambda 提高灵活性和可用性。
    • Lambda 表达式所需的代表类型成为目标类型
    • 方法引用让你重复使用现有的方法实现并之间传递他们。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容

  • 人生一辈子,转眼就走完, 走过了人生坎坷岁月, 饱经了风霜雨雪的洗礼, 品尝了得失成败的滋味, 看淡了人世间世事沧...
    荒漠耕者阅读 262评论 0 0
  • 在生活中,你的角色是子女,是父母,是朋友,是伴侣; 在工作中,你的角色是领导,是同事,是下属; 在社会中,你的角色...
    江月的书妆台阅读 972评论 8 3
  • 初二,回娘家,农历新年初雪。 路上的人已经多了起来,大年初二回娘家的日子,串门返程,尽管雪花飞舞,也阻挡不了回家的...
    M豆小姐阅读 166评论 0 0
  • 打卡!
    桔纱阅读 364评论 0 7