如何更快乐的使用Java 8中的Lambda特性

前言

Java 8 的 Lambda 特性较之于先前的泛型加入更能鼓舞人心的,我对 Lambda 的理解是它得以让 Java 以函数式思维的方式来写代码。而写出的代码是否是函数式,并不单纯在包含了多少 Lambda 表达式,而在思维,要神似。

实际中看过一些代码,为了 Lambda 表达式而 Lambda(函数式),有一种少年不识愁滋味,为赋新词强说愁的味道。从而致使原本一个简单的方调用硬生生的要显式的用类如 apply(), accept(obj) 等形式。不仅造成代码可读性差,且可测试性也变坏了。

为什么说的是快乐的使用 Java 8 的 Lambda 呢?我窃以为第一个念头声明 Lambda 表达式为实例/类变量(像本文第一段代码那样),而不是方法的,一定会觉得如此使用方式很快乐的。所谓独乐乐,不如众乐乐;独乐乐,众不乐定然是更大的快乐; 更极致一些,不管什么时候必须是:我快乐,所以你也快乐。

一方面也在于 Java 还没有进化到 JavaScript 或  Scala 那样的水平,JavaScript 的函数类型变量,不一定要用 apply 或 call, 直接括号就能实现方法调用。Scala 的函数类型用括号调用也会自动匹配到 apply 或 update 等方法上去。

看下面的样本代码

public class Account {

 public BiFunction<String, String, String> fullName = (firstName, lastName) -> {
  //some logic, i.e. logics of fullName in different countries
  return firstName + " " + lastName;
 };

 public String getName() {
  String firstName = "Speaker";
  String lastName = "Wolf";
  return fullName.apply(firstName, lastName);
 }
}

上面的 fullName Lambda 表达式看起来就有点别扭,完全可以写成一个普通方法

 public String makeFullName(String firstName, String lastName) {
  //return something with logics
 }

那么调用起来只需要简单的

makeFullName(firstName, lastName)

那么此例中把简单方法写成一个 Lambda 表达式来调用有什么不友好之处呢?

  • 不利于理解,Lambda 表达式的类型充斥着 Consumer, Function, BiFunction 等太宽泛的声明
  • 参数类型与形参分离在表达式等号两边,不利于一一对应(右方重复一遍参数类型更不可取),真正的返回值也不明了
  • 调用时更得多余的 get(), accept(obj), apply(obj1, obj2) 那样的方法
  • 既然有逻辑,就应该有测试,Lambda 表达式虽是一个变量也不例如,测试时也不得用 apply 那样的调用
  • Lambda 表达式为变量的形式,可能会随每一个对象实例有一单独的拷贝。当然声明为静态可以避免。
  • 重构时更需大动干戈,比如前面的例子还要考虑 middleName 的情况,表达式要更动为
public TriFunction<String, String, String, String> fullName = (firstName, middleName, lastName) -> {
 //.......
}

JDK 中还没有 TriFunction, 还得自己创造,不同数量的参数都得更新 Lambda 表达式的类型。如果是一个普通方法重构起来就方便多了,跟多一个人多一副碗筷一样。

解释上面第 #5 条,对于方法,实现代码在 JVM 中只有一份,而 Lambda 实例变量如果不捕获外部变量的话,与方法是一样的,例如前面的 Account 为例

Account account1 = new Account();
Account account2 = new Account();
System.out.println(account1.fullName == account2.fullName); //true

但是 Lambda 表达式需捕获外部变量时,例如

private String suffix = "Sir";
public BiFunction<String, String, String> fullName = (firstName, lastName) -> {
 return firstName + " " + lastName + " " + suffix;
};

.......
Account account1 = new Account();
Account account2 = new Account();
account1.fullName == account2.fullName; //就是 false 了, 而如果 suffix 是一个静态的变量时这个等式又是 true 了

那么新建的两个 Account 对象的 fullName 属性就不是同一个了。因为 Lambda 需要捕获外部一个不确定的值,所以它也随宿主实例也变。

难道不应该用 Lambda 表达式变量,那倒不是,如果一个方法接受的是一个函数,如

public String getName(BiFunction<String, String, String> builder) {
 return builder.apply(firstName, lastName);
}

那么是可以声明一个 Lambda 表达式变量,来传入。不过这种情况下用方法引用还是更方便些,方法的测试总是比 Lambda 表达式的测试容易。

