阅读 527

一文讲明白Lambda,函数式接口,方法引用,Stream流

[toc]


前言

"What->Why->How"是人类探索学习的思维过程,即心理学的解决过程,本文使用WWW思维过程,和大家学习一下JDK1.8的内容.

Java SE 8.0 / 1.8 Spider(蜘蛛)发布于 2014-03-18 ,现在已经是2021年了,相信还有很多同学不会使用jdk8的新特性,我们公司也是从去年才将jdk1.7升级为jdk 8.没实践过,不会很正常.但jdk1.8是个长期维护的大版本,虽然 JDK 现在已经更新到JDK15了(15.0.2 January 19, 2021 ).但是jdk8的使用率依然占有很大的比例.

话不多说,我们开始吧.

Lambda表达式

What

  • Lambda表达式基于数学中的λ演算得名, Lambda 表达式(lambda expression)是一个匿名函数

  • Lambda 一个希腊字母。拉姆达 Λ Lambda(大写Λ,小写λ),是第十一个希腊字母

  • Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。 Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。 使用 Lambda 表达式可以使代码变的更加简洁紧凑。

  • 语法

    • //在没有Lambda表达式的时代
      //通常使用匿名内部类来实现
          new Thread(new Runnable(){
              @Override
              public void run(){
                  // statement
              }
          })
      //可以使用lambda简化为
          new Thread(()->{statement});
      
      (parameters) -> expression 或
      (parameters) ->{ statements; }
      
      请往下看具体特征
      复制代码
  • 特征

    • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。

    • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。

    • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。

    • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需 要指定明表达式返回了一个数值。

    • lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内 部修改定义在域外的局部变量,否则会编译错误

    • //1. 不需要参数,返回值为 5
      () -> 5
      // 2. 接收一个参数(数字类型),返回其 2 倍的值
      x -> 2 * x
      // 3. 接受 2 个参数(数字),并返回他们的差值
      (x, y) -> x – y
      // 4. 接收 2 个 int 型整数,返回他们的和
      (int x, int y) -> x + y
      // 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回 void)
      (String s) -> System.out.print(s)
      复制代码

Why

  • Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。 在上面例子中,我们使用各种类型的 Lambda 表达式来定义 MathOperation 接口的方法。 然后我们定义了 sayMessage 的执行。 -
  • Lambda 表达式免去了使用匿名方法的麻烦,并且给予 Java 简单但是强大的函数化 的编程能力。

How

Talk is cheap,Show me the Code!

public class LambdaTest {


    /**
     * 首先定义一个接口 字符串消费者
     * <p>
     * 只有一个方法,消费字符串
     */
    @FunctionalInterface
    interface StringComsumer {
        /**
         * 消费字符串
         *
         * @param string
         */
        void consumeString(String string);
    }


    public static void main(String[] args) {
        //要被消费的字符串
        final String helloWorld = "Hello World!!!";

        //不用lambda表达式 想实现这个接口要么写个实现类,要么得用匿名类
        StringComsumer stringComsumer = new StringComsumer() {
            @Override
            public void consumeString(String string) {
                System.out.println(string);
            }
        };

        //用lambda表达式就这么写
        StringComsumer stringComsumerWithLambda = string -> System.out.println(string);
        //还可以使用方法引用简化
        StringComsumer stringConsumerWithLambdaSimplify = System.out::println;

        stringComsumer.consumeString(helloWorld);
        stringComsumerWithLambda.consumeString(helloWorld);
        stringConsumerWithLambdaSimplify.consumeString(helloWorld);
    }
}
复制代码

函数式接口

What

  • 函数式接口 FunctionalInterface 也叫功能性接口

  • 函数式接口【FunctionalInterface】是整个Lambda表达式的一个根源,换句话来说java8中的Lambda表达式要想彻底掌握,前提是要彻底理解好函数式接口.

  • Java中的lambda无法单独出现,它需要一个函数式接口来盛放,lambda表达式方法体其实就是函数接口的实现. (即每个lambda表达式 都是一个函数式接口的实现)

  • @FunctionalInterface //添加此注解后,接口中只能有一个抽象方法。但是可以有默认方法和静态方法
    public interface A {
    	void call();
    }
    复制代码
  • jdk默认提供的函数式接口

