JDK8新特性:Lambda表达式概述

本文详细介绍了JDK8中的Lambda表达式及其使用规则,包括语法特点、变量范围及this概念。此外,还深入探讨了Stream API的应用,如创建方式、常用方法及汇聚操作,并举例说明。

概述
  • lambda表达式可以说是实现SAM的语法糖,使得Java编程更加高级,省力
  • lambda表达式可以大大减少代码的冗余
使用规则
  • 接口需要有且仅有一个需要实现的方法

  • 使用方法,以创建新的线程为例

    // 例1
    // jdk8 之前
    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        System.out.println("创建线程。。。");
      }
    });
    thread.start();
    
    // jdk8 之后,使用lambda表达式
    Thread thread = new Thread(() -> System.out.println("创建线程。。。"));
    thread.start();
    
    
    // 例2
    // jdk8 之前
    List<String> list = Arrays.asList("b", "s", "f", "w", "a", "c", "y");
    Collections.sort(list, new Comparator<String>() {
      @Override
      public int compare(String o1, String o2) {
        return o1.compareTo(o2);
      }
    });
    
    // jdk8 之后,使用lambda表达式
    List<String> list = Arrays.asList("b", "s", "f", "w", "a", "c", "y");
    Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
    /*或者*/
    Collections.sort(list, Comparable::compareTo);
    
  • lambda表达式配合stream API可以更加高效的实现业务代码

    /*功能:将list中的所有元素小写*/
    List<String> list = Arrays.asList("Hello", "ArE", "yOU", "DOing");
    // jdk 8之前
    List<String> lowercase = new ArrayList<>();
    for (int i = 0; i < list.size(); i++) {
      lowercase.add(list.get(i).toLowerCase());
    }
    
    // jdk8 之后,使用lambda表达式
    List<String> lowerList = list.stream().map(param ->  param.toLowerCase()).collect(Collectors.toList());
    // 或者
    List<String> lowerList = list.stream().map(String::toLowerCase).collect(Collectors.toList());
    
lambda表达语法
  • 一般语法

    • 参数使用小括号
    • 使用箭头指向实现的代码体
    • 有返回值时最后直接return即🉑️。
    (Type1 param1, Type2 param2, ..., TypeN paramN) -> {
      statment1;
      statment2;
      //.............
      return statmentM;
    }
    
  • 单参数语法

    • 单个参数的情况下,可以省略参数的小括号
    param1 -> {
      statment1;
      statment2;
      //.............
      return statmentM;
    }
    
  • 单语句语法

    • 如果方法体只有一条语句,可以省略大括号
    • 如果有返回值,还可以省略return关键字
    param1 -> statment
    // 等同于
    param1 -> return statment;
    // 等同于
    param1 -> {return statment;}
    
  • 方法引用写法

    • 使用类名/对象实例 + "::"符号 + 方法名 的方式充当接口某方法的实现

      Class or instance :: method
      
Lambda表达式可用变量范围
  • lambda表达式外部的变量,内部的变量和传入的参数变量

    // outSider为外部变量,uuid是内部变量,param 是传递的变量
    String outSider = "outSider";
    List<String> list = Arrays.asList("How", "ArE", "yOU", "DOing");
    List<String> lowerList = list.stream().map(param -> {
      String uuid = UUID.randomUUID().toString();
      return outSider + " --- " + param.toLowerCase() + " --- " + uuid;
    }).collect(Collectors.toList());
    
  • ⚠️注意:使用外部变量时,编译器会为外部变量自动加了final修饰,不能修改其值

  • 因为加了final,如果希望修改,则需要使用对象引用的方式

    AtomicReference<String> outSider = new AtomicReference<>("outSider");
    
lambda表达式中this的概念
  • lambda表达式中的this并不指向其产生的SAM对象,而是指向外部的对象

    public class TestClazz {
        @Test
        public void test2() {
          String outSider = "outSider";
          List<String> list = Arrays.asList("How", "ArE", "yOU", "DOing");
          List<String> lowerList = list.stream().map(param -> {
            System.out.println("==>" + this.getClass().getName());
            return param.toLowerCase();
          }).collect(Collectors.toList());
    		}
    }
    
    输出:
    ==>com.test.TestClazz
    ==>com.test.TestClazz
    ==>com.test.TestClazz
    ==>com.test.TestClazz
    
方法引用和构造器引用
  • 方法引用

    • objectName::instanceMethod 对象实例::方法,将lambda表达式的参数作为方法的参数传入其中

    • ClassName::staticMethod 类名::静态方法,同样是将lambda表达式的参数作为方法的参数传入其中

    • ClassName::instanceMethod 类名::方法,将lambda表达式的第一个参数作为调用方,调用后面的instanceMethod,其余参数作为instanceMethod方法的参数

      第一种:System.out::println 等同于 x->System.out.println(x)

      第二种:Math::max 等同于 (x, y)->Math.max(x,y)

      第三种:String::toLowerCase 等同于 x->x.toLowerCase()

  • 构造器引用

    • 用法:ClassName::new

    • 将lambda表达式的参数作为构造器的参数,构造出新的对象

      eg:BigDecimal::new 等同于 x->new BigDecimal(x)

