Java函数式编程(一)概述

Java函数式编程(一)

1 什么是函数式编程?

在了解函数编程之前,先来看一段程序,规格如下:

写一段程序,从一个文件中统计关键字的个数,将结果写入另一个文件。

很简单对吗,我想到的实现是这样的:

package com.qupeng.fp.demo;

import java.io.*;

public class WordCounter {
    public static void main(String[] args) {
        String inputFilePath = "C:\\story.txt";
        String outputFilePath = "C:\\result.txt";
        WordCounter wordCounter = new WordCounter();
        System.out.println(wordCounter.count("streams", inputFilePath, outputFilePath));
    }
    public long count(String keyWord, String inputFilePath, String outputFilePath) {
        long count = 0;
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(inputFilePath))) {
            String line = bufferedReader.readLine();
            while(null != line) {
                if (line.contains(keyWord)) {
                    count++;
                }
                line = bufferedReader.readLine();
            }
        } catch(FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (FileWriter fileWriter = new FileWriter(outputFilePath)) {
            fileWriter.write(String.valueOf(count));
        } catch (IOException e) {
            e.printStackTrace();
        }

        return count ;
    }
}

完美运行。那么接下来要让你用函数式编程来重写这段代码呢?怎么写?什么叫函数式编程?它有那些特征?
下面是我想到的实现方式:

package com.qupeng.fp.demo;

import java.io.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class WordCounter {
    public static void main(String[] args) {
        String inputFilePath = "C:\\story.txt";
        String outputFilePath = "C:\\result.txt";
        WordCounter wordCounter = new WordCounter();
        System.out.println(wordCounter.countFP("streams", inputFilePath, outputFilePath));
    }

    public long countFP(String keyWord, String inputFilePath, String outputFilePath) {
        long[] count = {0};
        try {
            // 1.链式构造 2. 内部迭代 3. Lambda表达式 4.惰性求值
            findFP(inputFilePath, count, lines -> lines.filter(line -> line.contains(keyWord)).collect(Collectors.counting())) .writeTo(outputFilePath, count[0]); 
        } catch (IOException e) {
            e.printStackTrace();
        }
        return count[0];
    }

    // 5.高阶函数 6.函数接口
    public WordCounter findFP (String filePath, long[] count, Function<Stream<String>, Long> funcCounting) throws IOException {
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))) {
            count[0] = funcCounting.apply(bufferedReader.lines().parallel()); // 7. 并行计算
        }

        return this;
    }

    public long writeTo(String filePath, long count) throws IOException {
        try (FileWriter fileWriter = new FileWriter(filePath)) {
            fileWriter.write(String.valueOf(count));
        }

        return count;
    }
}

在这段程序中用到了函数编程的以下特性:

  1. 链式构造
  2. 内部迭代
  3. Lambda表达式
  4. 惰性求值
  5. 高阶函数
  6. 函数接口
  7. 并行计算

如果你非常清楚这些概念,那么可以关闭网页。如果

在计算机科学中,函数式编程是一种通过应用和组合函数来构建程序的编程范式。它是一种声明式编程范式,其中函数定义是将值映射到其他值的表达式树,而不是更新程序运行状态的一系列命令式语句。
既然函数式编程是一种编程范式,要理解什么是函数式编程,首先来了解编程范式。

1.1 编程范式(programming paradigm)

编程范式编程范型(Programming paradigm),是一类典型的编程风格,是指从事软件工程的一类典型的风格(可以对照方法学)。

编程范式提供了(同时决定了)程序员对程序执行的看法。例如,在面向对象编程中,程序员认为程序是一系列相互作用的对象,而在函数式编程中一个程序会被看作是一个无状态的函数计算的序列。

正如软件工程中不同的群体会提倡不同的“方法学”一样,不同的编程语言也会提倡不同的“编程范式”。一些语言是专门为某个特定的范型设计的(如Smalltalk和Java支持面向对象编程,而Haskell和Scheme则支持函数式编程),同时还有另一些语言支持多种范型(如Ruby、Common Lisp、Python和Oz)。

