java函数式编程

java函数式编程,第1张

java函数式编程

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

目录

函数式编程、lambda

冒号

双冒号调用静态方法

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

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

双冒号调用构造方法 

 "><?extends T> 

Stream

先看一些接口

 生成流

中间 *** 作

收集 *** 作

reduce和并行流


函数式编程、lambda

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

以List.foreach为例

  

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

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

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

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

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

如果接口方法需要返回值

        FutureTask futureTask = new FutureTask(()->{return "";});
        FutureTask 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())编译通过,这说明双冒号调用静态方法,必须要把接口参数 传入静态方法,不能不使用接口参数。但lamda表达式很明显用不用参数不影响
        Fruit fruit1 = new Fruit("1");
        Fruit fruit2 = new Fruit("2");
        Fruit fruit3 = new Fruit("3");
        List list = new ArrayList() {{
            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 biFunction){
            String apply = biFunction.apply(a, b);
            return apply;
        }

 BiFunction接口方法参数有两个

public interface BiFunction {

    
    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 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 list = new ArrayList() {{
            add(fruit1);
            add(fruit2);
            add(fruit3);
        }};

        list.forEach(Fruit::new);
<?extends T>

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

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

如果没有, Listlist是不能指向子类集合。

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

List list = new ArrayList();

list = new ArrayList();

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

只能从中get元素,拿到的是Fruit类型或者Fruit的父类。

List 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 {

    
    R apply(T t);
}

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

@FunctionalInterface
public interface BiFunction {

    
    R apply(T t, U u);

 条件判断

@FunctionalInterface
public interface Predicate {

    
    boolean test(T t);

 数据容器

public final class Optional {
    
    private static final Optional EMPTY = new Optional<>();

    
    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)转换元素的值,可以用方法引元或者lambda表达式
flatMap(fun)若元素是流,将流摊平为正常元素,再进行元素转换
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 reduce(BinaryOperator accumulator);把元素按照accumulator 计算,如下面,这种Stream流中未必有元素,所以计算结果可能是空,结果需要用Optional接收(一个数据容器,可以判断容器中是否空、得到里面的值)。

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

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

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

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

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

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

        Stream 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有关。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5682824.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)