Stream API

Stream的理解/特点

(1) Stream是元素的集合,类似于Iterator,使用这个集合可以进行多种Iterator无法进行的操作

(2) 支持顺序和并行的对原Stream进行汇聚的操作

public class TestClazz {
  @Test
  public void test3() {
    List<Integer> nums = Lists.newArrayList(1,null,3,4,null,6);
    long count = nums.stream().filter(num -> num != null).count();
    // Integer count = nums.stream().filter(Objects::nonNull).max(Comparable::compareTo).get();
    System.out.println(count);
  }
}

image
解释:图片就是对于Stream例子的一个解析,可以很清楚的看见:原本一条语句被三种颜色的框分割成了三个部分。红色框中的语句是一个Stream的生命开始的地方,负责创建一个Stream实例;绿色框中的语句是赋予Stream灵魂的地方,把一个Stream转换成另外一个Stream,红框的语句生成的是一个包含所有nums变量的Stream,进过绿框的filter方法以后,重新生成了一个过滤掉原nums列表所有null以后的Stream;蓝色框中的语句是丰收的地方,把Stream的里面包含的内容按照某种算法来汇聚成一个值,例子中是获取Stream中包含的元素个数。

使用stream的步骤
  1. 创建Stream;
  2. 转换Stream,每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换);
  3. 对Stream进行聚合(Reduce)操作,获取想要的结果;
创建Stream的方式(两种)
  • 通过使用Stream接口的静态方法:of(…)等 [注意:Java8里接口可以带静态方法]

    1. of()方法 [两个重载的方法,一个接受一个参数,一个接受可变参数]
    // 声明
    public static<T> Stream<T> of(T t);
    public static<T> Stream<T> of(T... values)
    // 使用
    Stream<Integer> integerStream = Stream.of(1, 2, 3, 5);
    Stream<String> stringStream = Stream.of("taobao");
    
    1. generate()方法,创建一个无限长度的stream,所以一般配合limit使用
    // 声明
    public static<T> Stream<T> generate(Supplier<T> s) {
      Objects.requireNonNull(s);
      return StreamSupport.stream(
        new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
    }
    
    // 使用
    Stream.generate(new Supplier<Double>() {
      @Override
      public Double get() {
        return Math.random();
      }
    }).limit(20L).forEach(System.out::println);
    // 或者
    Stream.generate(() -> Math.random()).limit(20L).forEach(System.out::println);
    // 或者
    Stream.generate(Math::random).limit(20L).forEach(System.out::println);
    // 以上三种表达方法一样的
    
    1. iterate() 方法,也是生成无限长度的stream,但与generate方法不一样的是,其元素的生成是重复的利用给定的种子值(seed)调用指定的函数生成的,就是将seed代入到函数并得到结果,然后再将结果代入到函数中得出结果,如此往复
    // 声明
    public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f);
    
    // 使用
    Stream.iterate(2, param -> 2 * param).limit(10).forEach(param -> System.out.print(param + " "));
    // 结果:2 4 8 16 32 64 128 256 512 1024 
    
    1. 其他生成Stream的方法:

      (1)生成空的Stream public static Stream empty()

      (2)将两个Stream连接后返回 Stream.concat(Stream.empty(), Stream.generate(Math::random).limit(20L));

  • 通过Collection接口的默认方法创建:stream()

    • Collection接口实现类的实例对象调用父接口的默认实现stream()方法即可返回对应的Stream对象
    Stream<Integer> stream = Lists.newArrayList(1,2,3,4,6).stream();
    
Stream的集中常用方法
  1. distinct: 对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素;(自定义类型是根据对象的hashCode和equals来去除重复元素的。)
    在这里插入图片描述

  2. filter: 对于Stream中包含的元素使用给定的过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素
    在这里插入图片描述

  3. map: 对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。这个方法有三个对于原始类型的变种方法,分别是:mapToInt,mapToLong和mapToDouble。这三个方法也比较好理解,比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗;
    在这里插入图片描述

  4. flatMap:和map类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中;
    在这里插入图片描述

// flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字
// flatMap给一段代码理解:
Stream<List<Integer>> inputStream = Stream.of(
 Arrays.asList(1),
 Arrays.asList(2, 3),
 Arrays.asList(4, 5, 6)
 );
Stream<Integer> outputStream = inputStream.
flatMap((childList) -> childList.stream());
  1. peek: 生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;
    在这里插入图片描述

  2. limit: 对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素;
    在这里插入图片描述

  3. skip: 返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream;
    在这里插入图片描述

  4. 整体调用例子

List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
int sum = nums.stream().filter(Objects::nonNull).distinct().mapToInt(num -> num * 2).peek(System.out::println).skip(2).limit(4).sum();
System.out.println(“sum is:+ sum);

说明:

这段代码演示了上面介绍的所有转换方法(除了flatMap),简单解释一下这段代码的含义:给定一个Integer类型的List,获取其对应的Stream对象,然后进行过滤掉null,再去重,再每个元素乘以2,再每个元素被消费的时候打印自身,在跳过前两个元素,最后去前四个元素进行加和运算(解释一大堆,很像废话,因为基本看了方法名就知道要做什么了。这个就是声明式编程的一大好处!)。大家可以参考上面对于每个方法的解释,看看最终的输出是什么。

2
4
6
8
10
12
sum is:36

针对多次转换的说明:

可能会有这样的疑问:在对于一个Stream进行多次转换操作,每次都对Stream的每个元素进行转换,而且是执行多次,这样时间复杂度就是一个for循环里把所有操作都做掉的N(转换的次数)倍啊。其实不是这样的,转换操作都是lazy的,多个转换操作只会在汇聚操作(见下节)的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在汇聚操作的时候循环Stream对应的集合,然后对每个元素执行所有的函数。

Stream方法总结
方法名方法作用返回值类型方法种类
count统计个数long终结
forEach逐一处理void终结
collect转化stream成为集合List/Map/…终结
limit取用前几个Stream函数拼接
skip跳过前几个Stream函数拼接
map映射Stream函数拼接
concat组合Stream函数拼接
distinct去重Stream函数拼接
peek添加额外处理函数Stream函数拼接
sorted排序Stream函数拼接
filter过滤Stream函数拼接

sorted 排序 Stream 函数拼接

Stream的3个注意事项:

  • Stream只能操作一次,因为操作一次就会返回一个新的Stream

  • Stream方法返回的是新的流

  • Stream不调用终结方法,中间的操作不会执行

汇聚(Reduce)Stream

概述
  1. 简介:汇聚操作(也称为折叠)是接收一个数据序列为输入,反复使用合并操作,把序列中的元素合并成一个汇总的结果。Stream接口有一些通用的汇聚操作,比如reduce()和collect();也有一些特定用途的汇聚操作,比如sum(),max()和count()。注意:sum方法不是所有的Stream对象都有的,只有IntStream、LongStream和DoubleStream是实例才有。

    例如

    ​ 查找一个数字列表的总和或者最大值,或者把这些数字累积成一个List对象。

  2. 分类

    **可变汇聚:**把输入的元素们累积到一个可变的容器中,比如Collection或者StringBuilder;
    **其他汇聚:**除去可变汇聚剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch;

可变汇聚
  • 可变汇聚只有一个方法:collect,它可以把Stream中需要的元素收集到一个容器中。比如,Collection
// 重载方法1
<R, A> R collect(Collector<? super T, A, R> collector);

// 使用范例
List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).collect(Collectors.toList());

参考链接:Collectors 参考地址

// 重载方法2
/* @param supplier 一个工厂函数,用来生成一个新的容器.
 * @param accumulator 一种添加新元素的、不干涉的、无状态的函数,用于将额外的元素合并到结果中
 * @param combiner 用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)
 */
<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);

// 使用范例
List<Integer> nums = Lists.newArrayList(1, 1, null, 2, 3, 4, null, 5, 6, 7, 8, 9, 10);
List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
  collect(() -> new ArrayList<Integer>(),
          (list, item) -> list.add(item),
          (list1, list2) -> list1.addAll(list2));

// 另一种写法
List<Integer> numsWithoutNull = nums.stream().filter(Objects::nonNull).
                collect(ArrayList::new,
                        ArrayList::add,
                        ArrayList::addAll);

参考链接 Collector 参考地址

其他汇聚
  • reduce方法:最通用的方法,count和sum都可以通过它实现

    • 三种重载实现

      • 参数accumulator的accept()方法的实现需要符合结合律,即 (a op b) op c = a op (b op c)
      • 返回值是Optional类型的值,防止了NullPointerException,因为Optional在初始化赋值的时候进行了检验,如果是null,就抛出NPE。
      Optional<T> reduce(BinaryOperator<T> accumulator);
      
      // 范例
      List<Integer> ints = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
      System.out.println("ints sum is:" + ints.stream().reduce((sum, item) -> sum + item).get());
      
      -  与上面基本一致,不同的是它允许用户提供一个循环计算的初始值,如果Stream为空,就直接返回该值。而且这个方法不会返回Optional,因为其不会出现null值
      
      ```java
      T reduce(T identity, BinaryOperator<T> accumulator);
      
      // 范例
      List<Integer> ints = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
      System.out.println("ints sum is:" + ints.stream().reduce(0, (sum, item) -> sum + item));
    
    <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
    
  • count方法:获取Stream中元素的个数

  • allMatch:是不是Stream中的所有元素都满足给定的匹配条件

  • anyMatch:Stream中是否存在任何一个元素满足匹配条件

  • findFirst: 返回Stream中的第一个元素,如果Stream为空,返回空Optional

  • noneMatch:是不是Stream中的所有元素都不满足给定的匹配条件

  • max和min:使用给定的比较器(comparator),返回Stream中的最大|最小值

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值