HOW

  • java 8提供 @FunctionalInterface作为注解,这个注解是非必须的,只要接口符合函数式接口的标准(即只包含一个方法的接口),虚拟机会自动判断, 但 最好在接口上使用注解@FunctionalInterface进行声明,以免团队的其他人员错误地往接口中添加新的方法

  • 可以使用默认方法改进函数式接口

  • public class FunctionalInterfaceTest {
    
    
        /**
         * 首先定义一个接口 字符串消费者
         * <p>
         * 只有一个方法,消费字符串
         */
        @FunctionalInterface
        interface StringComsumer {
            /**
             * 消费字符串
             *
             * @param string
             */
            void consumeString(String string);
    
            /**
             * 使用默认方法强化函数式接口
             *
             * @param after
             * @return
             */
            default StringComsumer andThen(StringComsumer after) {
                return (string) -> {
                    consumeString(string);
                    after.consumeString(string);
                };
            }
        }
    
    
        public static void main(String[] args) {
            //要被消费的字符串
            final String helloWorld = "Hello World!!!";
    
            //用lambda表达式就这么写
            StringComsumer stringComsumerWithLambda = string -> System.out.println(string);
    
            //调用强化后的函数式接口实现
            stringComsumerWithLambda.andThen(string -> System.out.println(helloWorld.toUpperCase()))
                    .consumeString(helloWorld);
        }
    }
    复制代码

方法引用

WHAT

  • 其实是lambda表达式的一种简化写法。所引用的方法其实是lambda表达式的方法体实现,语法也很简单,左边是容器(可以是类名,实例名),中间是"::",右边是相应的方法名
  • 一般方法的引用格式:
    1. 如果是静态方法,则是ClassName::methodName。如 Object ::equals
    2. 如果是实例方法,则是Instance::methodName。如Object obj=new Object();obj::equals;
    3. 构造函数.则是ClassName::new

WHY

  • 简化lambda语法,比lambda可读性更高
  • 方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法
  • 方法引用提供了一种引用而不执行方法的方式

HOW

代码示例

  1. 引用静态方法

    • public class MethodReferences {
      
          @SneakyThrows
          public static void main(String[] args) {
              //因为Runnable 等于一个无参无返回值的方法,所以可以用lambda表达出来,并用方法引用简化
              Runnable runnableLambda = ()->{};
              //创建一个Runnable,引用MethodReferences的静态方法,但不立即执行
              Runnable runnable = MethodReferences::print;
              new Thread(runnable).start();
              TimeUnit.MILLISECONDS.sleep(200);
          }
      
          public static void print() {
              System.out.println("run");
          }
      }
      复制代码
  2. 引用构造方法

    • public class MethodReferences {
      
          @SneakyThrows
          public static void main(String[] args) {
              //创建一个Runnable,引用MethodReferences的构造方法,但不立即执行
              Runnable runnable = MethodReferences::new;
              new Thread(runnable).start();
              TimeUnit.MILLISECONDS.sleep(200);
          }
      
          public MethodReferences() {
              System.out.println("构造方法");
          }
      }
      复制代码
    1. 引用实例方法
    • public class MethodReferences {
      
          @SneakyThrows
          public static void main(String[] args) {
              MethodReferences methodReferences = new MethodReferences();
              Runnable runnable = methodReferences::print;
              new Thread(runnable).start();
              TimeUnit.MILLISECONDS.sleep(200);
          }
      
          public void print() {
              System.out.println("run");
          }
      }
      复制代码
    1. 方法引用快速打印1->10
    • public class MethodReferences {
      
          public static void main(String[] args) {
              AtomicInteger atomicInteger = new AtomicInteger();
              //这行可以不用,用来演示的
              Supplier supplier = atomicInteger::incrementAndGet;
              Stream.generate(atomicInteger::incrementAndGet)
                      .limit(10)
                      .forEach(System.out::println);
          }
      }	
      复制代码
    • 这里可以在Stream接口的1053行实现类打个断点,验证无限消费是否真的无限,是否只有最后操作才会执行中间操作 Advice


Stream(java.util.stream.Stream)

