作者:DemonsI

my.oschina.net/demons99/blog/2223079

为什么要使用函数式编程

函数式编程更多时候是一种编程的思维方式,是种方法论。函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做。说白了,函数式编程是基于某种语法或调用API去进行编程。

例如,我们现在需要从一组数字中,找出最小的那个数字,若使用用命令式编程实现这个需求的话,那么所编写的代码如下:

public static void main(String[] args) {    int[] nums = new int[]{1, 2, 3, 4, 5, 6, 7, 8};

    int min = Integer.MAX_VALUE;    for (int num : nums) {        if (num             min = num;        }    }    System.out.println(min);}

而使用函数式编程进行实现的话,所编写的代码如下:

public static void main(String[] args) {    int[] nums = new int[]{1, 2, 3, 4, 5, 6, 7, 8};

    int min = IntStream.of(nums).min().getAsInt();    System.out.println(min);}

从以上的两个例子中,可以看出,命令式编程需要自己去实现具体的逻辑细节。而函数式编程则是调用API完成需求的实现,将原本命令式的代码写成一系列嵌套的函数调用,在函数式编程下显得代码更简洁、易懂,这就是为什么要使用函数式编程的原因之一。所以才说函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做,是一种思维的转变。

说到函数式编程就不得不提一下lambda表达式,它是函数式编程的基础。在Java还不支持lambda表达式时,我们需要创建一个线程的话,需要编写如下代码:

public static void main(String[] args) {    new Thread(new Runnable() {        @Override        public void run() {            System.out.println("running");        }    }).start();}

而使用lambda表达式一句代码就能完成线程的创建,lambda强调了函数的输入输出,隐藏了过程的细节,并且可以接受函数当作输入(参数)和输出(返回值):

public static void main(String[] args) {    new Thread(() -> System.out.println("running")).start();}

注:箭头的左边是输入,右边则是输出

该lambda表达式的作用其实就是返回了Runnable接口的实现对象,这与我们调用某个方法获取实例对象类似,只不过是将实现代码直接写在了lambda表达式里。我们可以做个简单的对比:

public static void main(String[] args) {    Runnable runnable1 = () -> System.out.println("running");    Runnable runnable2 = RunnableFactory.getInstance();}

JDK8接口新特性

1.函数接口,接口只能有一个需要实现的方法,可以使用@FunctionalInterface 注解进行声明。如下:

@FunctionalInterfaceinterface Interface1 {    int doubleNum(int i);}

使用lambda表达式获取该接口的实现实例的几种写法:

public static void main(String[] args) {    // 最常见的写法    Interface1 i1 = (i) -> i * 2;    Interface1 i2 = i -> i * 2;

    // 可以指定参数类型    Interface1 i3 = (int i) -> i * 2;

    // 若有多行代码可以这么写    Interface1 i4 = (int i) -> {        System.out.println(i);        return i * 2;    };}

2.比较重要的一个接口特性是接口的默认方法,用于提供默认实现。默认方法和普通实现类的方法一样,可以使用this等关键字:

@FunctionalInterfaceinterface Interface1 {    int doubleNum(int i);

    default int add(int x, int y) {        return x + y;    }}

之所以说默认方法这个特性比较重要,是因为我们借助这个特性可以在以前所编写的一些接口上提供默认实现,并且不会影响任何的实现类以及既有的代码。例如我们最熟悉的List接口,在JDK1.2以来List接口就没有改动过任何代码,到了1.8之后才使用这个新特性增加了一些默认实现。这是因为如果没有默认方法的特性的话,修改接口代码带来的影响是巨大的,而有了默认方法后,增加默认实现可以不影响任何的代码。

3.当接口多重继承时,可能会发生默认方法覆盖的问题,这时可以去指定使用哪一个接口的默认方法实现,如下示例:

@FunctionalInterfaceinterface Interface1 {    int doubleNum(int i);

    default int add(int x, int y) {        return x + y;    }}

@FunctionalInterfaceinterface Interface2 {    int doubleNum(int i);

    default int add(int x, int y) {        return x + y;    }}

@FunctionalInterfaceinterface Interface3 extends Interface1, Interface2 {

    @Override    default int add(int x, int y) {        // 指定使用哪一个接口的默认方法实现        return Interface1.super.add(x, y);    }}

函数接口

我们本小节来看看JDK8里自带了哪些重要的函数接口:

可以看到上表中有好几个接口,而其中最常用的是Function接口,它能为我们省去定义一些不必要的函数接口,减少接口的数量。我们使用一个简单的例子演示一下 Function 接口的使用:

import java.text.DecimalFormat;import java.util.function.Function;

class MyMoney {    private final int money;

    public MyMoney(int money) {        this.money = money;    }

    public void printMoney(Function moneyFormat) {        System.out.println("我的存款: " + moneyFormat.apply(this.money));    }}

public class MoneyDemo {    public static void main(String[] args) {        MyMoney me = new MyMoney(99999999);

        Function moneyFormat = i -> new DecimalFormat("#,###").format(i);// 函数接口支持链式操作,例如增加一个字符串        me.printMoney(moneyFormat.andThen(s -> "人民币 " + s));    }}

运行以上例子,控制台输出如下:

我的存款: 人民币 99,999,999

若在这个例子中不使用Function接口的话,则需要自行定义一个函数接口,并且不支持链式操作,如下示例:

import java.text.DecimalFormat;

// 自定义一个函数接口@FunctionalInterfaceinterface IMoneyFormat {    String format(int i);}

class MyMoney {    private final int money;

    public MyMoney(int money) {        this.money = money;    }

    public void printMoney(IMoneyFormat moneyFormat) {        System.out.println("我的存款: " + moneyFormat.format(this.money));    }}

public class MoneyDemo {    public static void main(String[] args) {        MyMoney me = new MyMoney(99999999);

        IMoneyFormat moneyFormat = i -> new DecimalFormat("#,###").format(i);        me.printMoney(moneyFormat);    }}

然后我们再来看看Predicate接口和Consumer接口的使用,如下示例:

public static void main(String[] args) {    // 断言函数接口    Predicate predicate = i -> i > 0;    System.out.println(predicate.test(-9));// 消费函数接口    Consumer consumer = System.out::println;    consumer.accept("这是输入的数据");}

运行以上例子,控制台输出如下:

false这是输入的数据

这些接口一般有对基本类型的封装,使用特定类型的接口就不需要去指定泛型了,如下示例:

public static void main(String[] args) {    // 断言函数接口    IntPredicate intPredicate = i -> i > 0;    System.out.println(intPredicate.test(-9));

    // 消费函数接口    IntConsumer intConsumer = (value) -> System.out.println("输入的数据是:" + value);    intConsumer.accept(123);}

运行以上代码,控制台输出如下:

false输入的数据是:123

有了以上接口示例的铺垫,我们应该对函数接口的使用有了一个初步的了解,接下来我们演示剩下的函数接口使用方式:

public static void main(String[] args) {    // 提供数据接口    Supplier supplier = () -> 10 + 1;    System.out.println("提供的数据是:" + supplier.get());// 一元函数接口    UnaryOperator unaryOperator = i -> i * 2;    System.out.println("计算结果为:" + unaryOperator.apply(10));// 二元函数接口    BinaryOperator binaryOperator = (a, b) -> a * b;    System.out.println("计算结果为:" + binaryOperator.apply(10, 10));}

运行以上代码,控制台输出如下:

提供的数据是:11计算结果为:20计算结果为:100

而BiFunction接口就是比Function接口多了一个输入而已,如下示例:

class MyMoney {    private final int money;    private final String name;

    public MyMoney(int money, String name) {        this.money = money;        this.name = name;    }

    public void printMoney(BiFunction moneyFormat) {        System.out.println(moneyFormat.apply(this.money, this.name));    }}

public class MoneyDemo {    public static void main(String[] args) {        MyMoney me = new MyMoney(99999999, "小明");

        BiFunction moneyFormat = (i, name) -> name + "的存款: " + new DecimalFormat("#,###").format(i);        me.printMoney(moneyFormat);    }}

运行以上代码,控制台输出如下:

小明的存款: 99,999,999

方法引用

在学习了lambda表达式之后,我们通常会使用lambda表达式来创建匿名方法。但有的时候我们仅仅是需要调用一个已存在的方法。如下示例:

Arrays.sort(stringsArray, (s1, s2) -> s1.compareToIgnoreCase(s2));

在jdk8中,我们可以通过一个新特性来简写这段lambda表达式。如下示例:

Arrays.sort(stringsArray, String::compareToIgnoreCase);

这种特性就叫做方法引用(Method Reference)。方法引用的标准形式是:类名::方法名。(注意:只需要写方法名,不需要写括号)。

目前方法引用共有以下四种形式:

下面我们用一个简单的例子来演示一下方法引用的几种写法。首先定义一个实体类:

public class Dog {    private String name = "二哈";    private int food = 10;

    public Dog() {    }

    public Dog(String name) {        this.name = name;    }

    public static void bark(Dog dog) {        System.out.println(dog + "叫了");    }

    public int eat(int num) {        System.out.println("吃了" + num + "斤");        this.food -= num;        return this.food;    }

    @Override    public String toString() {        return this.name;    }}

通过方法引用来调用该实体类中的方法,代码如下:

package org.zero01.example.demo;

import java.util.function.*;

/** * @ProjectName demo * @Author: zeroJun * @Date: 2018/9/21 13:09 * @Description: 方法引用demo */public class MethodRefrenceDemo {

    public static void main(String[] args) {        // 方法引用,调用打印方法        Consumer consumer = System.out::println;        consumer.accept("接收的数据");// 静态方法引用,通过类名即可调用        Consumer consumer2 = Dog::bark;        consumer2.accept(new Dog());// 实例方法引用,通过对象实例进行引用        Dog dog = new Dog();        IntUnaryOperator function = dog::eat;        System.out.println("还剩下" + function.applyAsInt(2) + "斤");// 另一种通过实例方法引用的方式,之所以可以这么干是因为JDK默认会把当前实例传入到非静态方法,参数名为this,参数位置为第一个,所以我们在非静态方法中才能访问this,那么就可以通过BiFunction传入实例对象进行实例方法的引用        Dog dog2 = new Dog();        BiFunction biFunction = Dog::eat;        System.out.println("还剩下" + biFunction.apply(dog2, 2) + "斤");// 无参构造函数的方法引用,类似于静态方法引用,只需要分析好输入输出即可        Supplier supplier = Dog::new;        System.out.println("创建了新对象:" + supplier.get());// 有参构造函数的方法引用        Function function2 = Dog::new;        System.out.println("创建了新对象:" + function2.apply("旺财"));    }}

类型推断

通过以上的例子,我们知道之所以能够使用Lambda表达式的依据是必须有相应的函数接口。这一点跟Java是强类型语言吻合,也就是说你并不能在代码的任何地方任性的写Lambda表达式。实际上Lambda的类型就是对应函数接口的类型。Lambda表达式另一个依据是类型推断机制,在上下文信息足够的情况下,编译器可以推断出参数表的类型,而不需要显式指名。

所以说 Lambda 表达式的类型是从 Lambda 的上下文推断出来的,上下文中 Lambda 表达式需要的类型称为目标类型,如下图所示:


接下来我们使用一个简单的例子,演示一下 Lambda 表达式的几种类型推断,首先定义一个简单的函数接口:

@FunctionalInterfaceinterface IMath {    int add(int x, int y);}

示例代码如下:

public class TypeDemo {

    public static void main(String[] args) {        // 1.通过变量类型定义        IMath iMath = (x, y) -> x + y;

        // 2.数组构建的方式        IMath[] iMaths = {(x, y) -> x + y};

        // 3.强转类型的方式        Object object = (IMath) (x, y) -> x + y;

        // 4.通过方法返回值确定类型        IMath result = createIMathObj();

        // 5.通过方法参数确定类型        test((x, y) -> x + y);

    }

    public static IMath createIMathObj() {        return (x, y) -> x + y;    }

    public static void test(IMath iMath){        return;    }}

变量引用

Lambda表达式类似于实现了指定接口的内部类或者说匿名类,所以在Lambda表达式中引用变量和我们在匿名类中引用变量的规则是一样的。如下示例:

public static void main(String[] args) {    String str = "当前的系统时间戳是: ";    Consumer consumer = s -> System.out.println(str + s);    consumer.accept(System.currentTimeMillis());}

值得一提的是,在JDK1.8之前我们一般会将匿名类里访问的外部变量设置为final,而在JDK1.8里默认会将这个匿名类里访问的外部变量给设置为final。例如我现在改变str变量的值,ide就会提示错误:


至于为什么要将变量设置final,这是因为在Java里没有引用传递,变量都是值传递的。不将变量设置为final的话,如果外部变量的引用被改变了,那么最终得出来的结果就会是错误的。

下面用一组图片简单演示一下值传递与引用传递的区别。以列表为例,当只是值传递时,匿名类里对外部变量的引用是一个值对象:


若此时list变量指向了另一个对象,那么匿名类里引用的还是之前那个值对象,所以我们才需要将其设置为final防止外部变量引用改变:


而如果是引用传递的话,匿名类里对外部变量的引用就不是值对象了,而是指针指向这个外部变量:


所以就算list变量指向了另一个对象,匿名类里的引用也会随着外部变量的引用改变而改变:


级联表达式和柯里化

在函数式编程中,函数既可以接收也可以返回其他函数。函数不再像传统的面向对象编程中一样,只是一个对象的工厂或生成器,它也能够创建和返回另一个函数。返回函数的函数可以变成级联 lambda 表达式,特别值得注意的是代码非常简短。尽管此语法初看起来可能非常陌生,但它有自己的用途。

级联表达式就是多个lambda表达式的组合,这里涉及到一个高阶函数的概念,所谓高阶函数就是一个可以返回函数的函数,如下示例:

// 实现了 x + y 的级联表达式Function> function1 = x -> y -> x + y;System.out.println("计算结果为: " + function1.apply(2).apply(3));  // 计算结果为: 5

这里的 y -< x + y 是作为一个函数返回给上一级表达式,所以第一级表达式的输出是 y -< x + y这个函数,如果使用括号括起来可能会好理解一些:

x -> (y -> x + y)

级联表达式可以实现函数柯里化,简单来说柯里化就是把本来多个参数的函数转换为只有一个参数的函数,如下示例:

Function>> function2 = x -> y -> z -> x + y + z;System.out.println("计算结果为: " + function2.apply(1).apply(2).apply(3));  // 计算结果为: 6

函数柯里化的目的是将函数标准化,函数可灵活组合,方便统一处理等,例如我可以在循环里只需要调用同一个方法,而不需要调用另外的方法就能实现一个数组内元素的求和计算,代码如下:

public static void main(String[] args) {    Function>> f3 = x -> y -> z -> x + y + z;int[] nums = {1, 2, 3};for (int num : nums) {if (f3 instanceof Function) {            Object obj = f3.apply(num);if (obj instanceof Function) {                f3 = (Function) obj;            } else {                System.out.println("调用结束, 结果为: " + obj);  // 调用结束, 结果为: 6            }        }    }}

级联表达式和柯里化一般在实际开发中并不是很常见,所以对其概念稍有理解即可,这里只是简单带过,若对其感兴趣的可以查阅相关资料。

END

推荐好文

强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

为什么MySQL不推荐使用uuid或者雪花id作为主键?

为什么建议大家使用 Linux 开发?爽(外加七个感叹号)

IntelliJ IDEA 15款 神级超级牛逼插件推荐(自用,真的超级牛逼)

炫酷,SpringBoot+Echarts实现用户访问地图可视化(附源码)

记一次由Redis分布式锁造成的重大事故,避免以后踩坑!

十分钟学会使用 Elasticsearch 优雅搭建自己的搜索系统(附源码)

java函数式编程_Java 函数式编程和 lambda 表达式详解相关推荐

  1. java lambda表达式详解_Java8新特性Lambda表达式详解

    课程目标: 通过本课程的学习,详细掌握Java8新特性之Lambda表达式: 适用人群:有Java基础的开发人员: 课程概述:从Java 8出现以来lambda是最重要的特性之一,它可以让我们用简洁流 ...

  2. java拉姆达表达式事例,Java Lambda表达式详解和实例

    简介 Lambda表达式是Java SE 8中一个重要的新特性.lambda表达式允许你通过表达式来代替功能接口. lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体( ...

  3. Java8 Lambda表达式详解手册及实例

    先贩卖一下焦虑,Java8发于2014年3月18日,距离现在已经快6年了,如果你对Java8的新特性还没有应用,甚至还一无所知,那你真得关注公众号"程序新视界",好好系列的学习一下 ...

  4. java lambda表达式详解_Java8新特性:Lambda表达式详解

    在 Java 版本的历次更新迭代中,Java8 是一个特殊的存在,与以往的版本升级不同.我们对 Java8 似乎抱有更大的期待,因为它是 Java5 之后最重要的一次升级,提供了十多个新特性,其中 L ...

  5. 【Java基础】Java Lambda表达式详解

    Lambda 表达式,即函数式编程是 JDK8 的一个新特性,也被称为闭包,Lambda表达式允许把函数作为一个方法的参数,即行为参数化,函数作为参数传递进方法中. Lambda表达式可以取代大部分的 ...

  6. java lambda表达式详解_java8新特性-Lambda表达式的详解(从0开始)

    这几天复习了java8的一些新特性,作为一个从java5以来最具革命性的版本,一直没有来得及总结.本系列文章主要是从<java8实战>总结的.这是第一篇文章主要介绍java8的lambda ...

  7. 什么是lambda表达式?lambda表达式详解

    定义 lambda表达式是一个可以传递的代码块,可以在以后一次或多次执行,也就是它会延迟执行,等待某个时刻或者某个事件发生在执行.它类似于一个方法的实现. 语法 (String f, String s ...

  8. jdk8新特性 lambda表达式详解

    本文主要讲到的内容有: 一- 前言 二- 背景 三- lambda表达式的语法 四- Lambda程序例子 4-1 Runnable Lambda 4-2 Comparator Lambda 4-3 ...

  9. Java 8 Lambda 表达式详解

    版权声明:本文由吴仙杰创作整理,转载请注明出处:https://segmentfault.com/a/1190000009186509 1. 引言 在 Java 8 以前,若我们想要把某些功能传递给某 ...

最新文章

  1. tensorflow,神经网络创建源码
  2. css中的代码图标,认识CSS中字体图标(示例代码)
  3. 生产交接班管理系统的安装设置并下载
  4. Swift调用第三方OC项目
  5. 1137. 第 N 个泰波那契数
  6. Git 原理详解及实用指南
  7. Nginx Parsing HTTP Package、header/post/files/args Sourcecode Analysis
  8. 链表的翻转(迭代法 递归法)
  9. element $prompt 输入类型密码_手把手教程 | 为你支招 安全保存账号和密码的两种方法...
  10. Manecher算法
  11. centos 6.5 安装 phpmyadmin
  12. android adb安装 apk,adb 安装并运行 apk
  13. 高等数学:第一章 函数与极限(6)极限存在准则、两个重要极限
  14. 微信短信显示服务器解包异常,图解微信常见帐号异常处理办法
  15. linux notifier
  16. 瀑布模型(waterfall model)需求明确+严格顺序执行
  17. 三次方程求根公式例子二
  18. zoho邮箱收信服务器,配置邮件交付 - Zoho Mail 设置
  19. 博客园北京俱乐部置顶消息汇总(2009-03-03更新)
  20. 直饮净水器什么牌子好,净水器评测

热门文章

  1. 远程连接Linux,如何使程序断开连接后继续运行
  2. GPU(CUDA)学习日记(九)------ CUDA存储器模型
  3. IO多路复用的三种机制Select,Poll,Epoll
  4. 云炬Android开发笔记 5-8文件下载功能设计与实现
  5. [原理篇] 逻辑回归
  6. CMake3:添加一个库
  7. [OS复习]文件管理2
  8. FFmpeg--av_register_all函数分析
  9. spring源码研究
  10. VMware上的ubuntu14.04与win7共享文件夹