lambda及其延伸

175 阅读10分钟

lambda及其延伸

一、函数式编程

什么是函数式编程,函数式编程是一种相对于命令式编程的一种编程范式,它不是一种具体的技术,而是一种如何搭建程序的方法论,个人理解在Java中能够熟练的使用Stream流API和lambda表达式,有流相关的思想,就可以说掌握了函数式编程的思想。

使用函数式编程可以让写法更加简单,他跟命令式编程的不同在于,命令式编程关注点在于“怎么做”,而函数式编程在于的关注点是“做什么”。
如以下例子:从一堆数中找出最小值。

import java.util.stream.IntStream;

class MinDemo() {
    public static void main(String[] args) {
        // 命令式编程的写法
        int[] nums = {33, 55, 90, -666, 90};
        int min = Integer.MAX_VALUE;
        for (int i : nums) {
            if (i < min) {
                min = i;
            }
        }
        System.out.println(min);


        // java8 Stream函数式编程的写法
        int asInt = IntStream.of(nums).min().getAsInt();
        System.out.println(asInt);
    }
}

同时lambda还可以让写法更加简单,如以下创建线程的例子

class ThreadDemo() {
    public static void main(String[] args) {
        // 传统的写法
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("xxx");
            }
        }).start();
        
        // lambda 还可以让写法更加简单
        new Thread(() -> System.out.println("xxx")).start();
    }
}

二、接触lambda
lambda表达式就是一个返回了一个实现自定义接口的对象实例,lambda表达式带参数的几种常见写法如下:

interface Interface {
    int doubleNum(int i);
}

class lambdaDemo() {
    public static void main(String[] args) {
        Interface i1 = (i) -> i * 2;
        // 这种式最常见的写法
        Interface i2 = i -> i * 2;

        Interface i3 = (int i) -> i * 2;

        Interface i4 = i -> {return i;};
    }
}

lambda对接口有一定的限制,在接口里面,必须只有一个要实现的方法,这种也叫做接口函数,新增加了一个注解 @FunctionalInterface ,这个注解更多的是做一个编译器的校验,加了这个注解表明这个接口是一个函数接口,再增加为实现的接口会导致报错,同时这种方式也是设计里面的思想,叫做单一责任制,一个接口只做一个事情。

因为Java8中新增了default默认实现,所以可以包含多个方法,但是只能有一个未实现的。

interface Interface {
    int doubleNum(int i);
    
    default int add(int x, int y) {
        return x + y;
    }
}

二、函数接口

接下来再举一个例子,对我的钱进行格式化。

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

interface 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 static void main(String[] args) {
        MyMoney myMoney = new MyMoney(999999);
        myMoney.printMoney(i -> new DecimalFormat("#,###").format(i));
    }
}

可以发现,我定义了一个接口 IMoneyFormat 接口,里面一个int 类型的参数输入和 String 类型的输出,其他的什么也没干,感觉这样很麻烦,有没有其他的方式呢?是有的,以上的例子可以改写为

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<Integer, String> moneyFormat) {
        System.out.println("我的存款:" + moneyFormat.apply(this.money));
    }

    public static void main(String[] args) {
        MyMoney myMoney = new MyMoney(99999);
        // 在上面那个例子当中使用了自定义的接口,接口只关注两个事,
        // 输入是什么、输出是什么,jdk也提供了这样的接口

        // 函数接口除了写起来比较方便,同时支持链式
        Function<Integer, String> moneyFormat = i -> new DecimalFormat("#,###").format(i);
        myMoney.printMoney(moneyFormat.andThen(s -> "人民币" + s));
    }
}

除了以上的Function方法,还默认提供了以下函数接口

接口输入参数返回类型说明
PredicateTboolean断言
ConsumerT/消费一个数据
Function<T, R>TR输入T输出R的函数
Supplier/T提供一个数据
UnaryOperatorTT一元函数(输入输出相同)
BiFunction<T, U, R>(T, U)R两个输入的函数
BinaryOperator(T, T)T二元函数(输入输出类型相同)

前面五个,是一个输入一个输出的,有些没有输出的,是一个消费者,有些接口提供了类型,就不需要我们在写泛型了,如 IntPredicate 等等。
例子如下


import java.util.function.*;

public class FunctionDemo {

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

        // 消费者函数接口
        Consumer<String> consumer = System.out::println;
        consumer.accept("输入的数据");

        Function<Integer, Integer> function = x -> x * 2;
        System.out.println(function.apply(1));

        Supplier<String> stringSupplier = () -> "2";
        System.out.println(stringSupplier.get());

        UnaryOperator<Integer> unaryOperator = x -> x * 2;
        System.out.println(unaryOperator.apply(1));

