java全端课--Lambda表达式和StreamAPI

62 阅读15分钟

一、Lambda表达式和StreamAPI

新特性的问题:

  • Java5是一个里程碑版本,它引入了很多新特性:foreach,注解,枚举等等
  • Java8是另一个里程碑版本,它引入了很多新特性:第3代日期时间API,接口中增加了静态方法和默认方法,Lambda表达式和StreamAPI
  • Java9-17之间,陆续引入了:记录类、密封类、switch表达式、isntanceof模式匹配等

1.1 Lambda表达式

1.1.1 什么是Lambda表达式

Lambda表达式是一种语法糖,它是为了简化函数式接口匿名内部类代码而设计的语法。本质上仍然是一种匿名内部类的原理。

1.1.2 什么是是函数式接口

函数式接口简称为SAM(Single Abstract Method)接口,即只有1个抽象方法需要重写的接口。这里没有限制接口的静态方法、默认方法等的数量。Java建议我们只对标记了@FunctionalInterface注解的函数式接口使用Lambda表达式,因为这些接口才有匿名内部类实现它的应用场景。

回忆:

java.lang.Comparable<T>接口  int compareTo(T t)//没有匿名内部类实现它的场景
java.util.Comparator<T>接口  int compare(T t1, T t2)//匿名内部类实现它的场景
java.util.Predicate<T>接口   boolean test(T t)//匿名内部类实现它的场景
.....
java.lang.Iterable<T>接口   只有一个抽象方法,但是没有匿名内部类实现它的场景
java.util.Iterator<T>接口   多个抽象方法
java.util.Collection<E>接口  多个抽象方法
java.util.Set接口           多个抽象方法
java.util.List接口		多个抽象方法
java.util.Map接口			多个抽象方法
java.util.Queue接口		多个抽象方法
 ....

3.1.3 Lambda表达式的语法

(形参列表) -> {方法体或Lambda体;}

//这里要写形参列表的意义,是为了我们自己给形参取名字。类型是不能改。方法体是完成功能的。

Lambda表达式的意义,在于让Java原来一切以“对象”为中心的编程方式,增加了可以以“函数或方法”为中心的编程方式。

如果按照之前的面向对象的语法,为了传递一段代码,我们也不得不new一个对象出来,哪怕这个对象的类是匿名的。这样使得代码很繁琐。

Lambda表达式还可以再简化:

  • 当抽象方法的形参类型可以确定或可以根据泛型自动推断的话,形参类型可以省略。
  • 当 {方法体或Lambda体;}只有1个语句,那么这个语句的;,和{}可以省略。如果这个语句是一个return语句,那么return也要一起省略。
  • 当形参列表只有1个形参,且类型省略的情况下,()可以省略。当然,如果形参不止1个或类型没有省略的情况,()不能省略。
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "hello","java","world","chai");
System.out.println("排序之前:" + list);

//按照字符串的长短进行排序,从短到长,长度相同,按照字符的编码值顺序排列
Comparator<String> c = new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
        int result =  Integer.compare(o1.length(), o2.length());
        return result != 0 ? result : o1.compareTo(o2);//这里的compareTo是String类的
    }
};

list.sort(c);
System.out.println("排序之后:" + list);
  
//=========== 使用表达式===============

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "hello","java","world","chai");
System.out.println("排序之前:" + list);

//按照字符串的长短进行排序,从短到长
// Comparator<String> c = (String o1, String o2) -> {return Integer.compare(o1.length(), o2.length());};//原型
//        Comparator<String> c =  (o1, o2) -> {return Integer.compare(o1.length(), o2.length());};//省略形参类型
Comparator<String> c =  (o1, o2) -> Integer.compare(o1.length(), o2.length());//省略return,; ,{}
list.sort(c);

//再简化
list.sort((o1, o2) -> Integer.compare(o1.length(), o2.length()));
//这里编写Lambda表达式就是为了给sort方法传递一段代码,它代表了排序规则
//这段代码是给sort方法内部使用的的
System.out.println("排序之后:" + list);

1.1.4 经典的函数式接口

比较器:Comparator

Java8在java.util.function包引入了很多新的函数式接口。它们大致可以分为4类。

1、判断型接口

经典代表:

Predicate<T>接口,  抽象方法 boolean test(T t)

它的抽象方法,有形参,有返回值,但是返回值类型是固定的boolean类型。相当于你在这个抽象方法中,一定是编写一个条件,用于判断形参t是不是满足xx条件。

序号接口名抽象方法描述
1Predicateboolean test(T t)接收一个对象
2BiPredicate<T,U>boolean test(T t, U u)接收两个对象
3DoublePredicateboolean test(double value)接收一个double值
4IntPredicateboolean test(int value)接收一个int值
5LongPredicateboolean test(long value)接收一个long值
2、消费型接口

