java函数式编程

本文深入探讨了Java 8中的功能性编程概念,特别是Lambda表达式的使用,以及如何通过双冒号操作符进行方法引用。文章还详细介绍了Stream API,包括生成流、中间操作和收集操作,以及并行流和reduce方法的应用。通过实例展示了如何利用这些特性简化代码和提高效率。
摘要由CSDN通过智能技术生成

参考oracle官网:https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

目录

函数式编程、lambda

双冒号

双冒号调用静态方法

双冒号调用一个对象的实例方法

 双冒号调用 指定类型的对象 的实例方法。

双冒号调用构造方法 

<?extends T> 

Stream

先看一些接口

 生成流

中间操作

收集操作

reduce和并行流


函数式编程、lambda

函数式编程,可以将函数作为方法参数传入另一个函数,也能作为函数的返回值返回。

以List.foreach为例

  

 foreach方法参数是Consumer接口,可以用匿名内部类作参数。

        List<String> list = new ArrayList();
        list.forEach(new Consumer<String>() {
            public void accept(String s) {
                System.out.println(s);
            }
        });

官网说法,如果您的匿名类的实现非常简单,例如仅包含一种方法的接口,那么匿名类的语法似乎笨拙且不清楚。在这些情况下,您通常会尝试将功能传递为另一个方法的参数,例如在某人点击按钮时应采取的操作。lambda表达式使您可以执行此操作,以将功能视为方法参数,或代码为数据。匿名类,向您展示了如何在不给出名称的情况下实现基类。虽然这通常比命名类更简洁,但对于只有一种方法的课程,即使是一个匿名的课程似乎有点过度和繁琐。Lambda表达式让您更加紧凑地表达单方法类的实例。

lambda简化了只有一个抽象方法的接口。当一个接口只有一个方法,可以用lambda表达式实现,代码更简洁。

        List<String> list = new ArrayList();
        //(参数)->{}
        list.forEach(a-> {System.out.println(a);});
        //只有一个语句,省去{}
        list.forEach(a-> System.out.println(a));

如果接口方法需要返回值

        FutureTask<String> futureTask = new FutureTask(()->{return "";});
        FutureTask<String> futureTask2 = new FutureTask(() -> "");
        new Thread(futureTask).start();

在Lambda表达式中能访问域外的局部非final变量、但不能修改Lambda域外的局部非final变量。因为在Lambda表达式中,Lambda域外的局部非final变量会在编译的时候,会被隐式地当做final变量来处理

访问lambda外变量可以。

        String a = "";
        list.forEach(l -> System.out.println(a));

修改lambda外变量不行,这样编译错误,提示把变量改成final数组 或Automic ,然后再修改

        String a = "";        
list.forEach(l -> {
            a="12";
            System.out.println(a);
        });

双冒号

参考官网,方法引用,https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

当lambda表达式中的内容,仅仅是调用另一个线程的方法,可以用双冒号继续简化代码。有四种用法。但注意,一旦使用双冒号,lambda中的参数一定要被使用、并且只能被用一次,否则编译错误。