        BiFunction<Integer, Integer, Integer>  biFunction = (x, y) -> x * y;
        System.out.println(biFunction.apply(2, 2));

        BinaryOperator<Integer> binaryOperator =  (x, y) -> x * y;
        System.out.println(binaryOperator.apply(2, 2));
    }
}

三、方法引用

lambda是一个匿名函数,箭头左边是参数,右边是执行体,当函数执行体里面只有一个函数调用,而且函数的参数跟箭头里边一样的话,就可以使用方法引用。
同时使用方法引用,不会多生成一个类似 lambda$0 这样的函数

import java.util.function.*;


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 + "叫了");
    }

    /**
     * 吃狗粮方法
     * @param num 吃狗粮的数目
     * @return 还剩多少狗粮
     */
    public int eat(int num) {
        System.out.println("吃了" + num + "斤狗粮");
        this.food -= num;
        return this.food;
    }

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

public class MethodRefrenceDemo {

    public static void main(String[] args) {
        Dog dog = new Dog();

        // 消费者函数接口
        Consumer<String> consumer = System.out::println;
        consumer.accept("输入的数据");

        // 静态方法的方法引用
        Consumer<Dog> consumer1 = Dog::bark;
        consumer1.accept(dog);

        // 非静态方法使用对象实例来使用
        // Function<Integer, Integer> function = dog::eat;
        // UnaryOperator<Integer> function = dog::eat;
        IntUnaryOperator function = dog::eat;
        System.out.println("还剩下" + function.applyAsInt(2) + "斤");

        // 使用类名来方法引用
        BiFunction<Dog, Integer, Integer> biFunction = Dog::eat;
        System.out.println("还剩下" + biFunction.apply(dog, 2) + "斤");

        // 构造函数的方法引用
        Supplier<Dog> supplier = Dog::new;
        System.out.println("创建了新对象" + supplier.get());

        // 带参数的构造函数的方法引用, JDK自己回去找对应的构造函数
        Function<String, Dog> function2 = Dog::new;
        System.out.println("创建了新对象" + function2.apply("旺财"));

    }

}

四、类型推断

lambda是一个匿名函数,最后返回了一个实现指定接口的对象,所以需要告诉实现的就近的接口,有以下几种方式告诉lambda。

interface IMath {
    int add(int x, int y);
}

public class TypeDemo {
    public static void main(String[] args) {
        // 变量类型定义
        IMath lambda = (x, y) -> x + y;

        // 数组里
        IMath[] lambdas = {(x, y) -> x + y};

        // 强转
        Object lambda2 = (IMath)(x, y) -> x + y;

        // 通过返回类型
        IMath createLambda = createLambda();
    }

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

}

五、变量引用

匿名内部类以用外部变量需要声明为final;但是默认可以不写,修改会报编译错误。
为什么内部类以用外部变量必须为final,因为java里面java参数传的是值、而不是引用。需要大概描述一下

六、级联表达式和柯里化

import java.util.function.Function;

public class CurryDemo {
    public static void main(String[] args) {
        Function<Integer, Function<Integer, Integer>> function = x ->y -> x + y;
        System.out.println(function.apply(1).apply(2));
    }
}

以上就是一个级联表达式,那么什么是一个柯里化呢。柯里化就是把多个参数的函数转换成只有一个参数的函数
柯里化的意义,函数标准化,函数标准化之后可以方便的统一处理。
高阶函数:返回函数的函数,不做详细探讨
柯里化高阶函数

Stream流编程

一、概念

概念

Stream是一个高级的迭代器,他不是一个数据结构,他不是一个集合,他不会存放数据,Stream关注的是高效处理数据,他其实就是一个把数据在一个流水线梳理的pipeline。Stream流API

外部迭代和内部迭代的概念

import java.util.stream.IntStream;

public class StreamDemo {
    public static void main(String[] args) {
        int[] nums = {1, 2, 3};
        // 外部迭代
        int sum = 0;
        for (int i: nums) {
            sum += i;
        }
        System.out.println(sum);

        // 使用Stream的内部迭代
        int sum2 = IntStream.of(nums).sum();
        System.out.println(sum2);
    }
}

从以上的两个例子当中可以很容易的看到内部迭代和外部迭代的区别,内部迭代更加的简短,并不关注怎么样去得到数据;使用外部迭代的缺点在于,首先他是一个串行的,在面临大数据量的时候性能不达标,如果我们用外部迭代的话就需要自己去做线程池,自己去拆分;使用流的话就不需要自己去做,只需要使用一个并行流就可以,而且还可以实现一些高级的特性,如惰性求值,并行,短路的一些操作,所谓的短路操作就是不需要等所有结果都处理完,只需要把符合条件的结果从中间拿出来。

中间操作/终止操作和惰性求值

import java.util.stream.IntStream;