经典代表:

Consumer<T>接口,抽象方法 void accept(T t)  

它的抽象方法,有形参,无返回值,返回值类型固定是void类型。相当于你在这个抽象方法中“吃”掉了这个形参。

序号接口名抽象方法描述
1Consumervoid accept(T t)接收一个对象用于完成功能
2BiConsumer<T,U>void accept(T t, U u)接收两个对象用于完成功能
3DoubleConsumervoid accept(double value)接收一个double值
4IntConsumervoid accept(int value)接收一个int值
5LongConsumervoid accept(long value)接收一个long值
6ObjDoubleConsumervoid accept(T t, double value)接收一个对象和一个double值
7ObjIntConsumervoid accept(T t, int value)接收一个对象和一个int值
8ObjLongConsumervoid accept(T t, long value)接收一个对象和一个long值
package com.mytest.lambda;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.function.Consumer;

public class TestLambda3 {
    @Test
    public void test1(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world","chai");

        //遍历上述集合,查看每一个单词的长度
        //(1)foreach循环(复习)
        for (String s : list) {
            System.out.println(s +"的长度:" + s.length());
        }
    }

    @Test
    public void test2(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world","chai");

        //遍历上述集合,查看每一个单词的长度
        //(2)迭代器Iterator(复习)
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            String s = iterator.next();
            System.out.println(s +"的长度:" + s.length());
        }
    }

    @Test
    public void test3(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world","chai");

        //遍历上述集合,查看每一个单词的长度
        //(3)forEach方法
        Consumer<String> c = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s +"的长度:" + s.length());
            }
        };//匿名内部类写法
        list.forEach(c);
    }

    @Test
    public void test4(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world","chai");

        //遍历上述集合,查看每一个单词的长度
        //(3)forEach方法
//        Consumer<String> c = (String s) ->{System.out.println(s +"的长度:" + s.length());};//Lambda表达式原型
       /* Consumer<String> c = s ->System.out.println(s +"的长度:" + s.length());//Lambda表达式简化
        list.forEach(c);*/

        list.forEach(s ->System.out.println(s +"的长度:" + s.length()));
    }
}
3、功能型接口

经典代表:

Function<T,R>,抽象方法 R apply(T t) 

它的抽象方法,有形参,有返回值,返回值类型不固定。返回值类型与形参类型可能不一致,可能一致。相当于你可以在这个抽象方法中,对形参做修改操作。

它有很多兄弟姐妹,子孙后代。例如:

UnaryOperator<T>,抽象方法 T apply(T t)  //在抽象方法中,修改形参的值,不改类型

它们的家族:

  • 接口名以Function或Operator结尾
    • 以Operator结尾,它的抽象方法的形参类型与返回值类型是一样的
  • Bi开头或包含Binary,抽象方法的形参都是2个
  • 以ToXxx,表示返回值是Xxx
序号接口名抽象方法描述
1Function<T,R>R apply(T t)接收一个T类型对象,返回一个R类型对象结果
2UnaryOperatorT apply(T t)接收一个T类型对象,返回一个T类型对象结果
3DoubleFunctionR apply(double value)接收一个double值,返回一个R类型对象
4IntFunctionR apply(int value)接收一个int值,返回一个R类型对象
5LongFunctionR apply(long value)接收一个long值,返回一个R类型对象
6ToDoubleFunctiondouble applyAsDouble(T value)接收一个T类型对象,返回一个double
7ToIntFunctionint applyAsInt(T value)接收一个T类型对象,返回一个int
8ToLongFunctionlong applyAsLong(T value)接收一个T类型对象,返回一个long
9DoubleToIntFunctionint applyAsInt(double value)接收一个double值,返回一个int结果
10DoubleToLongFunctionlong applyAsLong(double value)接收一个double值,返回一个long结果
11IntToDoubleFunctiondouble applyAsDouble(int value)接收一个int值,返回一个double结果
12IntToLongFunctionlong applyAsLong(int value)接收一个int值,返回一个long结果
13LongToDoubleFunctiondouble applyAsDouble(long value)接收一个long值,返回一个double结果
14LongToIntFunctionint applyAsInt(long value)接收一个long值,返回一个int结果
15DoubleUnaryOperatordouble applyAsDouble(double operand)接收一个double值,返回一个double
16IntUnaryOperatorint applyAsInt(int operand)接收一个int值,返回一个int结果
17LongUnaryOperatorlong applyAsLong(long operand)接收一个long值,返回一个long结果
18BiFunction<T,U,R>R apply(T t, U u)接收一个T类型和一个U类型对象,返回一个R类型对象结果
19BinaryOperatorT apply(T t, T u)接收两个T类型对象,返回一个T类型对象结果
20ToDoubleBiFunction<T,U>double applyAsDouble(T t, U u)接收一个T类型和一个U类型对象,返回一个double
21ToIntBiFunction<T,U>int applyAsInt(T t, U u)接收一个T类型和一个U类型对象,返回一个int
22ToLongBiFunction<T,U>long applyAsLong(T t, U u)接收一个T类型和一个U类型对象,返回一个long
23DoubleBinaryOperatordouble applyAsDouble(double left, double right)接收两个double值,返回一个double结果
24IntBinaryOperatorint applyAsInt(int left, int right)接收两个int值,返回一个int结果
25LongBinaryOperatorlong applyAsLong(long left, long right)接收两个long值,返回一个long结果
package com.mytest.lambda;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.UnaryOperator;

