JavaSE(十三)——函数式编程(Lambda表达式、方法引用、Stream流)

函数式编程

函数式编程 是 Java 8 引入的一个重要特性,它允许开发者以函数作为一等公民(first-class citizens)的方式编程,即函数可以作为参数传递给其他函数,也可以作为返回值。 这极大地提高了代码的可读性、可维护性和复用性。函数式编程的核心概念包括高阶函数、Lambda 表达式、函数式接口、流(Streams)和 Optional 类等。

函数式编程的核心是Lambda表达式,它是一种简洁的表示匿名方法的方式。Lambda表达式允许你以更简洁的方式传递代码块作为参数给方法,这是Java 8及以后版本中引入的一个重要特性,它主要用在实现只有一个抽象方法的接口(即函数式接口)时。

Lambda表达式只能替代函数式接口的匿名内部类,所以我们先了解函数式接口。

函数式接口

函数式接口是一个具有且仅有一个抽象方法的接口。 它可以包含默认方法和静态方法,但这些方法不会破坏接口作为函数式接口的性质,因为它们不是抽象方法。

函数式接口的特点

  • 单一抽象方法:只含有一个抽象方法
  • Lambda表达式支持:可以使用Lambda表达式简洁地实现该接口
  • @FunctionalInterface注解 :推荐使用@FunctionalInterface注解来标记一个接口为函数式接口。这有助于编译器检查接口是否符合函数式接口的规范,并在接口中错误地添加了第二个抽象方法时提供编译时错误。

例如,我们常用的Comparetor接口就是函数式接口

  • Comparator接口使用到@FunctionalInterface注释
  • compare方法是Comparator接口唯一的抽象方法,尽管存在其他方法,但都是静态方法和默认方法,不会影响函数式接口的性质

Lambda表达式

Lambda表达式的使用

Lambda表达式用于替代函数式接口的匿名内部类对象,从而让程序更简洁,可读性更好。

基本格式如下:

java 复制代码
(被重写方法的参数列表) -> {  
    被重写方法的方法体代码 
}

【举例演示Lambda表达式】

实现函数式接口:

  • 使用匿名内部类:
java 复制代码
interface ITest1 {
    void function();
}

public class LambdaDemo1 {
    public static void main(String[] args) {
        ITest1 iTest1 = new ITest1() {
            @Override
            public void function() {
                System.out.println("被重写的接口抽象方法...");
            }
        };
    }
}
  • Lambda表达式:
java 复制代码
interface ITest1 {
    void function();
}

public class LambdaDemo1 {
    public static void main(String[] args) {
        ITest1 iTest1 = ()->{System.out.println("被重写的接口抽象方法...");};
    }
}

自定义一个学生类,创建一个学生类对象数组,使用comparator比较器 和 Arrays.sort()方法对其排序(要求:使用匿名内部类和Lambda表达式两种方式实现):

java 复制代码
//自定义学生类
class Student {
    public String name;
    public int age;
    public double score;

    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}


public class LambdaDemo1 {

    //Lambda表达式
    public static void byLambda(Student[] student) {
        Student[] students = new Student[3];
        for (int i = 0; i < students.length; i++) {
            students[i] = student[i];
        }
        System.out.println("排序前:" + Arrays.toString(students));
        Arrays.sort(students, (Student o1, Student o2)->{return o1.age - o2.age;});
        System.out.println("排序后:" + Arrays.toString(students));
    }

    //匿名内部类
    public static void byInnerclass(Student[] student) {
        Student[] students = new Student[3];
        for (int i = 0; i < students.length; i++) {
            students[i] = student[i];
        }
        System.out.println("排序前:" + Arrays.toString(students));
        Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.age - o2.age;
            }
        });
        System.out.println("排序后:" + Arrays.toString(students));
    }


    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student("张三", 19, 98.2);
        students[1] = new Student("李四", 21, 88.5);
        students[2] = new Student("王五", 18, 99.9);

        byInnerclass(students);
        System.out.println("=====================");
        byLambda(students);
    }
  • 匿名内部类中重写的方法的 参数列表 对应 Lambda表达式->的左边; 方法体 对应 Lambda表达式->的右边

Lambda表达式的省略

Lambda 表达式可以进行化简,其规则如下:

  • 参数类型可以省略
  • 如果只有一个参数,参数类型省略的同时 "()" 也可以省略,但多个参数不能省略
  • 如果Lambda表达式中只有一行代码,大括号可以不写,同时分号 ";" 也要省略;如果这行代码是 return语句,也必须去掉return

例如

java 复制代码
//化简前
Arrays.sort(students, (Student o1, Student o2)->{return o1.age - o2.age;});

//化简后
Arrays.sort(students, (o1, o2)->o1.age - o2.age);