定义一个类试一试。

    static class Fruit {
        String name;

        public Fruit(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public static void hi(Fruit fruit) {
            System.out.println(fruit.getName());
        }

        public static void hello() {
        }
    }

双冒号调用静态方法

  • 注意静态方法 hi(Fruit f) 是有参数的,foreach把遍历的元素作参数 传给hi()。等同于 list.forEach(a->Fruit.hi(a));  如果hi方法没有参数、或参数不是一个Fruit,会编译错误
  • 有意思的地方,list.forEach(Fruit::hello)编译错误,list.forEach(a->Fruit.hello())编译通过,这是因为hello是静态方法并且没有参数,想用双冒号必须要使用lambda中的参数,list.forEach(Fruit::hello)中没有使用 接口参数。但lamda表达式很明显用不用参数不影响。
        Fruit fruit1 = new Fruit("1");
        Fruit fruit2 = new Fruit("2");
        Fruit fruit3 = new Fruit("3");
        List<Fruit> list = new ArrayList<Fruit>() {{
            add(fruit1);
            add(fruit2);
            add(fruit3);
        }};

        list.forEach(Fruit::hi);//编译通过
        list.forEach(Fruit::hello);//编译错误
        list.forEach(a->Fruit.hello());//编译通过

双冒号调用一个对象的实例方法

把list每个元素元素打印。

        //双冒号,调用实例方法
        list.forEach(System.out::print);

out是System类的类变量,是个对象,print实际上是调用out对象的实例方法。 

   

 双冒号调用 指定类型的对象 的实例方法。

  • 一开始说了,使用双冒号,lambda接口参数必须要被使用。getName方法虽然没参数,但实际上接 把接口参数作为 调用方,lambda接口参数是 list中的一个元素,调用这个元素对象的 getName方法。如果这里是setName肯定错误,因为setName方法的参数没有,接口参数只能用一次,被用做了调用方
list.forEach(Fruit::getName);
list.forEach(a->a.getName());//相当于

因为foreach只能消费一个参数,再举个消费两个参数 例子。

加一个静态方法,意思是执行BiFunction,把参数a,b传入BiFunction,得到一个结果。

        public static String print(String a, String b, BiFunction<String,String,String> biFunction){
            String apply = biFunction.apply(a, b);
            return apply;
        }

 BiFunction接口方法参数有两个

public interface BiFunction<T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param t the first function argument
     * @param u the second function argument
     * @return the function result
     */
    R apply(T t, U u);

执行结果打印出“HelloWorld”,String的concat实例方法参数只有一个,BiFunction接口方法参数有两个,为什么能使用? 

        Fruit.print("Hello","World",String::concat);

上面语句等价于,看到这应该明白了,(a,b)->a.concat(b) 简写成了 String::concat。接口参数a和b,第一个参数a作为了函数调用方,a调用自己concat,第二个接口参数作为concat的参数。

 Fruit.print("hello","world",(a,b)->a.concat(b));

 所以在用 双冒号 调用 对象的实例方法时,一定要理解 第一个lambda接口参数被用做了 实例方法调用方,其他接口参数 会被用作 实例方法的参数

再举个例子,跟上面意思一样

        Stream<String> stream = Stream.of("a", "b", "c");
        stream.reduce((a,b)->a.concat(b));
        stream.reduce(String::concat);

双冒号调用构造方法 

构造方法也一样,一定要使用接口参数,

假设Fruit构造方法传参是Fruit

        public Fruit(Fruit name) {
        }

就能这么用

        Fruit fruit1 = new Fruit("1");
        Fruit fruit2 = new Fruit("2");
        Fruit fruit3 = new Fruit("3");
        List<Fruit> list = new ArrayList<Fruit>() {{
            add(fruit1);
            add(fruit2);
            add(fruit3);
        }};

        list.forEach(Fruit::new);

<?extends T> <? super T>

参考 Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同? - 知乎

假设Fruit是基类,Apple、Banana是子类。

如果没有<? extends Fruit>, List<Fruit>list是不能指向子类集合。

List<? extends Fruit> list 代表list中的元素是Fruit的子类,好处是让list既可以指向 apple集合,也能指向Banana集合。

List<? extends Fruit> list = new ArrayList<Apple>();

list = new ArrayList<Banana>();

<? extends Furit>的副作用,list不能add元素,因为你不知道list指向的是apple集合还是banana。

<? extends Furit>只能从中get元素,拿到的是Fruit类型或者Fruit的父类。

List<? super Fruit> list,代表集合中的元素是Fruit的父类,副作用是list只能add Fruit及其子类,get只能拿到Object类型。

Stream

参考 java8 Stream的实现原理 (从零开始实现一个stream流) - 小熊餐馆 - 博客园

Java8的Stream流详解_长风-CSDN博客_java stream流操作

stream更方便的处理合数据。

先看一些接口

只能举一些例子。

Function,入参一个泛型,出参另一个泛型,类似于 y = F(x)

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}

 BiFunction 类似于 z = F(x,y)

@FunctionalInterface
public interface BiFunction<T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param t the first function argument
     * @param u the second function argument
     * @return the function result
     */
    R apply(T t, U u);

 条件判断

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

 数据容器