public class TestLambda4 {
    @Test
    public void test1(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world","chai");

        //要将上述单词转为大写
        UnaryOperator<String> u = new UnaryOperator<String>() {
            @Override
            public String apply(String s) {
                return s.toUpperCase();
            }
        };//匿名内部类写法
        list.replaceAll(u);
    }
}
4、供给型接口

经典代表:

Supplier<T> ,抽象方法 T get()  

它的抽象方法,没有形参,有返回值。返回值类型不固定。相当于你在这个抽象方法中,返回了一个结果,但是不需要人家给你任何参数。属于奉献型接口。

序号接口名抽象方法描述
1SupplierT get()返回一个对象
2BooleanSupplierboolean getAsBoolean()返回一个boolean值
3DoubleSupplierdouble getAsDouble()返回一个double值
4IntSupplierint getAsInt()返回一个int值
5LongSupplierlong getAsLong()返回一个long值

1.2 方法引用

方法引用也是一种语法糖,它是用于简化Lambda表达式的语法。但是,不是所有的Lambda表达式都能用它进行简化的(就像不是所有匿名内部类都可以使用Lambda表达式简化一样,要有条件)。

当Lambda表达式同时满足以下3个条件时,才能用方法引用进行简化,(如果是编写代码,其实大家不用去记这些条件,因为IDEA都会给你提示):

  • Lambda表达式的{Lambda体}必须只有一个语句,如果{}中有多个语句,不可以。
  • 这个语句还必须是一个类.静态方法(【实参】)或一个对象.实例方法(【实参】)的语句。不能是其他语句,例如 a-b; 这样的表达式
  • 类名.静态方法用到的实参,全部来源于Lambda表达式的形参,没有额外的数据参与。对象.实例方法用到的实参,全部来源于Lambda表达式的形参,没有额外的数据参与。甚至调用实例方法的对象,都是Lambda表达式的第1个形参。

方法引用语法的3种形式:

  • 类名::方法名
  • 对象名::方法名
  • 类名::new :这种情况是属于你的{Lambda体}在调用一个类的构造器创建对象,而且调用构造器用到的实参,全部来源于Lambda表达式的形参
package com.atguigu.reference;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.function.Consumer;

public class TestMethodReference {
    @Test
    public void test1(){

        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world","chai");

        //使用list的forEach方法进行遍历
        //list.forEach((String s) -> {System.out.println(s);});
        //Lambda表达式写法,位简化
        list.forEach(s -> System.out.println(s));//Lambda表达式写法
        /*
        {System.out.println(s);} 只有1个语句
                                这个语句是System.out对象 .println方法
         println方法的实参s来源于 Lambda表达式的形参s
         */
    }

    @Test
    public void test3(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world","chai");

        //使用list的forEach方法进行遍历
        list.forEach(System.out::println);//方法引用
    }
}

1.3 StreamAPI

1.3.1 什么是StreamAPI

这里的Stream流是用于数据加工或数据的统计分析的。Java希望通过一些方法来完成像SQL一样,可以快速的对内存中的数据进行加工处理等操作。

Stream是一个接口,它有很多实现类,但是平时我们不太关注它的具体实现类,一般都是面向接口编程。

1.3.2 StreamAPI的使用有几个特点

  • 操作分为3个步骤

    • 创建Stream:必选
    • 中间的加工处理:可选 0- n
    • 终结操作:必选
  • 这些流操作的中间处理是懒处理,它会等到执行终结操作时,才一并处理。

  • Stream对象是不可变的,只要修改,就会返回新对象

  • Stream流操作不会写修改数据源。就像是SQL中的select语句对数据库的查询一样。

1.3.3 创建Stream对象的API

1、通过集合对象.stream()

2、通过数组工具类Arrays.stream(数组)

3、Stream.generate(Supplier接口的实现类)

4、Stream.of()

5、Stream.iterate(种子, UnaryOperator接口的实现类)

1.3.4 Stream的中间处理