对于原Lambda表达式:(Student o1, Student o2)->{return o1.age - o2.age;},首先省略了参数类型,但由于不止一个参数所以小括号不能省略,方法体中只包含一条语句,可以省略大括号并同时省略分号,由于是return语句,省略return。


方法引用

Java中的方法引用 也是Java 8引入的一个特性,它 提供了一种更简洁的方式来引用方法或Lambda表达式。 方法引用可以看作是Lambda表达式的一种特殊形式,其中Lambda表达式仅仅是调用了已存在的方法。使用方法引用可以使代码更加简洁易读。

所以,可以将方法引用理解为对Lambda表达式的再进一步化简,主要分为:

  • 静态方法引用
  • 实例方法引用
  • 特定类型方法引用
  • 构造方法引用

静态方法引用

静态方法引用:通过类名直接引用静态方法。

格式 为:类名::静态方法

应用场景 :如果某个Lambda表达式中只是通过类名调用了一个静态方法,并且->前后参数的形式一致,就可以使用静态方法引用

【示例】

创建一个场景,一个学生类,实现一个静态方法,实现了根据年龄的排序规则,利用Arrays.sort(T[] a, Comparator<? super T> c)方法排序时,Lambda表达式的方法体中仅调用这个静态方法,使用静态方法引用化简:

java 复制代码
class Student {
    public String name;
    public int age;
    public double score;

    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    //静态比较方法
    public static int compareByAge(Student s1, Student s2) {
        return s1.age - s2.age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}

public class LambdaDemo1 {

    public static void byLambda(Student[] student) {
        Student[] students = new Student[3];
        for (int i = 0; i < students.length; i++) {
            students[i] = student[i];
        }
        System.out.println("===== Lambda表达式 =====");
        System.out.println("排序前:" + Arrays.toString(students));
        Arrays.sort(students, (o1, o2)->Student.compareByAge(o1, o2));
        System.out.println("排序后:" + Arrays.toString(students));
    }


    public static void byInnerclass(Student[] student) {
        Student[] students = new Student[3];
        for (int i = 0; i < students.length; i++) {
            students[i] = student[i];
        }
        System.out.println("===== 匿名内部类 =====");
        System.out.println("排序前:" + Arrays.toString(students));
        Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return Student.compareByAge(o1, o2);
            }
        });
        System.out.println("排序后:" + Arrays.toString(students));
    }

    public static void byMethodReference(Student[] student) {
        Student[] students = new Student[3];
        for (int i = 0; i < students.length; i++) {
            students[i] = student[i];
        }
        System.out.println("===== 静态方法引用 =====");
        System.out.println("排序前:" + Arrays.toString(students));
        Arrays.sort(students, Student::compareByAge);
        System.out.println("排序后:" + Arrays.toString(students));
    }

    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student("张三", 19, 98.2);
        students[1] = new Student("李四", 21, 88.5);
        students[2] = new Student("王五", 18, 99.9);
        
        byInnerclass(students);
        byLambda(students);
        byMethodReference(students);
    }
}

实例方法引用

静态方法引用:通过实例对象直接引用实例方法。

格式 为:对象名::实例方法

应用场景 :如果某个Lambda表达式中只是通过对象名调用了一个实例方法,并且->前后参数的形式一致,就可以使用实例方法引用

【示例】

相似的业务场景,只不过将静态方法引用中调用静态方法改为调用一个实例方法,使之成为实例方法引用的场景。


特定类型方法引用

特定类型的方法引用:使用类名来引用该类中任意对象的实例方法,此时Lambda表达式的第一个参数会是该方法的调用者。

格式 为:特定类的名称::方法

应用场景:如果某个Lambda表达式里只是调用一个特定类型的实例方法,并且前面的参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为该实例方法的入参的,此时就可以使用特定类型的方法引用。

【示例】

现有一个字符串数组,要对该数组按字母顺序进行排序,排序时忽略大小写,即"Zh"应该在"ang"的后面:

java 复制代码
public static void main(String[] args) {
        String[] strings = new String[]{"Zh","ang","Job","Aa","bb","Oo"};
        //正常排序,不能实现需求
        String[] strings1 = Arrays.copyOf(strings,strings.length);
        System.out.println("排序前:");
        System.out.println(Arrays.toString(strings1));
        Arrays.sort(strings1);
        System.out.println("排序后:");
        System.out.println(Arrays.toString(strings1));

        //忽略大小写排序
        String[] strings2 = Arrays.copyOf(strings,strings.length);
        System.out.println("排序前:");
        System.out.println(Arrays.toString(strings2));
        Arrays.sort(strings2, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.compareToIgnoreCase(o2);
            }
        });
        System.out.println("排序后:");
        System.out.println(Arrays.toString(strings2));

        //Lambda表达式
        String[] strings3 = Arrays.copyOf(strings,strings.length);
        System.out.println("排序前:");
        System.out.println(Arrays.toString(strings3));
        Arrays.sort(strings3, (o1, o2)->o1.compareToIgnoreCase(o2));
        System.out.println("排序后:");
        System.out.println(Arrays.toString(strings3));

        //特定方法引用
        String[] strings4 = Arrays.copyOf(strings,strings.length);
        System.out.println("排序前:");
        System.out.println(Arrays.toString(strings4));
        Arrays.sort(strings4, String::compareToIgnoreCase);
        System.out.println("排序后:");
        System.out.println(Arrays.toString(strings4));
    }

