专栏首页互扯程序Java8新特性学习--函数式编程

Java8新特性学习--函数式编程

什么是函数式编程

函数式编程并不是Java新提出的概念,其与指令编程相比,强调函数的计算比指令的计算更重要;与过程化编程相比,其中函数的计算可以随时调用。

当然,大家应该都知道面向对象的特性(抽象、封装、继承、多态)。其实在Java8出现之前,我们关注的往往是某一类对象应该具有什么样的属性,当然这也是面向对象的核心--对数据进行抽象。

但是java8出现以后,这一点开始出现变化,似乎在某种场景下,更加关注某一类共有的行为(这似乎与之前的接口有些类似),这也就是java8提出函数式编程的目的。如图所示,展示了面向对象编程到面向行为编程的变化。

Java8新引入函数式编程方式,大大的提高了编码效率。

lambda表达式

为什么需要Lambda表达式?首先,不得不提增加Lambda的目的,其实就是为了支持函数式编程,而为了支持Lambda表达式,才有了函数式接口。另外,为了在面对大型数据集合时,为了能够更加高效的开发,编写的代码更加易于维护,更加容易运行在多核CPU上,java在语言层面增加了Lambda表达式。

lambda表达式即匿名函数,它是一段没有函数名的函数体,可以作为参数直接传递给相关调用者。

Lambda 表达式通常使用 (argument) -> (body) 语法书写,例如:

(arg1, arg2...) -> { body }
(type1 arg1, type2 arg2...) -> { body }

举例来说:

(int a, int b) -> {  return a + b; }
() -> System.out.println("Hello World");
(String s) -> { System.out.println(s); }
() -> 42
() -> { return 3.1415 }

Lambda 表达式的结构:

1. 一个 Lambda 表达式可以有零个或多个参数

2. 参数的类型既可以明确声明,也可以根据上下文来推断。例如:(int a)与(a)效果相同

3. 所有参数需包含在圆括号内,参数之间用逗号相隔。例如:(a, b) 或 (int a, int b) 或 (String a, int b, float c)

4. 空圆括号代表参数集为空。例如:() -> 42

5. 当只有一个参数,且其类型可推导时,圆括号()可省略。例如:a -> return a*a

6. Lambda 表达式的主体可包含零条或多条语句

7. 如果 Lambda 表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致

8. 如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空

函数式接口

关于接口的变动,Java8中新定义了一种接口类型,函数式接口,与其他接口的区别就是:

1. 函数式接口中只能有一个抽象方法(我们在这里不包括与Object的方法重名的方法);

2. 可以有从Object继承过来的抽象方法,因为所有类的最终父类都是Object;

3. 接口中唯一抽象方法的命名并不重要,因为函数式接口就是对某一行为进行抽象,主要目的就是支持Lambda表达式。

一般通过@FunctionalInterface这个注解来表明某个接口是一个函数式接口,虽然这个注解的使用不是强制性的,但是使用它的好处是让此接口的目的更加明确,同时编译器也会对代码进行检查,来确保被该注解标注的接口的使用没有语法错误。函数式接口是Java支持函数式编程的基础。

Java8函数式编程语法入门

Java8中函数式编程语法能够精简代码。

使用Consumer作为示例,它是一个java中的预先定义的函数式接口,包含一个抽象方法accept,这个方法只有输入而无输出。

@FunctionalInterface //指定为函数式接口
public interface Consumer < T > {

    void accept(T t);
    // 注意函数式接口只能有一个抽象函数,如果多于1个就会报错
//  void accept2(T t); 

    // default是java8另外一个新特性,默认实现函数,所以不是抽象的,不会报错
    // 下面我们再讲一下这个函数
    default Consumer< T > andThen(Consumer< ? super T > after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }

}

为什么需要default method? 1. 即使你的API已经发布出去了,你依然可以为接口添加新方法并且无需考虑向后兼容问题。 2. java8 对Lambda的支持必然会影响JDK API的接口,如果直接在接口中添加方法,就会导致所有实现该接口的类或者接口无法通过编译。