public final class Optional<T> {
    /**
     * Common instance for {@code empty()}.
     */
    private static final Optional<?> EMPTY = new Optional<>();

    /**
     * If non-null, the value; if null, indicates no value is present
     */
    private final T value;

stream接口定义挺清晰,Stream接口继承BaseStream,BaseStream还有其他子类,IntStream、LongStream、DoubleStream都差不多,Stream流中的元素是泛型,IntStream里元素是Integer。

 生成流

Collection接口的stream()或parallelStream()方法
静态的Stream.of()、Stream.empty()方法
Arrays.stream(array, from, to)
静态的Stream.generate()方法生成无限流,接受一个不包含引元的函数
静态的Stream.iterate()方法生成无限流,接受一个种子值以及一个迭代函数
Pattern接口的splitAsStream(input)方法
静态的Files.lines(path)、Files.lines(path, charSet)方法
静态的Stream.concat()方法将两个流连接起来

中间操作

filter(Predicate)将结果为false的元素过滤掉


map(fun)转换元素的值,map常用在 把流中类型T变为类型R,一个T变为一个R,如Stream<String> s =Stream.of(new Student()).map(Student::getName),学生类变为String类,流中是学生姓名。

 


flatMap(fun)转换元素的值,和map一样把 流中T类转为R类,区别是flatmap接口参数 要求把一个T元素转为Stream<R>,这样如果有多个T元素就会得到多个Stream<R>对象,flatMap又会把多个Stream<R>合并为一个Stream<R>。如Stream<Student> s= Stream.of(new School()).flatMap(school->school.getStudents().stream()),一个School有很多Student,把School变为Student流,流中是学校里的所有学生。

 


limit(n)保留前n个元素
skip(n)跳过前n个元素
distinct()剔除重复元素
sorted()将Comparable元素的流排序
sorted(Comparator)将流元素按Comparator排序
peek(fun)流不变,但会把每个元素传入fun执行,可以用作调试

收集操作

iterator()
forEach(fun)
forEachOrdered(fun)
可以应用在并行流上以保持元素顺序
toArray()
toArray(T[] :: new)
返回正确的元素类型
collect(Collector)
collect(fun1, fun2, fun3)
fun1转换流元素;fun2为累积器,将fun1的转换结果累积起来;fun3为组合器,将并行处理过程中累积器的各个结果组合起来

reduce和并行流

Optional<T> reduce(BinaryOperator<T> accumulator);把元素按照accumulator 计算,如下面,这种Stream流中未必有元素,所以计算结果可能是空,结果需要用Optional接收(一个数据容器,可以判断容器中是否空、得到里面的值)。

        Stream<Integer> stream = Stream.of(3, 6, 5, 8, 7, 4);
        Optional<Integer> reduce = stream.reduce((a, b) -> a + b);

T reduce(T identity, BinaryOperator<T> accumulator)也是按照accumulator计算,但会有个初值。先计算初值和第一个元素,因为必然有初值,所以结果肯定不是空,直接会得到Integer,不需要Optional。

        Stream<Integer> stream = Stream.of(3, 6, 5, 8, 7, 4);
        Integer reduce = stream.reduce(1, (a, b) -> a + b);

最后,<U> U reduce(U identity,
             BiFunction<U, ? super T, U> accumulator,
             BinaryOperator<U> combiner);需要用并行流才有效果。

流可以通过stream.parallel()设置并行,sequential设置串行。设置为并行后,会用多线程去执行。

流分为多个线程执行,就涉及到了多个线程结果的合并。

        Stream<Integer> stream = Stream.of(3, 6, 5, 8, 7, 4);
        stream.parallel().reduce(0, (a, b) -> a + b, (c, d) -> c + d);

第三个参数就是 combiner 怎么合并两个线程的结果,上面的代码是把线程结果相加,当然根据业务的需要对 两个线程结果合并,比如要求 两个线程要乘2,stream.parallel().reduce(0, (a, b) -> a + b, (c, d) -> 2 * (c + d)); 这样会把两个线程结果相加后成2,再继续和剩下的线程结果计算。

并行流的代码简单debug了下,大概是跟ForkJoinTask有关。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值