DIY主题讨论7:Lambda表达式

【DIY主题讨论:Lambda表达式】

函数式编程是什么

函数式编程,是一种使用函数进行编程的方式,一个“函数”对应于一个数学函数:它接受零个或多个参数,生成一个或多个结果,并且不会有任何副作用,函数式函数无论在何处、何时、何地对于同样的输入总会返回相同的结果。

一、函数式编程优劣势对比

匿名类与Lambda表达式

代码简洁,相较于匿名内部类,Lambda表达式大大简化了代码量,代码可读性也会更好

Runnable r1 = new Runnable(){
	public void run(){
		System.out.println("Hello");
	}
}
Runnable r2 = () -> System.out.println("hello");

局限

  1. 匿名类与lambda表达式中的this和super含义是不同的,在匿名类中,this代表自身,而lambda代表的是包含类。
  2. 匿名类可以屏蔽包含类的变量,而lambda不能,但是与包含类使用相同的变量,变量就容易产生歧义,难于理解。
int a = 10;
Runnable r1 = () -> {
//   int a = 10;  // 编译错误
     int a1 = 10;
     System.out.println(a1);
};
  1. 在涉及重载的方法,Lambda表达式可能会导致模棱两可,但可以通过强制类型转换来解决。
interface Task {
    public void execute();
}
    
public static void doSomething(Runnable r){
    r.run();
};

public static void doSomething(Task r){
    r.execute();
};

public static void main(String[] args) {
    // Error:(19, 9) java: 对doSomething的引用不明确
	// doSomething(() -> System.out.println("1234"));
    doSomething((Task) () -> System.out.println("1234"));
 }       
        
行为参数化

Lambda表达式引入了将方法作为参数传递的能力,在环绕场景下(一个方法只有中间部分逻辑不一样),增加了方法的复用,减少代码冗余。结合泛型理解,泛型使类或方法可以复用于更多的变量类型, 而函数式则进一步拓展了方法的复用性。
局限

  1. 提供行为参数化后,如果行为较为复杂,则很难一眼看出行为的含义,这时候就会降低代码的可读性,相较而言,命名规范的函数则有见名知意的好处。
无副作用纯函数,引用透明

所谓共享数据就是数据可能被多个方法读取更新,在并发使用数据的时候必须通过上锁来确保线程安全。函数式编程所倡导的避免共享可变数据,只要参数确定就一定会返回确定结果,增加程序的可控性,不用考虑复杂易错的锁机制,使并行更加容易,充分利用计算机多核优势。

Alt text
为了维持不可变性,“函数式”的函数或者方法都只能修改本地变量,并且它引用的对象都应是不可变对象.。

局限

  1. 为了确保避免共享可变数据引入——增加定义变量与赋值——增大了空间的使用
异常

函数式要求函数或者方法不应抛出任何异常,因为一旦抛出异常,结果就被终止了;类比于数学函数,传入一个合法的参数,一定会返回一个确定的结果。在不使用异常的情况下,Java8引入了Optional< T>类型来承载异常情况,如果异常不能返回结果则返回一个空的optional对象。

声明式编程

经典的面向对象编程我们专注于如何实现,思维模式为:“首先做这个,紧接着更新那个,然后……”,面向对象是抽象对象、对象之间的交互;而函数式编程更关注与要做什么,Stream流是典型的应用,采用这种“要做什么”风格的编程就是声明式编程,编程者考虑的是指定规则,而由系统或者封装来觉得如何实现这个目标。这样带来的好处是让代码更加接近于问题陈述

习惯于声明式编程思维,我们可以更容易的利用化归思想,将复杂问题拆分为若干小问题逐步解决,自顶向下,开始更加关注于函数的输入以及输出结果,而不是过早的考虑如何做、修改哪些东西。

高阶函数与科里化

满足接受一个或多个函数作为参数或返回结果是一个函数的函数都是高阶函数,高阶函数提供了链式调用的功能。

科里化是一种将具备2个参数(比如,x和y)的函数f转化为使用一个参数的函数g,并且这个函数的返回值也是一个函数,它可以作为新的函数的一个参数。后者返回值与初始函数返回值,f(x,y)=(g(x))y

高阶函数与科里化也使声明式编程可以更加运用自如。

延迟计算与惰性求值