构造方法引用

构造方法引用:使用类名来引用构造方法

格式 为:类名::new

应用场景 :如果某个Lambda表达式里只是在创建对象,并且->前后参数情况一致,此时就可以使用构造方法引用

【示例】

以下三种方法等价:

java 复制代码
    public static void main(String[] args) {
        //匿名内部类
        ICarFactory carFactory1 = new ICarFactory() {
            @Override
            public Car createOb(String name, double price, String brand) {
                return new Car(name, price, brand);
            }
        };

        //Lambda表达式
        ICarFactory carFactory2 = (name, price, brand) -> new Car(name,price,brand);

        //构造方法引用
        ICarFactory carFactory3 = Car::new;
    }

Stream流

Stream 是JDK8也是引入的新特性,是一套全新的API(java.util.stream.*) ,是函数式编程的重要部分,可以用于操作集合或者数组的数据。Stream流大量结合了Lambda表达式的语法风格,能够简化代码,增强代码可读性。

Stream流的使用步骤:

  1. 获取Stream流
  2. 调用中间方法,处理流上的数据
  3. 调用终止方法

获取Stream流

Stream 本身是一个接口,但集合(如 ListSet 等)和数组可以通过特定的方法或操作来"使用"它,即生成 Stream 实例。

  • 获取集合(Collection下) 的Stream流
Collection提供 解释
default Stream stream() 获取当前集合对象的Stream流

Collection底下的集合类可以直接通过对象实例调用stream方法,但Map底下的不可以。

Map相关集合类需要先调用values()keySet()entrySet()三种方法,再调用stream()方法分别获取值流、键流、键值对流。

示例:

java 复制代码
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.stream();

        Set<Integer> set = new HashSet<>();
        set.stream();

        Map<String,Integer> map = new HashMap<>();
        map.values().stream();
        map.keySet().stream();
        map.entrySet().stream();
    }

  • 获取数组的Stream流

对于数组,Java 8 并没有在数组类型上直接添加 stream() 方法,因为数组是基本类型或者是固定大小的对象集合,并不直接实现 Collection 接口,但可以通过以下两种方法获取数组的Stream流:

Arrays类提供的方法 解释
public static Stream stream(T[] array) 获取当前数组的Stream流
Stream提供的方法 解释
public static Stream of(T... values) 获取当前接收数据的Stream流
  • of方法的参数T...values是可变参数

示例:

java 复制代码
    public static void main(String[] args) {
        String[] strings = new String[10];
        
        Arrays.stream(strings);
        Stream.of(strings);
    }

补充:可变参数

可变参数 (也称为可变长参数或不定参数)是一种允许你传递任意数量参数的方法。这个功能通过 ... 语法在方法定义中指定。使用可变参数,你可以调用方法时传递零个或多个指定类型的参数,而不需要使用数组。这些参数在方法内部被当作一个数组处理。

格式数据类型...参数名称

可变参数可以接收一个或多个数据,也可以直接接收数组,甚至可以不传入任何数据。

注意

  • 一个参数列表只能有一个可变参数
  • 可变参数必须在参数列表的最后面
  • 可变参数在方法内部就是一个数组
  • 如果方法之间存在参数数量不同的重载版本,并且其中一个版本使用了可变参数,那么编译器可能会报告冲突。这是因为编译器可能会将具有固定数量参数的方法调用解释为对可变参数方法的调用。

示例

java 复制代码
    public static void varargs(int a, int...b) {
        System.out.println("可变参数测试:");
        System.out.println(a);
        System.out.println(Arrays.toString(b));
    }

    public static void main(String[] args) {
        int[] array = new int[]{1, 2, 3, 4, 5};

        //不向可变参数传参
        varargs(1);

        //向可变参数传入一个数据
        varargs(1, 2);

        //向可变参数传入多个数据
        varargs(1, 2,3,4,5);
        
        //向可变参数传入数组
        varargs(1, array);
    }

中间方法

中间方法 指的是调用完成后会返回新的Stream流,可以继续使用,即支持链式编程,可以连续使用多个中间方法。

可以将Stream流看作是一个传送带,集合或数组的数据都倒到传送带上,再由中间方法对数据进行筛选。

