Java函数式编程技巧

1 解耦

例,我们验证邮箱,将邮箱校验逻辑和校验成功失败后处理的业务逻辑拆分。

public class EmailValidation1 {
	// 定义校验逻辑
    static Pattern emailPattern = Pattern.compile("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$");
    // 定义校验成功失败回调逻辑
    static Consumer<String> success = s -> System.out.println("Mail sent to " + s);
    static Consumer<String> failure = s -> System.err.println("Error message logged: " + s);

	// 应用
    public static void main(String... args) {
        emailChecker.apply("this.is@my.email").bind(success, failure);
        emailChecker.apply(null).bind(success, failure);
        emailChecker.apply("").bind(success, failure);
    }

	// 校验逻辑
    static Function<String, Result<String>> emailChecker = s ->
            s == null
                    ? new Result.Failure<>("email must not be null")
                    : s.length() == 0
                    ? new Result.Failure<>("email must not be empty")
                    : emailPattern.matcher(s).matches()
                    ? new Result.Success<>(s)
                    : new Result.Failure<>("email " + s + " is invalid.");
}

接口定义:

public interface Result<T> {
    void bind(Consumer<T> success, Consumer<String> failure);

    static <T> Result<T> failure(String message) {
        return new Failure<>(message);
    }

    static <T> Result<T> success(T value) {
        return new Success<>(value);
    }

    class Success<T> implements Result<T> {
        private final T value;
        private Success(T t) {value = t;}

        @Override
        public void bind(Consumer<T> success, Consumer<String> failure) {
            success.accept(value);
        }
    }

    class Failure<T> implements Result<T> {
        private final String errorMessage;
        private Failure(String s) {this.errorMessage = s;}

        @Override
        public void bind(Consumer<T> success, Consumer<String> failure) {
            failure.accept(errorMessage);
        }
    }
}

2 处理集合

	
    @Test
    public void testFold() {
        List<Integer> listInteger = list(1, 2, 3, 4, 5, 6);
        // 对集合元素进行累加
        assertEquals(Integer.valueOf(21), fold(listInteger, 0, x -> y -> x + y));
        // 字符串拼接
        Function<Integer, Function<String, String>> f = x -> y -> "(" +  + " + " + y + ")";
        assertEquals("(1 + (2 + (3 + (4 + (5 + 0)))))", foldRight(list, "0", f));
    }

	// 对集合元素进行折叠
    public static Integer fold(List<Integer> is, Integer identity,
                               Function<Integer, Function<Integer, Integer>> f) {
        for (Integer i : is)
            identity = f.apply(identity).apply(i);
        return identity;
    }

    public static <T, U> U foldRight(List<T> ts, U identity, Function<T, Function<U, U>> f) {
        for (int i = ts.size(); i > 0; i--) {
            identity = f.apply(ts.get(i - 1)).apply(identity);
        }
        return identity;
    }

3 按规则生成数据

    @Test
    public void testUnfold() {
        assertEquals("[1, 2, 4, 8]", unfold(1, x -> x * 2, x -> x < 10).toString());
        assertEquals("[x, xx, xxx, xxxx]", unfold("x", x -> x + "x", x -> x.length() < 5).toString());
    }
    
    // 对折叠(表达式也算是折叠)进行展开
    public static <T> List<T> unfold(T seed, Function<T, T> f, Function<T, Boolean> p) {
        List<T> result = new ArrayList<>();
        T temp = seed;
        while (p.apply(temp)) {
            result = append(result, temp);
            temp = f.apply(temp);
        }
        return result;
    }

    public static <T> List<T> append(List<T> list, T t) {
        List<T> ts = new ArrayList<>(list);
        ts.add(t);
        return Collections.unmodifiableList(ts);
    }

	// 范围生成
    public static List<Integer> range(Integer start, Integer end) {
        return range_(list(), start, end).eval();	// eval利用迭代,执行生成逻辑
    }

    private static TailCall<List<Integer>> range_(List<Integer> acc, Integer start, Integer end) {
        return end <= start
                ? ret(acc)	// 见5递归抽象,封装计算完成后的结果
                // 函数不能修改对象,因此append逻辑是根据现有的数据创建新的list对象,再添加新元素,返回
                : sus(() -> range_(append(acc, start), start + 1, end));	
    }

    @Test
    public void testRange() {
        assertEquals("[0, 1, 2, 3, 4]", Range.range(0, 5).toString());
        assertEquals("[]", Range.range(5, 1).toString());
    }