现在我们要定义一个Consumer对象,传统的方式是这样定义的:

Consumer c = new Consumer() {
    @Override
    public void accept(Object o) {
        System.out.println(o);
    }
};

而在Java8中,针对函数式编程接口,可以这样定义:

Consumer c = (o) -> {
    System.out.println(o);
}; 

上面已说明,函数式编程接口都只有一个抽象方法,因此在采用这种写法时,编译器会将这段函数编译后当作该抽象方法的实现。

如果接口有多个抽象方法,编译器就不知道这段函数应该是实现哪个方法的了。

因此,=号后面的函数体我们就可以看成是accept函数的实现。

输入:->前面的部分,即被()包围的部分。此处只有一个输入参数,实际上输入是可以有多个的,如两个参数时写法:(a, b);当然也可以没有输入,此时直接就可以是()。 函数体:->后面的部分,即被{}包围的部分;可以是一段代码。 输出:函数式编程可以没有返回值,也可以有返回值。如果有返回值时,需要代码段的最后一句通过return的方式返回对应的值。

当函数体中只有一个语句时,可以去掉{}进一步简化:

Consumer c = (o) -> System.out.println(o);

然而这还不是最简的,由于此处只是进行打印,调用了System.out中的println静态方法对输入参数直接进行打印,因此可以简化成以下写法:

Consumer c = System.out::println;

它表示的意思就是针对输入的参数将其调用System.out中的静态方法println进行打印。

到这一步就可以感受到函数式编程的强大能力。

通过最后一段代码,我们可以简单的理解函数式编程,Consumer接口直接就可以当成一个函数了,这个函数接收一个输入参数,然后针对这个输入进行处理;当然其本质上仍旧是一个对象,但我们已经省去了诸如老方式中的对象定义过程,直接使用一段代码来给函数式接口对象赋值。 而且最为关键的是,这个函数式对象因为本质上仍旧是一个对象,因此可以做为其它方法的参数或者返回值,可以与原有的代码实现无缝集成!

下面对Java中的几个预先定义的函数式接口及其经常使用的类进行分析学习。

Java函数式接口

Consumer

Consumer是一个函数式编程接口; 顾名思义,Consumer的意思就是消费,即针对某个东西我们来使用它,因此它包含有一个有输入而无输出的accept接口方法;

除accept方法,它还包含有andThen这个方法;

其定义如下:

default Consumer< T > andThen(Consumer< ? super T > after) {
    Objects.requireNonNull(after);
    return (T t) -> { accept(t); after.accept(t); };
}

可见这个方法就是指定在调用当前Consumer后是否还要调用其它的Consumer;

使用示例:

public static void consumerTest() {
    Consumer f = System.out::println;
    Consumer f2 = n -> System.out.println(n + "-F2");

    //执行完F后再执行F2的Accept方法
    f.andThen(f2).accept("test");

    System.out.println("------------------");
    
    //连续执行F的Accept方法
    f.andThen(f).andThen(f).andThen(f).accept("test1");
}

输出结果:(是不是感觉很绕?)

test test-F2 ------------------ test1 test1 test1 test1

2.2 Function

Function也是一个函数式编程接口;它代表的含义是“函数”,而函数经常是有输入输出的,因此它含有一个apply方法,包含一个输入与一个输出;

除apply方法外,它还有compose与andThen及indentity三个方法,其使用见下述示例;

/**
 * Function测试
 */
public static void functionTest() {
    Functionf = s -> s++;
    Functiong = s -> s * 2;

    /**
     * 下面表示在执行F时,先执行g,并且执行F时使用g的输出当作输入。
     * 相当于以下代码:
     * Integer a = g.apply(2);
     * System.out.println(f.apply(a));
     */
    System.out.println(f.compose(g).apply(2));

    /**
     * 表示执行F的Apply后使用其返回的值当作输入再执行g的Apply;
     * 相当于以下代码
     * Integer a = f.apply(2);
     * System.out.println(g.apply(a));
     */
    System.out.println(f.andThen(g).apply(2));

    /**
     * identity方法会返回一个不进行任何处理的Function,即输出与输入值相等; 
     */
    System.out.println(Function.identity().apply("a"));
}