序号方 法描 述
1Stream filter(Predicate p)接收 Lambda , 从流中排除某些元素
2Stream distinct()筛选,通过流所生成元素的equals() 去除重复元素
3Stream limit(long maxSize)截断流,使其元素不超过给定数量
4Stream skip(long n)跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
5Stream peek(Consumer action)接收Lambda,对流中的每个数据执行Lambda体操作
6Stream sorted()产生一个新流,其中按自然顺序排序
7Stream sorted(Comparator com)产生一个新流,其中按比较器顺序排序
8Stream map(Function f)接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
9Stream mapToDouble(ToDoubleFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。
10Stream mapToInt(ToIntFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。
11Stream mapToLong(ToLongFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。
12Stream flatMap(Function f)接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
package com.mytest.stream;

import org.junit.Test;
import java.util.Arrays;
import java.util.Random;
import java.util.stream.Stream;

public class TestMiddle {
  
    //错误操作
  
    @Test
    public void test1(){
        //演示过滤filter
        Stream<Integer> stream = Stream.of(1,2,3,4,5);//选择其中一种方式创建Stream
        //中间处理
        //需求:筛选出偶数查看
        //Predicate<T>接口,抽象方法 boolean test(T t)
        stream.filter( t-> t%2==0);

        //终结操作
        //Consumer<T>接口,抽象方法 void accept(T t)
        stream.forEach(t-> System.out.println(t));
        //报错java.lang.IllegalStateException: stream has already been operated upon or closed
        //因为Stream对象不可变,中间处理后,Stream改变了,必须接收新的Stream
    }

    /正确操作
  
    @Test
    public void test2(){
        Stream<Integer> stream = Stream.of(1,2,3,4,5);//选择其中一种方式创建Stream
        //中间处理
        //需求:筛选出偶数查看
        //Predicate<T>接口,抽象方法 boolean test(T t)
        stream  = stream.filter( t-> t%2==0);

        //终结操作
        //Consumer<T>接口,抽象方法 void accept(T t)
//        stream.forEach(t-> System.out.println(t));
        stream.forEach(System.out::println);
    }

    @Test
    public void test3(){
        //链式编写法
        //用前一步方法的返回值,直接调用后续的方法
        Stream.of(1,2,3,4,5).filter( t-> t%2==0).forEach(System.out::println);
        //forEach方法后面为什么不能继续.了呢?因为forEach的返回值类型,表示无返回值。
    }
}

1.3.5 Stream的终结处理

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void。流进行了终止操作后,不能再次使用。

序号方法的返回值类型方法描述
1booleanallMatch(Predicate p)检查是否匹配所有元素
2booleananyMatch(Predicate p)检查是否至少匹配一个元素
3booleannoneMatch(Predicate p)检查是否没有匹配所有元素
4OptionalfindFirst()返回第一个元素
5OptionalfindAny()返回当前流中的任意元素
6longcount()返回流中元素总数
7Optionalmax(Comparator c)返回流中最大值
8Optionalmin(Comparator c)返回流中最小值
9voidforEach(Consumer c)迭代
10Treduce(T iden, BinaryOperator b)可以将流中元素反复结合起来,得到一个值。返回 T
11Ureduce(BinaryOperator b)可以将流中元素反复结合起来,得到一个值。返回 Optional
12Rcollect(Collector c)将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法

Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。另外, Collectors 实用工具类提供了很多静态方法,可以方便地创建常见收集器实例。

package com.mytest.stream;

import org.junit.Test;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TestEnding {
    @Test
    public void test1(){
        //allMatch方法的参数类型是Predicate接口
        //Predicate接口的抽象方法 boolean test(T t)
        System.out.println("都是偶数?" + Stream.of(1, 2, 3, 4, 5).allMatch(t -> t % 2 == 0));
        System.out.println("有偶数?" + Stream.of(1, 2, 3, 4, 5).anyMatch(t -> t % 2 == 0));
        System.out.println("没有偶数?" + Stream.of(1, 2, 3, 4, 5).noneMatch(t -> t % 2 == 0));
    }
}

1.4 自定义函数式接口(了解)

函数式接口定义的要求有2个:

  • 保证这个接口中有且只有1个抽象方法需要重写
  • 在函数式接口的上方加@FunctionaInterface,用来强制校验

案例:

@FunctionalInterface
public interface Callable {
    void call();
}
package com.mytest.inter;

import org.junit.Test;

public class TestCallable {
    @Test
    public void test1(){
        //可以使用匿名内部类对象,给函数式接口的变量赋值
        Callable c = new Callable() {
            @Override
            public void call() {
                System.out.println("test123");
            }
        };
        c.call();
    }

    @Test
    public void test2(){
        //使用Lambda表达式,给函数式接口的变量赋值
        Callable c = () -> System.out.println("test123");
        c.call();
    }
}