What

  • Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。

  • Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。

  • 官方API描述

    • A sequence of elements supporting sequential and parallel aggregate operations.

      支持顺序和并行聚合的元素序列操作。

  • Stream(流)是一个来自数据源的元素队列并支持聚合操作

    • 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
    • 数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
    • 聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。
    • Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
    • 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
  • 特点

    1. 不是数据结构,不会保存数据
    2. 不会修改原来的数据源,它会将操作后的数据保存到另外一个对象中。
    3. 惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行,需要等到执行终止操作的时候才会进行实际的计算。 当遇到最终操作时,流才开始真正的执行,如果执行了FindFirst,之后的操作都不会执行

Why

  • Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
  • 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性.
  • 流机器(动画来自 Tagir Valeev)
  • Stream的效果就像上图展示的它可以先把数据变成符合要求的样子(map),吃掉不需要的东西(filter)然后得到需要的东西(collect)。

How

  1. 创建流的方式

    • 流可以从集合中创建,也可以从数组中创建,也可以手动构建或提供supplier,还可以使用无限流和两个流连接,并且初始阶段都可以轻松的转为并行流多线程执行,

      package cn.snx.test;
      
      import lombok.SneakyThrows;
      
      import java.nio.charset.Charset;
      import java.nio.file.Files;
      import java.nio.file.Path;
      import java.nio.file.Paths;
      import java.util.Arrays;
      import java.util.List;
      import java.util.Random;
      import java.util.regex.Pattern;
      import java.util.stream.*;
      
      /**
       * 创建流的方式
       *
       * @author :snx at 2021/2/2 15:56
       */
      public class StreamCreate {
          @SneakyThrows
          public static void main(String[] args) {
              //元数据
              String[] strings = {"a", "b", "c", "d"};
              //从数组中创建流
              Stream<String> stream = Arrays.stream(strings); //abcd
              Stream<String> stream1 = Stream.of(strings);//abcd
              //集合创建流
              List<String> stringList = Arrays.asList(strings);
              Stream<String> stream2 = stringList.stream();//abcd
      
              //创建无限流,并限制10个元素
              Stream<String> stream3 = Stream.generate(() -> "e").limit(10);//eeeeeeeeee
              //流构造器
              Stream<Object> stream4 = Stream.builder().add("aaa").add("bbb").build();//aaa bbb
              //单个元素流
              Stream<String> stream5 = Stream.of("aaa");//aaa
              //连接两个流,必须是未开启的流,使用过的流会报错
              Stream<String> stream6 = Stream.concat(stream3, stream5);//eeeeeeeeee aaa
              //空Stream,一般用于处理空指针,无数据返回空流遍历不会空指针
              Stream stream7 = Stream.empty();
              //无限流,使用limit截取10个
              Stream<Integer> stream8 = Stream.iterate(20, (e) -> e + 2).limit(10);//20,22,24,26,28,30,32,34,36,38
      
              //字符串创建char流
              IntStream stream9 = "abc".chars();//abc
              //有序的范围流,不包含最后节点
              IntStream stream10 = IntStream.range(1, 3);//1,2
              //有序的范围流,包含最后节点
              LongStream stream11 = LongStream.rangeClosed(1, 3);//1,2,3
              Random random = new Random();
              //获取三个随机数
              DoubleStream stream12 = random.doubles(3);//0.9123...,0.2123....,0.2213....
              //特殊方式,正则中创建
              Stream<String> stream13 = Pattern.compile(", ").splitAsStream("a, b, c");//a,b,c
      
              Path path = Paths.get("C:\\file.txt");
              //文件行流
              Stream<String> stream14 = Files.lines(path);
              //带编码格式的文件行流
              Stream<String> stream15 = Files.lines(path, Charset.forName("UTF-8"));
              //创建流的方法理论上有无限个,因为可以使用StreamSupport类的方法创建流,以上只是展示一些jdk提供的创建方式,方便使用
              Stream<String> stream16 = StreamSupport.stream(stringList.spliterator(), false);//a,b,c,d
          }
      }
      复制代码
    • IntStream LongStream DoubleStream 用于 减轻了不必要的自动装箱,从而提高了效率

  2. 中间操作

    • 惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行
    1. filter(Predicate) 将结果为false的元素过滤掉
    2. map(fun) 转换元素的值,可以用方法引元或者lambda表达式
    3. flatMap(fun) 若元素是流,将流摊平为正常元素,再进行元素转换
    4. limit(n) 保留前n个元素
    5. skip(n) 跳过前n个元素
    6. distinct() 剔除重复元素
    7. sorted() 将Comparable元素的流排序
    8. sorted(Comparator) 将流元素按Comparator排序
    9. peek(fun) 流不变,但会把每个元素传入fun执行,可以用作调试
  3. 流的Terminal方法(终结操作)

    1. 约简操作
      1. max(Comparator)
      2. min(Comparator)
      3. count()
      4. findFirst() 返回第一个元素
      5. findAny() 返回任意元素
      6. anyMatch(Predicate) 任意元素匹配时返回true
      7. allMatch(Predicate) 所有元素匹配时返回true
      8. noneMatch(Predicate) 没有元素匹配时返回true
      9. reduce(fun) 从流中计算某个值,接受一个二元函数作为累积器,从前两个元素开始持续应用它,累积器的中间结果作为第一个参数,流元素作为第二个参数
      10. reduce(a, fun) a为幺元值,作为累积器的起点
      11. reduce(a, fun1, fun2)
    • 与二元变形类似,并发操作中,当累积器的第一个参数与第二个参数都为流元素类型时,可以对各个中间结果也应用累积器进行合并,但是当累积器的第一个参数不是流元素类型而是类型T的时候,各个中间结果也为类型T,需要fun2来将各个中间结果进行合并
  4. 收集操作

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