很多编程范式已经被熟知他们禁止使用哪些技术,同时允许使用哪些。例如,纯粹的函数式编程不允许有副作用;结构化编程不允许使用goto。可能是因为这个原因,新的范型常常被那些习惯于较早的风格的人认为是教条主义或过分严格。然而,这样避免某些技术反而更加证明了关于程序正确性——或仅仅是理解它的行为——的法则,而不用限制程序语言的一般性。

编程范式和编程语言之间的关系可能十分复杂,由于一个编程语言可以支持多种范型。例如,C++设计时,支持过程化编程、面向对象编程以及泛型编程。然而,设计师和程序员们要考虑如何使用这些范型元素来构建一个程序。一个人可以用C++写出一个完全过程化的程序,另一个人也可以用C++写出一个纯粹的面向对象程序,甚至还有人可以写出杂揉了两种范型的程序。
函数式编程是一种编程范式,看待问题的一种方式,每一个函数都是为了用小函数组织成更大的函数,函数的参数也是函数,函数返回的也是函数。 而我们常见的编程范式有命令式编程、函数式编程、逻辑式编程。 常见的面向对象编程是也是一种命令式编程。

编程语言的几种编程范式:

  • 过程式
    所谓程序就是一连串告诉计算机怎样处理程序输入的指令。C、Pascal 甚至 Unix shells 都是过程式语言。

  • 声明式
    SQL 可能是比较熟悉的声明式语言了。 一个 SQL 查询语句描述了你想要检索的数据集,并且 SQL 引擎会决定是扫描整张表还是使用索引,应该先执行哪些子句等等。

  • 面向对象
    程序会操作一组对象。 对象拥有内部状态,并能够以某种方式支持请求和修改这个内部状态的方法。Smalltalk 和 Java 都是面向对象的语言。 C++ 和 Python 支持面向对象编程,但并不强制使用面向对象特性。

  • 函数式
    函数式编程则将一个问题分解成一系列函数。 理想情况下,函数只接受输入并输出结果,对一个给定的输入也不会有影响输出的内部状态。

而函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。

面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。

函数式编程可以被认为是面向对象编程的对立面。一些语言的设计者选择强调一种特定的编程方式。 这通常会让以不同的方式来编写程序变得困难。其他多范式语言则支持几种不同的编程方式。 Lisp,C++ 和 Python 都是多范式语言;使用这些语言,你可以编写主要为过程式,面向对象或者函数式的程序和函数库。 在大型程序中,不同的部分可能会采用不同的方式编写;比如 GUI 可能是面向对象的而处理逻辑则是过程式或者函数式。

1.2 理解函数式编程

函数式编程的核心是:在思考问题时,使用不可变值纯函数,函数对一个值进行处理,映射成另一个值。

在函数式程序里,输入会流经一系列函数。每个函数接受输入并输出结果。函数式风格反对使用带有副作用的函数,这些副作用会修改内部状态,或者引起一些无法体现在函数的返回值中的变化。完全不产生副作用的函数被称作“纯函数”。消除副作用意味着不能使用随程序运行而更新的数据结构;每个函数的输出必须只依赖于输入。

函数式风格的程序并不会极端到消除所有 I/O 或者赋值的程度;相反,他们会提供像函数式一样的接口,但会在内部使用非函数式的特性。比如,函数的实现仍然会使用局部变量,但不会修改全局变量或者有其他副作用。

1.3 函数式编程的特点

1.3.1 函数是一等公民

所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

1.3.2 只用表达式(expression),不用语句(statement)

表达式(expression)是一个单纯的运算过程,总是有返回值;语句(statement)是执行某种操作,没有返回值。也就是说,每一步都是单纯的运算,而且都有返回值。

函数式编程要求,只使用表达式,不使用语句。
原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以就被排斥在外。当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

