vlambda博客
学习文章列表

Java8笔记十一(函数式编程)


  • 1.1 概述

  • 1.2 Lambda表达式

  • 1.3 Runnable接口

  • 1.4 函数式接口

  • 1.5 高阶函数

  • 1.6 闭包

  • 1.7 函数组合

  • 1.8 柯里化参数

  • 1.9 方法引用


1.1 概述

函数式编程(FP)通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素)。

Java8之前希望方法在调用时行为不同,只能在方法中创建包含所需行为的对象,然后将该对象传递给我们想要控制的方法来完成此操作,Java8增加了把 方法(你的代码)作为参数传递给另一个方法的能力,可以直接通过Lambda表达式和方法引用完成此操作。

1.2 Lambda表达式

Lambda 表达式是使用最小可能语法编写的函数定义,其参数和函数体被箭头 -> 分隔开,箭头左侧是参数,箭头右侧是从 Lambda 返回的表达式。

package com.java8.study;
public class Java8Study {
    interface A {
        String m1();
    }
    interface B {
        String m1(String p);
    }
    interface C {
        String m1(String p1, String p2);
    }
    public static void main(String[] args) {
        // java8之前
        A a = new A() {
            @Override
            public String m1() {
                return "Hello World";
            }
        };
        // java8之后
        // 如果没有参数,则必须使用括号 () 表示空参数列表
        A a1 = () -> "Hello World";
        // 当只用一个参数,可以不需要括号()
        B b = p -> p + "Hello World";
        C c = (p1, p2) -> p1 + p2;
        // 如果有多行,需要加上花括号和return,单行不需要
        C c1 = (p1, p2) -> {
            String result = p1 + p2;
            return result;
        };
    }
}

递归函数是一个自我调用的函数,可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。

package com.java8.study;

import java.util.function.Function;
import java.util.stream.Stream;

public class Java8Study {
    static Function<Integer, Integer> fib;
    public static void main(String[] args) {
        fib = n -> n == 0 ? 1 : n * fib.apply(n - 1);
        Stream.iterate(0, n -> n + 1)
                .limit(11)
                .forEach(n -> System.out.println(fib.apply(n)));
    }
}
// 输出
1
1
2
6
24
120
720
5040
40320
362880
3628800

1.3 Runnable接口

Runnable接口它的方法 run() 不带参数,也没有返回值,因此,我们可以使用 Lambda 表达式和方法引用作为Runnable。

package com.java8.study;

public class Java8Study {

    static class A {
        static void m1() {
            System.out.println("m1");
        }
    }
    public static void main(String[] args) {
        Runnable r = () -> System.out.println("lambda");
        Runnable r1 = A::m1;
        new Thread(r).start();
        new Thread(r1).start();
    }
}

// 输出
lambda
m1

1.4 函数式接口

方法引用和 Lambda 表达式都必须被赋值,同时赋值需要类型信息才能使编译器保证类型的正确性,为了让编译器知道传递给方法的参数的类型,Java8 引入了 java.util.function 包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型,使得我们一般情况下不需再定义自己的接口。每个接口只包含一个抽象方法,称为 函数式方法 ,这个接口称为函数式接口。

在编写接口时,可以使用 @FunctionalInterface,它是可选的,它的作用是当接口中抽象方法多于一个时产生编译期错误。

下面是java.util.function中接口的基本命名准则:

  1. 如果只处理对象而非基本类型,名称则为 FunctionConsumerPredicate 等。参数类型通过泛型添加。
  2. 如果接收的参数是基本类型,则由名称的第一部分表示,如 LongConsumerDoubleFunctionIntPredicate 等,但返回基本类型的 Supplier 接口例外。
  3. 如果返回值为基本类型,则用 To 表示,如 ToLongFunction <T>IntToLongFunction
  4. 如果返回值类型与参数类型相同,则是一个 Operator :单个参数使用 UnaryOperator,两个参数使用 BinaryOperator
  5. 如果接收参数并返回一个布尔值,则是一个 谓词 ( Predicate)。
  6. 如果接收的两个参数类型不同,则名称中有一个 Bi

下面是常用函数式接口:

// 无参数,无返回值
Runnable runnable = () -> System.out.println("Hello World");

// 无参数,返回任意类型
Supplier<String> supplier = () -> "Hello World";
Callable<String> callable = () -> "Hello World";

// 1个参数,无返回值
Consumer<String> consumer = s -> System.out.println(s);
// 2个参数,无返回值,第一个参数是引用,第二个参数是基本类型
ObjIntConsumer<Character> objIntConsumer = (p1, p2) -> System.out.println(p1 + p2);

// 1个参数,返回不同类型
Function<String, Boolean> function = p1 -> "p1".equals(p1);

// 1个参数,返回类型和参数类型相同
UnaryOperator<String> unaryOperator = p1 -> "parmas:" + p1;

// 2个参数,类型相同,返回整型
Comparator<String> comparator = (c1, c2) -> c1.compareTo(c2);

// 1个参数,返回布尔值
Predicate<String> predicate = p1 -> "p1".equals(p1);

请注意,任何函数式接口都不允许抛出受检异常( checked exception)。如果你需要Lambda表达式来抛出异常, 有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。

1.5 高阶函数

高阶函数只是一个消费或产生函数的函数。

package com.java8.study;