import lombok.*;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 流的一些基础操作
 *
 * @author :snx at 2021/2/2 15:56
 */
@Getter
@Setter
@Builder
@ToString
public class StreamSample {

    /**
     * 姓名
     */
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    @SneakyThrows
    public static void main(String[] args) {
        //创建测试数据数组
        StreamSample[] samples = {
                StreamSample.builder().name("aa").age(15).build(),
                StreamSample.builder().name("bb").age(17).build(),
                StreamSample.builder().name("cc").age(15).build(),
                StreamSample.builder().name("dd").age(20).build(),
                StreamSample.builder().name("ee").age(16).build(),
        };
        //Array创建集合
        List<StreamSample> sampleList = Arrays.asList(samples);

        //下面演示集中常用的操作
        //1.遍历打印,调用对象的toString方法(由lombok生成的ToString)
        Arrays.stream(samples).forEach(System.out::println);
        //2.过滤年龄大于16岁的 案例
        Arrays.stream(samples).filter(e -> e.getAge() > 16).forEach(System.out::println);
        //3.打印年龄大于16岁数量
        System.out.println(Arrays.stream(samples).filter(e -> e.getAge() > 16).count());

        //上面操作都是从数组中创建流,下面从集合中创建
        //1.没有中间操作,直接获取数量,方法效果同List.size()
        sampleList.stream().count();
        //2.跳过前两个元素,且最多遍历2次
        sampleList.stream().skip(2).limit(2).forEach(System.out::println);
        //3.获取案例中所有的年龄,先通过map将对象转为年龄,然后收集为Set,自动去重
        Set<Integer> ageSet = sampleList.stream().map(StreamSample::getAge).collect(Collectors.toSet());
        //4.获取所有案例年龄总和
        sampleList.stream().map(StreamSample::getAge).reduce(0, (a, b) -> a + b);
        //5.按年龄排序
        sampleList.stream().sorted((a, b) ->
                Integer.compare(a.getAge(), b.getAge())
        ).forEach(System.out::println);
        //5.1 直接排序年龄
        sampleList.stream().map(StreamSample::getAge).sorted(Integer::compareTo).forEach(System.out::println);
        //6. 分组,按年龄分组,每组含有1个或多个案例
        Map<Integer, List<StreamSample>> ageGroup = sampleList.stream()
                .collect(
                        Collectors.groupingBy(StreamSample::getAge)
                );

        /*
            以上演示了比较常用的流操作方法,展示了一些中间操作和最终操作.
            这个东西是活学活用的,先学会这些基础的,自己灵活运用,最终就能举一反三,写出更流畅的代码.
         */
    }
}
复制代码

参考资料

lambda

www.runoob.com/java/java8-…

<<java工程师成神之路3.0>> --Hollis

方法引用

www.cnblogs.com/xiaoxi/p/70…

FunctionalInterface

www.cnblogs.com/webor2006/p…

www.cnblogs.com/webor2006/p…

www.cnblogs.com/jianwei-dai…

stream

www.runoob.com/java/java8-…

segmentfault.com/a/119000001…

blog.csdn.net/lixiaobuaa/…

文章分类
后端
文章标签