4 尾递归

	// 线性递归
    Integer sum(List<Integer> list) {
        return list.isEmpty()
                ? 0
                : head(list) + sum(tail(list));
    }

	// 尾递归
    Integer sumTail(List<Integer> list, int acc) {
        return list.isEmpty()
                ? acc
                // 比线型递归多一个参数,这个参数就是上一次函数调用计算的值
                // 可以看到,当前函数计算结果后直接return,没有剩余任何指令,不会再回来了
                : sumTail(tail(list), acc + head(list));
    }

如果编译器支持消除尾调用(Tail Call Eliminate),使用尾递归能优化普通的线型递归。
优点:尾递归每次调用都在收集结果,避免了线性递归不收集结果只能依靠依次展开消耗大量栈内存。

测试结果:线性递归sum调用次数5464、5462、5464,尾递归sumTail调用次数5033、5030、5030。这,,,怎么资源消耗还提升了呢?答:很遗憾,Java没有TCE。

尾递归就真的没有用了吗?请看6。

5 递归抽象

上面的尾递归在Java中没有用,那很不行啊,Java的递归深度问题难道就无解了么?

有还是有的,利用函数式、迭代辅助实现:

// 方法调用可以暂停和恢复
public abstract class TailCall<T> {
    public abstract TailCall<T> resume();
    public abstract T eval();
    public abstract boolean isSuspend();

    public static class Return<T> extends TailCall<T> {
        private final T t;  // 存储递归计算最终结果
        public Return(T t) {this.t = t;}

        @Override
        public T eval() {return t;}

        @Override
        public boolean isSuspend() {return false;}

        @Override
        public TailCall<T> resume() {throw new IllegalStateException("Return has no resume");}
    }

    public static class Suspend<T> extends TailCall<T> {
        private final Supplier<TailCall<T>> resume;     // 存储下一个调用
        public Suspend(Supplier<TailCall<T>> resume) {this.resume = resume;}

        @Override
        public T eval() {throw new IllegalStateException("Suspend has no value");}

        @Override
        public boolean isSuspend() {return true;}

        @Override
        public TailCall<T> resume() {return resume.get();}
    }

	// 工厂方法
    public static <T> Return<T> ret(T t) {
        return new Return<>(t);
    }

    public static <T> Suspend<T> sus(Supplier<TailCall<T>> s) {
        return new Suspend<>(s);
    }
}

定义计算逻辑:

    static TailCall<Integer> add(int x, int y) {
        return y == 0
                ? TailCall.ret(x)  // 保存最终计算结果
                : TailCall.sus(() -> add(x + 1, y - 1));  // 利用Lambda表达式创建对象并保存当前调用计算结果
    }

测试:

    @Test
    public void test() {
        TailCall<Integer> tailCall = add(0, 100000000);
        while (tailCall.isSuspend())
            tailCall = tailCall.resume();
        assertEquals(Integer.valueOf(100000000), tailCall.eval());
    }

暴露更好用的API:
修改Suspend:

        @Override
        public T eval() {
            TailCall<T> tailRec = this;
            while (tailRec.isSuspend()) {
                tailRec = tailRec.resume();
            }
            return tailRec.eval();
        }

重新封装计算逻辑,add方法变字段(add是固定算法,就不传参了):

    static Function<Integer, Function<Integer, Integer>> add = x -> y -> {
        class AddHelper {   // 定义计算逻辑
            final Function<Integer, Function<Integer, TailCall<Integer>>> addHelper =
                    a -> b -> b == 0
                            ? ret(a)
                            : sus(() -> this.addHelper.apply(a + 1).apply(b - 1));  // this 指向 AddHelper 实例
        }
        return new AddHelper().addHelper.apply(x).apply(y).eval();
    };

调用测试:

    @Test
    public void test() {
        assertEquals(Integer.valueOf(100000003), add.apply(3).apply(100000000));
    }

6 递归优化