import java.util.function.Consumer;
import java.util.function.Function;

public class Java8Study {

    static Consumer<String> product() {
        return s -> System.out.println(s);
    }

    static boolean consume(String p1, Function<String, Boolean> function) {
        return function.apply(p1);
    }
    public static void main(String[] args) {
        // 产生一个函数
        Consumer<String> product = product();
        product.accept("Hello World");

        // 消费一个函数
        System.out.println(consume("p1", p1 -> "p1".equals(p1)));
    }
}

// 输出
Hello World
true

1.6 闭包

考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。当你调用函数时,它对那些 “外部 ”变量引用了什么?  如果语言不能自动解决,那问题将变得非常棘手。能够解决这个问题的语言被称作 支持闭包

Java8 提供了有限但合理的闭包支持,被Lambda 表达式引用的局部变量必须是 final 或者是等同 final 效果的。等同 final 效果表示虽然没有明确地声明变量是 final 的,但是因变量值没被改变过而实际有了 final 同等的效果。如果局部变量的初始值永远不会改变(不管在Lambda表达式内还是外面),那么它实际上就是 final 的。

总结Lambda可以没有限制地引用实例变量和静态变量,因为它们有独立的生命周期。但局部变量必须显式声明为final,或者是等同 final 效果的 。

内部类也会有闭包,Java 8 只是简化了闭包操作。

1.7 函数组合

函数组合意为多个函数组合成新函数。

package com.java8.study;

import java.util.Locale;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

public class Java8Study {
    public static void main(String[] args) {
        UnaryOperator<String> uo = p -> p;
        UnaryOperator<String> uo1 = p -> p + " World";
        UnaryOperator<String> uo2 = p -> p.toUpperCase(Locale.ROOT);
        System.out.println(uo.apply("Hello"));
        // compose(argument)方法先执行参数操作,再执行原操作,andThen(argument)先执行原操作,再执行参数操作
        System.out.println(uo.compose(uo1).andThen(uo2).apply("Hello"));

        Predicate<String> predicate = p -> p.contains("p1");
        Predicate<String> predicate1 = p -> p.length() <= 5;
        Predicate<String> predicate2 = p -> p.contains("p2");
        // negate()该谓词的逻辑非
        System.out.println(predicate.negate().test("p1"));
        // and(argument)原谓词和参数谓词的短路逻辑与
        System.out.println(predicate.and(predicate1).test("p1dddddd"));
        // or(argument)原谓词和参数谓词的短路逻辑或
        System.out.println(predicate.or(predicate2).test("p2dddddd"));
    }
}

// 输出
Hello
HELLO WORLD
false
false
true

1.8 柯里化参数

柯里化参数表示将一个多参数的函数,转换为一系列单参数函数。

package com.java8.study;

import java.util.function.Function;

public class Java8Study {
    public static void main(String[] args) {
        Function<String, Function<String, String>> function = p -> p1 -> p + p1;
        System.out.println(function.apply("Hello").apply(" World"));
    }
}

// 输出
Hello World

1.9 方法引用

方法引用组成:类名或对象名,后面跟 :: ,然后跟方法名称。

package com.java8.study;

public class Java8Study {
    interface A {
        void call(String s);
    }
    static class A1 {
        void m1(String msg) {
            System.out.println(msg);
        }
        static void m2(String msg) {
            System.out.println("static:" + msg);
        }
    }

    interface B {
        void call(B1 b1, String msg);
    }
    static class B1 {
        void m1(String msg) {
            System.out.println("B1::m1 " + msg);
        }
    }

    interface C {
        C1 create(String msg);
    }
    static class C1 {
        public C1(String msg) {
            System.out.println(msg);
        }
    }
    public static void main(String[] args) {
        // 只要一个方法的签名(方法参数和返回类型)和接口方法的签名相同,就可以将方法引用赋给接口
        A1 a1 = new A1();
        // 未绑定的方法引用是指没有关联对象的普通方法,使用未绑定的引用时,我们必须先提供对象。
        A a = a1::m1;
        a.call("m1");
        // 静态方法可以通过类名绑定
        a = A1::m2;
        a.call("m2");

        // 未绑定的方法引用还可以通过在接口上添加一个额外参数(对象实例)来实现方法引用
        B b = B1::m1;
        b.call(new B1(), "Hello");

        // 可以捕获构造函数的引用,然后通过引用调用该构造函数
        C c = C1::new;
        C1 c1 = c.create("C1创建");
    }
}

// 输出
m1
static:m2
B1::m1 Hello
C1创建

方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法,事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。

package com.java8.study;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;

public class Java8Study {
    public static void main(String[] args) {
        // 指向静态方法的方法引用,等同于 p -> Integer.parseInt(p)
        Function<String, Integer> fun1 = Integer::parseInt;
        
        // 指向指向任意类型实例方法的方法引用 ,等同于 p -> p.length()
        Function<String, Integer> fun2 = String::length;
        
        // 指向已经存在的外部对象中的方法的方法引用,等同于 () -> str.length()
        String str = "Hello World";
        Supplier<Integer> fun3 = str::length;
        
        // 指向构造器的方法引用,等同于 () -> new ArrayList<>()
        Supplier<List<String>> fun4 = ArrayList::new;
    }
}

参考链接

https://lingcoder.github.io/OnJava8/#/