常用方法:

Stream提供的方法 说明
Stream filter(Predicate<? super T> predicate) 对流中数据进行过滤
Stream sorted() 对元素进行升序排序,T 必须可比较
Stream sorted(Comparator<? super T> comparator) 按照指定规则排序
Stream limit(long maxSize) 获取前maxSize个元素
Stream skip(long n) 跳过前n个元素
Stream distinct(); 去重
Stream map(Function<? super T, ? extends R> mapper) 加工/映射,将流上数据加工后变成新数据再放回流中
public static Stream concat(Stream<? extends T> a, Stream<? extends T> b) 将a流和b流合并为一个流

示例:

java 复制代码
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        Collections.addAll(list, 78,36,88,98,39,56,41,66,78);

        //filter-过滤出60+的数据
        list.stream().filter(s -> s >=60).forEach(System.out::println);

        //sorted-升序排序
        list.stream().sorted().forEach(System.out::println);

        //sorted-指定降序排序
        list.stream().sorted((o1, o2) -> o2 - o1).forEach(System.out::println);

        //limit-获取前3大的数据
        list.stream().sorted((o1, o2) -> o2 - o1).limit(3).forEach(System.out::println);

        //skip-跳过前3大的元素
        list.stream().sorted((o1, o2) -> o2 - o1).skip(3).forEach(System.out::println);

        //distinct-去重
        list.stream().distinct().forEach(System.out::println);

        //map-将所有数据-10
        list.stream().map(s -> s - 10).forEach(System.out::println);

        //concat-合并流
        List<String> list1 = new ArrayList<>();
        Collections.addAll(list1, "a","b","c");
        Stream.concat(list.stream(), list1.stream()).forEach(System.out::println);
    }

终止方法

终止方法 指的是调用完成后,不返回新的Stream流,即无法再继续使用流。

当一个流调用了终止方法后,就不能再使用该流了,否则会抛出IllegalStateException异常!

常用方法:

Stream提供的方法 解释
void forEach(Consumer<? super T> action) 对流运算后的元素执行遍历操作
long count() 统计流运算后的元素个数
Optional max(Comparator<? super T> comparator) 获取流运算后的最大值元素
Optional min(Comparator<? super T> comparator) 获取流运算后的最小值元素

Optional 类是 Java 8 引入的一个容器类,用于包含非空值。Optional 类的引入主要是为了解决空指针异常(NullPointerException)的问题,提供了一种更好的方式来处理可能为 null 的情况。

Optional 类是一个可以包含也可以不包含非 null 值的容器对象。如果值存在,isPresent() 方法将返回 true,调用 get() 方法将返回该对象。


终止方法中有一种收集方法,能将Stream流操作后的结果转回到集合或者数组中。

开发中将数据收集到集合或数组中才会进一步使用,Stream流只是方便操作集合和数组的手段。

方法 解释
<R, A> R collect(Collector<? super T, A, R> collector) 将流处理后的结果集收集到一个指定的集合中
Object[] toArray() 将流处理后的结果集收集到一个数组中

关于collect方法的参数,要关注Collectors工具类提供的具体的收集方式:

方法 解释
public static Collector<T, ?, List> toList() 把元素收集到List集合
public static Collector<T, ?, Set> toSet() 把元素收集到Set集合
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) 把元素收集到Map集合

这三个方法的返回值和参数较为复杂,搭配collect方法使用:

java 复制代码
    public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<>();
        Collections.addAll(list1, 78,36,88,98,39,56,41,66,78);
        List<Student> list2 = new ArrayList<>();
        Collections.addAll(list2, new Student("张三",12), new Student("李四",24 ),
                                  new Student("王五",18));

        //toList
        List<Integer> newList = list1.stream().sorted().collect(Collectors.toList());
        System.out.println(newList);

        //toSet
        Set<Integer> set = list1.stream().collect(Collectors.toSet());
        System.out.println(set);

        //toMap
        Map<String,Integer> map = list2.stream().collect(Collectors.toMap(s -> s.name, s -> s.age));
        System.out.println(map);

        //toArray
        Object[] array = list1.stream().sorted().toArray();
        System.out.println(Arrays.toString(array));
    }

最后小结一下函数式编程的优势

  • 代码简洁:Lambda 表达式和 Stream API 可以让代码更简洁、更易读。
  • 并行处理:Stream API 支持并行操作,可以自动利用多核处理器进行并行计算。
  • 易于测试:函数式编程倾向于使用小的、独立的函数,这使得测试更加简单。
  • 无副作用:函数式编程鼓励编写没有副作用(即不修改外部状态)的函数,这有助于保持代码的清晰和可预测性。

相关推荐
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei2 小时前
java的类加载机制的学习
java·学习
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉3 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer3 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端