首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java8新特性学习--函数式编程

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

作者头像
beifengtz
发布2019-07-10 15:36:42
5260
发布2019-07-10 15:36:42
举报
文章被收录于专栏:北风IT之路北风IT之路

什么是函数式编程

函数式编程并不是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. 代码更加简洁和可读

END

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 北风IT之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档