我们常用递归斐波那契数列写走楼梯的题,然而普通写法哪怕输入30都得计算半天,而优化后,哪怕是几百万的楼梯都能很快计算出结果。

    // 时间复杂度 2^n,拉胯得很
    public int fib(int num) {
        if (num == 0 || num == 1)
            return num;
        return fib(num - 1) + fib(num - 2);
    }

    // 优化1 尾递归优化
    public BigInteger fib_(BigInteger acc1, BigInteger acc2, BigInteger x) {
        return x.equals(BigInteger.ZERO)
                ? BigInteger.ZERO
                : x.equals(BigInteger.ONE)
                ? acc1.add(acc2)
                : fib_(acc2, acc1.add(acc2), x.subtract(BigInteger.ONE));   // n 长度的斐波那契数列就会调用n次函数计算
    }

    // 优化2 迭代,避免栈溢出
    private static TailCall<BigInteger> fib_2(BigInteger acc1, BigInteger acc2, BigInteger x) {
        return x.equals(BigInteger.ZERO) 
                ? ret(BigInteger.ZERO)
                : x.equals(BigInteger.ONE)
                ? ret(acc1.add(acc2))
                : sus(() -> fib_2(acc2, acc1.add(acc2), x.subtract(BigInteger.ONE)));
    }

    @Test
    public void testFib1() {
        System.out.println(fib(5));    // 只能计算几十,否则时间非常长
    }

	// 解决计算慢,利用到了尾递归,在时间复杂度上进行优化
    @Test
    public void testFib2() {
        System.out.println(fib_(BigInteger.ONE, BigInteger.ZERO, BigInteger.valueOf(5)));   // 8000 左右,主要看线程栈的大小
    }

	// 解决上面堆栈溢出,利用递归变迭代,在空间复杂度上进行优化
    @Test
    public void testFib3() {
        // 轻松计算几十万的斐波那契数列
        // 计算出下一个逻辑,根据下一个逻辑再计算,直到终止条件;通过迭代完成上述逻辑,相当于变成了线型计算
        // (将递归计算值,转变为迭代计算计算公式)
        // 可以简单理解为,走泥巴路,丢一块石头踏上去,再丢一块石头,再走,,,直到走出去为止
        System.out.println(fib_2(BigInteger.ONE, BigInteger.ZERO, BigInteger.valueOf(6)).eval());
    }

7 组合函数

    static <T> Function<T, T> composeAll(List<Function<T, T>> list) {
        // 应用:x.apply(y).apply(z) 调用顺序:x<-y<-z
        // 等价于  x -> y -> x.compose(y)  等价于  x -> x::compose
        return foldRight(list, identity(), x -> y -> z -> x.apply(y.apply(z))); 
    }

    static <T> Function<T, T> composeAllViaAndThen(List<Function<T, T>> list) {
        return foldRight(list, identity(), x -> y -> y.andThen(x));
    }

测试:

    @Test
    public void testComposeAll1() {
        Function<String, String> f1 = x -> x + "a";
        Function<String, String> f2 = x -> x + "b";
        Function<String, String> f3 = x -> x + "c";
        Function<String, String> f4 = x -> x + "d";
        Function<String, String> f5 = x -> x + "e";
        List<Function<String, String>> list = Arrays.asList(f1, f2, f3, f4, f5);
        assertEquals("edcba", ComposeAll.composeAll(list).apply(""));
    }

    @Test
    public void testComposeAllViaAndThen() {
        Function<String, String> f1 = x -> x + "a";
        Function<String, String> f2 = x -> x + "b";
        Function<String, String> f3 = x -> x + "c";
        Function<String, String> f4 = x -> x + "d";
        Function<String, String> f5 = x -> x + "e";
        List<Function<String, String>> list = Arrays.asList(f1, f2, f3, f4, f5);
        assertEquals("edcba", ComposeAll.composeAllViaAndThen(list).apply(""));
    }

8 函数记忆化

定义一个缓存器:

public class Memorizer<T, U> {
    private final Map<T, U> cache = new ConcurrentHashMap<>();
    private Memorizer() {}

    public static <T, U> Function<T, U> memoize(Function<T, U> function) {
        return new Memorizer<T, U>().doMemoize(function);
    }

    private Function<T, U> doMemoize(Function<T, U> function) {
        return input -> cache.computeIfAbsent(input, function::apply);
    }
}

测试:

	// 定义计算逻辑
    private static Integer longCalculation(Integer x) {
        // 模拟耗时计算
        try {Thread.sleep(1_000);} catch (InterruptedException ignored) {}
        return x * 2;
    }
	// 定义多参函数,组合计算逻辑
    public static Function<Tuple3<Integer, Integer, Integer>, Integer> ft =
            x -> longCalculation(x._1) + longCalculation(x._2) - longCalculation(x._3);
    // 将函数与计算结果缓存,实现记忆化
    public static Function<Tuple3<Integer, Integer, Integer>, Integer> ftm = Memorizer.memoize(ft);

    @Test
    public void automaticMemoizationExample3() {
        long startTime = System.currentTimeMillis();
        // 将Tuple3对象作为key,保存其计算结果
        Integer result1 = ftm.apply(new Tuple3<>(2, 3, 4));
        long time1 = System.currentTimeMillis() - startTime;
        startTime = System.currentTimeMillis();
        Integer result2 = ftm.apply(new Tuple3<>(2, 3, 4));
        long time2 = System.currentTimeMillis() - startTime;
        System.out.println("result1=" + result1 + "计算result1耗时:" + time1);
        System.out.println("result2=" + result2 + "计算result2耗时:" + time2);
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值