public class StreamDemo {
    public static void main(String[] args) {
        int[] nums = {1, 2, 3};
        // map就是中间操作(返回stream流)
        // sum就是终止操作, 产生一个结果(专业术语 副作用)
        int sum = IntStream.of(nums).map(x -> x * 2).sum();
        System.out.println(sum);

        // 那什么是惰性求值呢
        int sum2 = IntStream.of(nums).map(StreamDemo::doubleNum).sum();
        System.out.println(sum2);
        System.out.println("----------分割线--------------");
        // 惰性求值就是终止操作没有调用的情况下,中间操作不会执行
        IntStream.of(nums).map(StreamDemo::doubleNum);

    }

    public static int doubleNum(int i) {
        System.out.println("执行了乘以2");
        return i * 2;
    }
}

二、流的创建


相关方法
集合Collection.stream/parallelStream
数组Arrays.stream
数组StreamIntStream/LongStream.range/rangeClosed
Random.ints/longs/doubles
自己创建Stream._generate/_iterate 可以自己创建提供者创建一个无限的流
import java.util.*;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class StreamDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        // 从集合中创建
        list.stream();
        list.parallelStream();

        // 从数组中创建
        Arrays.stream(new int[]{1, 2, 3});

        // 创建数组流
        IntStream.of(1, 2, 3);
        IntStream.rangeClosed(1, 10);

        // 使用random创建一个无限流
        new Random().ints().limit(10);

        // 自己产生流
        Random random = new Random();
        Stream.generate(() -> random.nextInt()).limit(20);
    }
}

三、流的中间操作


相关方法
无状态操作map/maoToXxx
flatMap/ flatMapToXxx
filter
peek
unordered
有状态操作distinct
sorted
limit/skip
import java.util.*;
import java.util.stream.Stream;

public class StreamDemo {
    public static void main(String[] args) {
        String str = "my name is hero";

        Stream.of(str.split("" )).map(String::length).filter(length -> length > 2).forEach(System.out::println);

        // flatMap A -> B 属性(是个集合),最终得到所有的A元素里面的所有B属性集合
        // intStream/longStream并不是Stream的子类,所以要进行装箱 boxed;
        Stream.of(str.split(" ")).flatMap(f -> f.chars().boxed()).forEach(s -> System.out.println((char)s.intValue()));

        // peek 用于 debug,是个中间操作,和 foreach是终止操作
        System.out.println("----------------peek-------------");
        Stream.of(str.split(" ")).peek(System.out::println).forEach(System.out::println);

        // limit 使用,主要用于无限流
        new Random().ints().filter(i -> i > 100 && i < 1000).limit(10).forEach(System.out::println);
    }
}

四、流的终止操作


相关方法
非短路操作forEach/forEachOrdered
collect/toArray
reduce
min/max/count
短路操作findFirst/findAny
allMatch/anyMatch/noneMatch
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamDemo {
    public static void main(String[] args) {
        String str = "my name is hero";

        // 不保证顺序
        str.chars().parallel().forEach(s -> System.out.println((char) s));
        System.out.println("----------------------------------------");
        // 使用forEachOrdered保证顺序
        str.chars().parallel().forEachOrdered(s -> System.out.println((char) s));

        // 收集操作 collect
        List<String> collect = Stream.of(str.split(" ")).collect(Collectors.toList());
        System.out.println(collect);

        // reduce
        Optional<String> reduce = Stream.of(str.split(" ")).reduce((s1, s2) -> s1 + "|" + s2);
        System.out.println(reduce.orElse(""));

        // 带初始值的reduce
        String reduce2 = Stream.of(str.split(" ")).reduce("", (s1, s2) -> s1 + "|" + s2);
        System.out.println(reduce2);

        // 计算所有单词的总长度
        Integer length = Stream.of(str.split(" ")).map(String::length).reduce(0, (s1, s2) -> s1 + s2);
        System.out.println(length);

        // max 的使用
        Optional<String> max = Stream.of(str.split(" ")).max((s1, s2) -> s1.length() - s2.length());
        System.out.println(max.get());

        // 使用findFirst短路操作
        OptionalInt findFirst = new Random().ints().findFirst();
        System.out.println(findFirst);

    }
}

五、并行流