延迟计算以Stream为例,向Stream发起的一系列中间操作会先被一一保存起来,直到发起一个终端操作(Stream分为中间操作和中端操作,中间操作返回一个Stream,终端操作从流水线生成结果),才会进行实际的计算。延迟计算与惰性求值使得代码具备了巨大的优化潜能。支持惰性求值的编译器会像数学家看待代数表达式那样看待函数式程序:抵消相同项从而避免执行无谓的代码,优化代码的执行顺序从而实现更高的执行效率甚至是减少错误。
局限

  1. 惰性求值最大的问题还是惰性,现实世界中很多问题还是需要严格求值的,需要严格的顺序执行,如System.nextLine()每次会读取下一行记录
测试与调试

许多同学提到函数式程序难调试,个人觉得并不是因为难调试,而是我们要掌握调试的方法和工具

  1. 单元测试:因为函数式程序无副作用的特性使得单元测试更加容易,唯一需要做的就是传递一些可以代表边界条件的参数给这些函数并返回确定的结果,并且不会受其他因素干扰,如调用顺序、外部状态干扰等。
    Lambda没有函数名,的确带来了测试困难,这时可以借助某个字段访问Lambda函数来测试函数内封装的逻辑,可能又会问每个表达式定义一个变量做测试也太麻烦了,从声明式编程的角度出发,我们应该关注的是一个方法的可靠性,每个lambda仅仅是函数的实现细节,当放在函数内整体测试。
  2. Java Stream Debugger
    工欲善其事必先利其器,推荐个调试插件Java Stream Debugger,可以以可视化的形式展现stream筛选过滤的过程。
    Alt text
  3. 日志输出
    使用peek方法可以在stream输出当前执行的元素,便于判断问题。
List<People> list = peopleList.stream()
            .peek(x -> System.out.println("frist:"+x))
            .filter(item -> item != null)
            .peek(x -> System.out.println("second:"+x))
            .filter(item -> item.getAge() > 5)
            .peek(x -> System.out.println("three:"+x))
            .collect(Collectors.toList());
性能
  1. 在Java7引入了InvokeDynamic指令,用于支持在JVM上运行动态类型语言,Lambda表达式使用InvokeDynamic指令使表达式转化为字节码推迟到了运行时,避免了静态初始化,生成大量的匿名类,由此带来的问题,函数首次运行需要先进行编译,也就造成首次运行可能会占用较长时间,因此需要注意预热。
  2. 包装类型转换往往不易发现,需要重点关注
  3. 总的来说lambda不会对程序性能带来提升,甚至有可能性能下降,但是我们还是得拥抱它,因为它在多核并行计算、代码可读性、可拓展行上的优势足以抵消它降低的性能。
其他问题:
  1. 增加了使用门槛,但是工欲善其事必先利其器,作为一个合格Java程序猿本身就应具备持续的学习能力

二、Stream流的哪一个方法最有价值,为什么?

同学们为自己心目中最有价值的Stream流方法进行了投票,投票结果来看最受欢迎的三个方法是filter、map、collect,filter主要因为可以进行条件过滤,map方法则可以映射每个元素生成新的元素,collect则是最后收集元素必不可少的一环,这三个方法无疑是Stream流操作最常用的方法。

Alt text

简单讲一下流的定义:从支持数据处理的源生成的元素序列

  • 元素序列——流提供了可以访问特定元素类型的一组有序值的接口。流的目的在于表达计算,比如前面见到的 filter、sorted和map。
  • ——流会使用一个提供数据的源,如集合、数组或输入/输出资源。
  • 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中 的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。

流的另外两个特点

  • 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大
    的流水线。流水线背后其实是一种建造者模式
  • 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

流的使用一般包括三件事:

  • 一个数据源(如集合)来执行一个查询;
  • 一个中间操作链,形成一条流的流水线;
  • 一个终端操作,执行流水线,并能生成结果。

parallelStream与parallel区别
parallelStream是Collection接口定义的方法,在stream构造时传入参数使用parallel生成并行流。parallel是BaseStream的一个方法,能够将一个顺序流转化为一个并行流,因此在执行parallel前可以以顺序流先进行预处理。
如:

public static long parallelSum(long n) {
        return Stream.iterate(1L, i -> i + 1)
                     .limit(n)
                     .parallel()      // 转化为并行流
                     .reduce(0L, Long::sum);
}

Stream流方法的使用Alt text

参考文章:
傻瓜函数式编程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值