输出结果:

4 4 a

2.3 Predicate

Predicate为函数式接口,predicate的中文意思是“断定”,即判断的意思,判断某个东西是否满足某种条件; 因此它包含test方法,根据输入值来做逻辑判断,其结果为True或者False。

它的使用方法示例如下:

/**
 * Predicate测试
 */
private static void predicateTest() {
    Predicatep = o -> o.equals("test");
    Predicateg = o -> o.startsWith("t");

    /**
     * negate: 用于对原来的Predicate做取反处理;
     * 如当调用p.test("test")为True时,调用p.negate().test("test")就会是False;
     */
    Assert.assertFalse(p.negate().test("test"));

    /**
     * and: 针对同一输入值,多个Predicate均返回True时返回True,否则返回False;
     */
    Assert.assertTrue(p.and(g).test("test"));

    /**
     * or: 针对同一输入值,多个Predicate只要有一个返回True则返回True,否则返回False
     */
    Assert.assertTrue(p.or(g).test("ta"));
}

使用函数式代码的好处:

1. 减少了可变量(Immutable Variable)的声明

2. 能够更好的利用并行(Parallelism)

3. 代码更加简洁和可读

本文分享自微信公众号 - 互扯程序(chat_routine)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java中的 Threadpoolexecutor类

    在之前的文章Java中executors提供的的4种线程池中,学习了一下Executors类中提供的四种线程池.

    呼延十
  • 怎么才能学好Java编程写好Java代码?

    动力节点Java培训最新上线Java实验班,等你来测试自己适不适合学习Java编程哦!

    动力节点Java学院
  • [Keras填坑之旅]·图片分类中是否使用img_to_array的影响

    在使用keras进行图片分类的任务,笔者最开始的方法是使用opencv库cv2.imread读取照片,再使用cv2.resize重设尺寸。在和别人的代码进行训练...

    小宋是呢
  • Redis的五种数据类型及应用场景

    redis是用键值对的形式来保存数据,键类型只能是String,但是值类型可以有String、List、Hash、Set、Sorted Set五种,来满足不同场...

    Java_老男孩
  • LeetCode-15 三数之和

    今天我们学习第15题三数之和,这是一道中等题。像这样字符串的题目经常作为面试题来考察面试者算法能力和写代码能力,因此最好能手写出该题。下面我们看看这道题...

    用户3470542
  • Guava中的一些增强集合类

    写了好多和Java集合类有关的文章,学习了好多集合类的用法,有没有感觉还是有一些常见的需求集合类没有办法满足呢?需要自己使用Java集合中的类去实现,但是这种常...

    呼延十
  • 突破Java面试(15)-分布式搜索引擎Elastic Search的工作流程

    ES无非就是写/查数据,你如果不明白你发起写入/搜索请求后,ES做了什么,那你该劝退了.

    JavaEdge
  • 类加载器以及双亲委派模型

    首先我们来描述一个小说场景,通过这个场景在去理解我们相关的类加载器的执行以及双亲委派模型。

    胖虎
  • 深入挖崛:mysql主从复制原理

    实现整个复制操作主要由三个进程完成的,其中两个进程在Slave(Sql进程和IO进程),另外一个进程在 Master(IO进程)上。

    李红
  • LeetCode-19 删除链表中的倒数第N个节点

    今天我们学习第19题删除链表中的倒数第N个节点,这是一道中等题。这个题属于面试中的高频题,一定要能手写出来。下面我们看看这道题的题目描述。

    用户3470542

扫码关注云+社区

领取腾讯云代金券