可以非常方便的管理线程,不需要我们自己去拆分任务

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class StreamDemo {
    public static void main(String[] args) {
        // 现在要实现一个这样的效果,先并行再串行,
        // 多次调用 parallel / sequential, 以最后一次调用为准
        long sum = IntStream.range(1, 100)
                // 调用 parallel 产生并行流
                .parallel()
                .peek(StreamDemo::debug)
                // 调用 sequential 产生串行流
                .sequential()
                .peek(StreamDemo::debug2)
                .sum();
    }

    public static void debug(int i) {
        System.out.println("debug " + i);
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void debug2(int i) {
        System.out.println("debug2 " + i);
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

并行流使用的线程池是jdk自带的线程池 ForkJoinPool.commonPool ,这个线程池数是当前线程的cpu个数,使用 System._setProperty_("java.util.concurrent.ForkJoinPool.common.parallelism", "20"); 改变并行度的线程数。

由于我们使用的都是同一个默认的线程池,我们知道,线程池是有一个排队的调度的相关操作,因为使用的都是一个,就会有一个阻塞,导致我们的任务执行可能比较晚,所以在一些比较重要的场合,需要我们自定义的线程池,就不会被其他任务阻塞,代码如下。

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class StreamDemo {
    public static void main(String[] args) {
        // 现在要实现一个这样的效果,先并行再串行,
        // 多次调用 parallel / sequential, 以最后一次调用为准
        ForkJoinPool pool = new ForkJoinPool(20);
        pool.submit(() -> IntStream.range(1, 100).parallel().peek(StreamDemo::debug).sum());
        pool.shutdown();

        synchronized (pool) {
            try {
                pool.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void debug(int i) {
        System.out.println(Thread.currentThread().getName() + "debug " + i);
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

六、收集器

这个比较简单,不说了,注意分析类的可以使用MapUtils

JDK9 Reactive Stream

一、概念

Reactive Stream 响应式流或反应式流,是JDK9引入的一套标准,是一套基于发布者订阅模式的数据处理的规范或机制,在Java中叫 Flow API。

Reactive Stream 支持背压。

import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;

public class FlowDemo {
    public static void main(String[] args) throws InterruptedException {
        // 1. 定义发布者,发布的数据类型是Integer
        // 直接使用jdk自带的SubmissionPublisher,它实现了Publisher接口
        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();

        // 2. 定义订阅者
        Flow.Subscriber<Integer> subscriber = new Flow.Subscriber<>() {

            private Flow.Subscription subscription;

            @Override
            public void onSubscribe(Flow.Subscription subscription) {
                // 保存订阅关系,需要用它来给发布者相应
                this.subscription = subscription;
                // 请求一个数据
                this.subscription.request(1);
            }

            @Override
            public void onNext(Integer item) {
                // 接收到一个数据,处理
                System.out.println("接受到的数据: " + item);

                // 处理完调用request在请求一个数据
                this.subscription.request(1);

                // 或者已经达到了目标,调用cancel告诉发布者不在接受数据了
                // this.subscription.cancel();
            }

            @Override
            public void onError(Throwable throwable) {
                // 出现了异常(例如处理数据的时候产生了异常)
                throwable.printStackTrace();

                // 我们可以告诉发布者,后面是不接收数据了
                this.subscription.cancel();

            }

            @Override
            public void onComplete() {
                // 全部数据处理完了(发布者关闭了)
                System.out.println("处理完了");
            }
        };

        // 3. 发布者和订阅者建立订阅关系
        publisher.subscribe(subscriber);

        // 4. 生产数据,并发布
        // 这里忽略数据生产过程
        int data = 111;

        publisher.submit(data);
        publisher.submit(123213);
        publisher.submit(44444);

        // 5. 结束后,关闭发布者
        // 这是环境,应该放finally或者使用 try-resource 确保关闭
        publisher.close();

        // 主线程延迟停止,否则数据没有消费就退出
        Thread.currentThread().join(1000);
    }
}

/**
 * Processor, 需要继承SubmissionPublisher并实现 Processor接口
 *
 * 输入元数据integer,过滤掉小于0的,然后转换成字符串发不出去
 */
class MyProcessor extends SubmissionPublisher<String> implements Flow.Processor<Integer, String> {

    private Flow.Subscription subscription;

    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        // 保存定于关系,需要用它来给发布者相应
        this.subscription = subscription;

        this.subscription.request(1);

    }

    @Override
    public void onNext(Integer item) {
        // 接收到一个数据,处理
        System.out.println("处理器接受到的数据: " + item);

        // 处理完调用request在请求一个数据
        if (item > 0) {
            this.submit("转换后的数据" + item);
        }
        this.subscription.request(1);
    }

    @Override
    public void onError(Throwable throwable) {
        // 出现了异常(例如处理数据的时候产生了异常)
        throwable.printStackTrace();

        // 我们可以告诉发布者,后面是不接收数据了
        this.subscription.cancel();

    }

    @Override
    public void onComplete() {
        // 全部数据处理完了(发布者关闭了)
        System.out.println("处理完了");
    }

}

结束

其实Lambda的主要特性就在这里了,其主要的应用地方Stream也介绍完了,同时又延申出了Reactive Stream,当然这也只是简单介绍,更加细节的地方,希望与诸君以后共同探索。