String name = getName(this::makeFullName);

个人习惯,一般需要 Lambda 表达式变量时基本是声明为局部变量,或是调用接受函数参数的方法时以内联的方法书写,像

String name = getName((firstName, lastName) -> {
 //logics
 return ......
});

对于使用方法引用方式的重构也不难,getName() 的参数类型变为 TriFunction, makeFullName() 方法再加一个参数就行, 调用形式仍然不变,还是

String name = getName(this::makeFullName);

如果引用的方法是别人写的也不用慌,无须总去创建一样的方法签名来强型上方法引用,也可以和改 Lambda 实现代码一样的方式比改动,如下

String name = getName((firstName, lastName) ->
 makeFullName(firstName, lastName) + " " + suffix
)

本人希望的是,对函数的 apply(), accept(obj) 这样的显式调用应该是框架设计实现的职责,对框架使用者应该透明,或者说是隐藏背后的细节,只管传入需要的函数类型或方法引用。如果函数实现需要共享的话,写成方法更优于一个 Lambda 表达式,方法容易单独测试。特别是用 Mockito 捕获到了一个传入某个方法的 Lambda  表达式实例时,不那么好验证它的内部实现。

小结一下:

  • 函数式思维最关键应该是 Data In, Data Out, 编程语言 Lambda 特性可以促使我们达成这一目的; 但不是代码中有了  Lambda 表达就是函数式风格。
  • 其次代码的首先是人阅读,其次才是机器,所以它应该表达直截,明了,很强的可读性与可测试性。
  • 具体讲如何快乐使用 Java 8 的 Lambda 呢,仅代表本人想法,可以用内联式,或方法引用,或局部的 Lambda 表达式变量,最后才是实例/类的 Lambda 表达式变量。

补充一个例子,在方法体中重复声明完全相同的不捕获任何外部变量的 Lambda 表达式都是新的实例

 Consumer<String> f1 = a -> System.out.println(a);
 Consumer<String> f2 = a -> System.out.println(a);
 System.out.println(f1 == f2); //false

以上测试在 Java 8 平台上进行的。

lambda表达式优劣

lambda表达式有优点也有缺点,优点在于大大简化代码行数,使代码在一定程度上变的简洁干净,但是同样的,这可能也会是一个缺点,由于省略了太多东西,代码可读性有可能在一定程度上会降低,这个完全取决于你使用lambda表达式的位置所设计的API是否被你的代码的其他阅读者所熟悉。另外的优点,也是lambda表达式比较显眼的优点就是对外部定义的局部变量的使用更加灵活,想象一种极端情况,你的代码中有地方需要接口回调套接口回调,有可能套了好几层,虽然这种情况出现的概率比较低,但是一旦出现这种代码,lambda表达式的这个优点就到了大显身手的时机。虽然我说了,lambda表达式能用的地方非常有限,但是不得不否认,接口中只有一个抽象方法这种情况在接口回调中发生的概率绝对比接口中有多个抽象方法的概率高的多,所以,虽然使用情况很单一,但是能用到的次数却足够的多,如果你决定用lambda表达式替换你项目中接口回调的传统写法,你会发现,这样的情况非常多。