wiki 中表达式(计算机科学):
表达式是一个或多个常量、变量、运算符和函数的组合,编程语言根据其特定的优先级和关联规则解释它们,并计算它们来生成另外一个值。这个过程,在数学表达式中被称为求值。
在简单设置中,得到的值通常是基本类型的一种,例如数字、字符串、布尔值、复杂数据类型或者其他。

wiki 中语句(计算机科学):
语句是命令式编程语言的一个语法单元,表示程序要执行的操作。程序是有一个或多个语句序列,语句可能包含内部组件(例如表达式)。
语句决定了程序的外观,编程语言表现为它们使用的语句类型(例如花括号),许多语句由标识符 if、while 或者 repeat 引入,语句关键字是保留的。
大多数语言中,语句和表达式的区别在于,语句不返回结果,执行语句只是为了产生副作用,而表达式总是返回结果,而通常没有副作用。
在命令式编程语言中,Algol 68 是少数几种语句可以返回结果的语言之一。在混合了命令式和函数式风格的语言中,如 Lisp 家族,表达式和语句之间没有区别。在纯函数式编程中,没有语句,一切都是表达式。
这种区别经常表现为:执行语句,而计算表达式。这可以在某些语言中的 exec 和 eval 函数中找到:在 Python 中,exec 应用于语句,eval 应用于表达式。

1.3.3 使用纯函数

纯函数:一个函数的返回结果只依赖其参数,并且执行过程中没有副作用

  • 返回值只和函数参数有关,与外部无关。无论外部发生什么样的变化,函数的返回值都不会改变。
    就是说,表达式的值不依赖于可以改变值的全局状态。这使您可以从形式上推断程序行为,因为表达式的意义只取决于其子表达式而不是计算顺序或者其他表达式的副作用。这有助于验证正确性、简化算法,甚至有助于找出优化它的方法。
  • 函数执行的过程中对外部产生了可观察的变化,我们就说函数产生了副作用。
    函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
    在其他类型的语言中,变量往往用来保存"状态"(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。
    副作用副作用是修改系统状态的语言结构。因为函数式编程语言不包含任何赋值语句,变量值一旦被指派就永远不会改变。而且,调用函数只会计算出结果 ── 不会出现其他效果。因此,函数式编程语言没有副作用。
    例如修改外部的变量、调用DOM API修改页面,发送Ajax请求、调用window.reload刷新浏览器甚至是console.log打印数据,都是副作用。

1.3.4 闭包和高阶函数

函数编程支持函数作为第一类对象,有时称为闭包或者仿函数(functor)对象。实质上,闭包是起函数的作用并可以像对象一样操作的对象。与此类似,FP 语言支持高阶函数。高阶函数可以用另一个函数(间接地,用一个表达式) 作为其输入参数,在某些情况下,它甚至返回一个函数作为其输出参数。这两种结构结合在一起使得可以用优雅的方式进行模块化编程,这是使用 FP 的最大好处。

1.3.5 惰性计算

除了高阶函数和仿函数(或闭包)的概念,FP 还引入了惰性计算的概念。在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。延迟的计算使您可以编写可能潜在地生成无穷输出的函数。因为不会计算多于程序的其余部分所需要的值,所以不需要担心由无穷计算所导致的 out-of-memory 错误。一个惰性计算的例子是生成无穷 Fibonacci 列表的函数,但是对第n个Fibonacci 数的计算相当于只是从可能的无穷列表中提取一项。

1.3.6 递归做为控制流程的机制

FP 还有一个特点是用递归做为控制流程的机制。例如,Lisp 处理的列表定义为在头元素后面有子列表,这种表示法使得它自己自然地对更小的子列表不断递归。

2 函数式编程的优点

  1. 代码简洁,开发快速
    函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。
  2. 接近自然语言,易于理解
    函数式编程的自由度很高,可以写出很接近自然语言的代码。
  3. 更方便的代码管理
    函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。
  4. 易于"并发编程"
    函数式编程不需要考虑"死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。
  5. 代码的热升级
    函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。Erlang语言早就证明了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级当然是不能停机的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值