总而言之,接口回调和lambda表达式这两种写法各有优劣,java 8在出现lambda表达式以后不代表原先的写法不能再用了,所以如何选择适合项目的写法,全看各位开发者如何自己选择,现在多了一种写法可选,总归是一件好事。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Java8中Lambda表达式使用和Stream API详解

    前言 Java8 的新特性:Lambda表达式.强大的 Stream API.全新时间日期 API.ConcurrentHashMap.MetaSpace.总得来说,Java8 的新特性使 Java 的运行速度更快.代码更少.便于并行.最大化减少空指针异常. 0x00. 前置数据 private List<People> peoples = null; @BeforeEach void before () { peoples = new ArrayList<>(); peoples

  • 详解Java中的Lambda表达式

    简介 Lambda表达式是Java SE 8中一个重要的新特性.lambda表达式允许你通过表达式来代替功能接口. lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块). Lambda表达式还增强了集合库. Java SE 8添加了2个对集合数据进行批量操作的包: java.util.function 包以及java.util.stream 包. 流(stream)就如同迭代器(iterator),但附加了许多额外的功能.

  • java使用lambda表达式对List集合进行操作技巧(JDK1.8)

    具体代码如下所示: import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; public class LambdaStudy { public static void main(String[] args) { //初始化list集合 List<String> list = new ArrayList&l

  • java中Lambda常用场景代码实例

    本文实例为大家分享了java中Lambda常用场景的具体代码,供大家参考,具体内容如下 public class test18 { /** * lambda表达式的常用场景 */ @Test public void test() { List<String> list_one = new ArrayList<>(); list_one.add("NIKE"); list_one.add("Addidas"); /** * 用在匿名内部类里简写

  • Java8简单了解Lambda表达式与函数式接口

    Java8被称作Java史上变化最大的一个版本.其中包含很多重要的新特性,最核心的就是增加了Lambda表达式和StreamAPI.这两者也可以结合在一起使用.首先来看下什么是Lambda表达式. 使用Lambda表达式不仅让代码变的简单.而且可读.最重要的是代码量也随之减少很多.然而,在某种程度上,这些功能在Scala等这些JVM语言里已经被广泛使用. 并不奇怪,Scala社区是难以置信的,因为许多Java 8里的内容看起来就像是从Scala里搬过来的.在某种程度上,Java 8的语法要比Sc

  • Java8与Scala中的Lambda表达式深入讲解

    前言 最近几年Lambda表达式风靡于编程界.很多现代编程语言都把它作为函数式编程的基本组成部分.基于JVM的编程语言如Scala.Groovy及Clojure把它作为关键部分集成在语言中.而如今,(最终)Java 8也加入了这个有趣的行列. Java8 终于要支持Lambda表达式!自2009年以来Lambda表达式已经在Lambda项目中被支持.在那时候,Lambda表达式仍被称为Java闭包.在我们进入一些代码示例以前,先来解释下为什么Lambda表达式在Java程序员中广受欢迎. 1.为

  • 深入学习 Java 中的 Lambda

    前言 我花了相当多的阅读和编码时间才最终理解Java Lambdas如何在概念上正常工作的.我阅读的大多数教程和介绍都遵循自顶向下的方法,从用例开始,最后以概念性问题结束.在这篇文章中,我想提供一个自下而上的解释,从其他已建立的Java概念中推导出Lambdas的概念. 首先介绍下方法的类型化,这是支持方法作为一流公民的先决条件.基于此,Lambdas的概念是被以匿名类用法的进化和特例提出的.所有这一切都通过实现和使用高阶函数映射来说明. 这篇文章的主要受众是那些已掌握函数式编程基础的人,以及那

  • Java8使用lambda实现Java的尾递归

    前言 本篇介绍的不是什么新知识,而是对前面讲解的一些知识的综合运用.众所周知,递归是解决复杂问题的一个很有效的方式,也是函数式语言的核心,在一些函数式语言中,是没有迭代与while这种概念的,因为此类的循环通通可以用递归来实现,这类语言的编译器都对递归的尾递归形式进行了优化,而Java的编译器并没有这样的优化,本篇就要完成这样一个对于尾递归的优化. 什么是尾递归 本篇将使用递归中最简单的阶乘计算来作为例子 递归实现 /** * 阶乘计算 -- 递归解决 * * @param number 当前阶

  • 如何更快乐的使用Java 8中的Lambda特性

    前言 Java 8 的 Lambda 特性较之于先前的泛型加入更能鼓舞人心的,我对 Lambda 的理解是它得以让 Java 以函数式思维的方式来写代码.而写出的代码是否是函数式,并不单纯在包含了多少 Lambda 表达式,而在思维,要神似. 实际中看过一些代码,为了 Lambda 表达式而 Lambda(函数式),有一种少年不识愁滋味,为赋新词强说愁的味道.从而致使原本一个简单的方调用硬生生的要显式的用类如 apply(), accept(obj) 等形式.不仅造成代码可读性差,且可测试性也变

  • Java编程中使用lambda表达式的奇技淫巧

    为什么使用Lambda表达式 先看几个例子: 第一个例子,在一个独立的线程中执行某项任务,我们通常这么实现: class Worker implements Runnable { public void run() { for (int i = 0; i < 100; i++) doWork(); } ... } Worker w = new Worker(); new Thread(w).start(); 第二个例子,自定义字符串比较的方法(通过字符串长度),一般这么做: class Leng

  • 使用Java 8中的Lambda表达式实现工厂模式

    前言 工厂模式是面向对象设计模式中大家最为熟知的设计模式之一.传统的实现方式大家都在熟悉不过了,今天将向大家介绍使用Java8 Lambda 表达式更加优雅的实现工厂模式. 封面 工厂模式在java中最常用的设计模式之一,它提供了一种很好的实例化对象的方法,是替代new操作的一种模式常用的方式.工厂设计模式可以让你实例化对象的逻辑不用暴露给客户端. 在下面的文章中我将给出使用传统的代码实现工厂模式的一个例子,然后再使用 Java8 Lambada 方式重新实现 一个例子 首先我将创建一个 Sha

  • java多线程中的volatile和synchronized用法分析

    本文实例分析了java多线程中的volatile和synchronized用法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: package com.chzhao; public class Volatiletest extends Thread { private static int count = 0; public void run() {         count++;     } public static void main(String[] args) {  

  • java Lucene 中自定义排序的实现

    Lucene中的自定义排序功能和Java集合中的自定义排序的实现方法差不多,都要实现一下比较接口. 在Java中只要实现Comparable接口就可以了.但是在Lucene中要实现SortComparatorSource接口和ScoreDocComparator接口.在了解具体实现方法之前先来看看这两个接口的定义吧. SortComparatorSource接口的功能是返回一个用来排序ScoreDocs的comparator(Expert: returns a comparator for so

  • Java编程中的一些常见问题汇总

    本文列举了我在周围同事的Java代码中看到的一些比较典型的错误.显然,静态代码分析(我们团队用的是qulice)不可能发现所有的问题,这也是为什么我要在这里列出它们的原因. 如果你觉得少了什么,请不吝赐教,我会很乐意把它们加上. 下面列出的所有这些错误基本都与面向对象编程有关,尤其是Java的OOP. 类名 读下这篇短文"什么是对象".类应该是真实生活中的一个抽象实体,而不是什么"validators","controller", "m

  • 详解Java正则表达式中Pattern类和Matcher类

    前言 本文将介绍Java正则表达式中的Pattern类与Matcher类.首先我们要清楚指定为字符串的正则表达式必须首先被编译为pattern类的实例.因此如何更好的了解这两个类,是编程人员必须知道的. 以下我们就分别来看看这两个类: 一.捕获组的概念 捕获组可以通过从左到右计算其开括号来编号,编号是从1 开始的.例如,在表达式 ((A)(B(C)))中,存在四个这样的组: 1 ((A)(B(C))) 2 (A) 3 (B(C)) 4 (C) 组零始终代表整个表达式. 以 (?) 开头的组是纯的

  • Java 8中日期和时间的处理方法

    Java 8新增了LocalDate和LocalTime接口,为什么要搞一套全新的处理日期和时间的API?因为旧的java.util.Date实在是太难用了. java.util.Date月份从0开始,一月是0,十二月是11,变态吧!java.time.LocalDate月份和星期都改成了enum,就不可能再用错了. java.util.Date和SimpleDateFormatter都不是线程安全的,而LocalDate和LocalTime和最基本的String一样,是不变类型,不但线程安全,

  • 深入讲解Java 9中的九个新特性

    本文主要跟大家分享了Java 9中的九个新特性,对大家具有一定的参考学习价值,下面来看看详细的介绍: 一. Java 平台级模块系统 Java 9 的定义功能是一套全新的模块系统.当代码库越来越大,创建复杂,盘根错节的"意大利面条式代码"的几率呈指数级的增长.这时候就得面对两个基础的问题: 很难真正地对代码进行封装, 而系统并没有对不同部分(也就是 JAR 文件)之间的依赖关系有个明确的概念.每一个公共类都可以被类路径之下任何其它的公共类所访问到, 这样就会导致无意中使用了并不想被公开

  • Java接口中尽量避免使用数组

    如果你发现在一个接口使用有如下定义方法: public String[] getParameters(); 那么你应该认真反思.数组不仅仅老式,而且我们有合理的理由避免暴露它们.在这篇文章中,我将试图总结在Java API中使用数组的缺陷.首先从最出人意料的一个例子开始. 数组导致性能不佳 你可能认为使用数组是最快速的,因为数组是大多数collection实现的底层数据结构.使用一个纯数组怎么会比使用一个包含数组的对象性能更低? 让我们先从这个看起来很熟悉的普遍的习惯用法开始: public S

随机推荐