IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Java函数式编程基础总结 -> 正文阅读

[Java知识库]Java函数式编程基础总结

参考:<-------------------------------------廖雪峰学Java-------------------------------------------->

1. 函数式编程基本概念

  • 函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为Lambda计算
  • 计算机(Computer)和计算(Compute)的概念:计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令;计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远;对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言
  • 函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的
  • 函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数
  • Java不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带this参数的函数
  • Java平台从Java 8开始,支持函数式编程(引入lambda表达式)

2. Lambda基础

  • 函数式编程(Functional Programming)把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。历史上研究函数式编程的理论是Lambda演算,所以我们经常把支持函数式编程的编码风格称为Lambda表达式
  • 从Java 8开始,我们可以用Lambda表达式替换单方法接口
public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, (s1, s2) -> {
            return s1.compareTo(s2);
        }); // Arrays.sort(_, _)第二个参数需要接受一个Comparator对象,这里用lambda表达式代替
        System.out.println(String.join(", ", array));
    }
}
  • 什么是单方法接口,这里以Comparator<T>接口为例:
@FunctionalInterface
public interface Comparator<T> {
	......
}

首先需要说明的是 @FunctionalInterface注解:(文档)
An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification. Conceptually, a functional interface has exactly one abstract method. Since default methods have an implementation, they are not abstract. If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface’s abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere.
总结包括三个要点:

  1. 由FunctionalInterface包裹的接口只有一个 “ 抽象方法 ”(单方法接口)
  2. 定义在接口中的default方法不算做 “ 抽象方法 ”
  3. 重写java.lang.Objects中的方法而形成的抽象方法,不算做“抽象方法”,因总是为会有来自Object或者其他类中的实现
    Comparator
  • 观察Lambda表达式的写法,它只需要写出方法定义:
(s1, s2) -> {
    return s1.compareTo(s2);
}

参数是(s1, s2),参数类型可以省略,因为编译器可以自动推断出String类型。-> { ... }表示方法体,所有代码写在内部即可。Lambda表达式没有class定义,因此写法非常简洁

  • 如果只有一行return xxx的代码,完全可以用更简单的写法:
Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));

返回值的类型也是由编译器自动推断的,这里推断出的返回值是int,因此,只要返回int,编译器就不会报错

3. 方法引用

  • 解决上面的问题,除了Lambda表达式,我们还可以直接传入方法引用:
public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, Main::cmp);
        System.out.println(String.join(", ", array));
    }

    static int cmp(String s1, String s2) {
        return s1.compareTo(s2);
    }
}

所谓方法引用,是指如果某个方法签名和接口恰好一致,就可以直接传入方法引用(除了方法名不相同之外,方法参数以及方法返回值都相同)

  • 注意:在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系
  • 引用实例方法:
public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, String::compareTo);
        System.out.println(String.join(", ", array));
    }
}

这里String::compareTo的定义为:

public final class String {
    public int compareTo(String o) {
        ...
    }
}

明明只接受了一个String参数为什么却可以编译通过并且顺利执行呢?这是因为实例方法隐含了一个this参数

  • 除了引用静态方法以及实例方法,还可以对构造方法进行引用,例如可以在Stream的map处理过程中传入构造方法的引用(关于Stream的更详细信息可以参考下一小节)

4. 使用Stream

  • Java8不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中
  • Stream的特点总结
    1. 不同于java.io的InputStream和OutputStream,Stream代表的是任意Java对象的序列
    2. Stream可以存储有限个或者“无限个”元素,因为Stream并不是将所有元素都真实地保存在内存中,Stream可以通过实时计算来得到相应的结果
    3. Stream实现了惰性计算;可以将对Stream的操作分为两个部分:一、逻辑算子;二、执行算子;在添加逻辑算子的过程中,Stream并不会直接对表示的各个元素进行相应的运算,而是存储计算的逻辑,只有当执行算子需求当前元素的最终结果时才会依据存储的计算逻辑,实时地计算得到相应的结果并交还
    4. Stream API支持函数式编程和链式操作
    具体实现方式,下面介绍

4.1 创建Stream

  • 方式一:Stream.of()方法
public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("A", "B", "C", "D");
        // forEach()方法相当于内部循环调用,
        // 可传入符合Consumer接口的void accept(T t)的方法引用:
        stream.forEach(System.out::println);
    }
}

这种方式没有办法体现Stream的强大,适用于测试情景

  • 方式二:基于数组或者Collection:
public class Main {
    public static void main(String[] args) {
        Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
        Stream<String> stream2 = List.of("X", "Y", "Z").stream();
        stream1.forEach(System.out::println);
        stream2.forEach(System.out::println);
    }
}

前两种方法创造出来的元素值都是固定的

  • 方式三:基于Supplier,创建Stream还可以通过Stream.generate()方法,它需要传入一个Supplier对象:
Stream<String> s = Stream.generate(Supplier<String> sp);

基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列
示例:自然数序列

public class Main {
    public static void main(String[] args) {
        Stream<Integer> natual = Stream.generate(new NatualSupplier());
        // 注意:无限序列必须先变成有限序列再打印:
        natual.limit(20).forEach(System.out::println);
    }
}

class NatualSupplier implements Supplier<Integer> {
    int n = 0;
    public Integer get() {
        n++;
        return n;
    }
}

对于无限序列,如果直接调用forEach()或者count()这些最终求值操作,会进入死循环,因为永远无法计算完这个序列,所以正确的方法是先把无限序列变成有限序列,例如,用limit()方法可以截取前面若干个元素,这样就变成了一个有限序列

  • 其他方式:
    • 基于一些API接口:例如Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:
    try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
    	...
    }
    
    • 基本类型:因为Java的范型不支持基本类型,所以我们无法用Stream这样的类型,会发生编译错误。为了保存int,只能使用Stream,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStream、LongStream和DoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率:
    // 将int[]数组变为IntStream:
    IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
    // 将Stream<String>转换为LongStream:
    LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);
    

4.2 使用map方法

  • Stream.map()是Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream
  • map操作,把一个Stream的每个元素一一对应到应用了目标函数的结果上:
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);

map效果

  • map()方法接收的对象是Function接口对象,它定义了一个**apply()**方法,负责把一个T类型转换成R类型:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Function的定义为:

@FunctionalInterface
public interface Function<T, R> {
    // 将T类型转换为R:
    R apply(T t);
}

因此我们可以直接传入Lambda表达式来代替匿名类

  • 示例:利用map完成对字符串的操作
public class Main {
    public static void main(String[] args) {
        List.of("  Apple ", " pear ", " ORANGE", " BaNaNa ")
                .stream()
                .map(String::trim) // 去空格
                .map(String::toLowerCase) // 变小写
                .forEach(System.out::println); // 打印
    }
}

4.3 使用filter

  • filter() 操作是对一个Stream的所有元素一一进行测试,不满足条件的元素就会被“过滤”,剩下的满足条件的元素则组成一个新的Stream
  • 示例:过滤偶数
public class Main {
    public static void main(String[] args) {
        IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .filter(n -> n % 2 != 0)
                .forEach(System.out::println);
    }
}
  • filter()方法接收的对象是Predicate接口对象,它定义了一个**test()**方法,负责判断元素是否符合条件:
@FunctionalInterface
public interface Predicate<T> {
    // 判断元素t是否符合条件:
    boolean test(T t);
}

用Lambda表达式替代

  • filter()除了常用于数值外,也可应用于任何Java对象,过滤规则需要自己定义

4.4 使用reduce

  • Stream.reduce() Stream的一个聚合方法(执行方法),它可以把一个Stream的所有元素按照聚合函数聚合成一个结果
  • 示例:累加
public class Main {
    public static void main(String[] args) {
    	// 第一个参数表示起始值,acc表示当前的累加值,n表示当前位置的元素
        int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);  // 单行操作(且是返回语句)可以省略return
        System.out.println(sum); // 45
    }
}

可以解析为:

// 计算过程:
acc = 0 // 初始化为指定值
acc = acc + n = 0 + 1 = 1 // n = 1
acc = acc + n = 1 + 2 = 3 // n = 2
acc = acc + n = 3 + 3 = 6 // n = 3
acc = acc + n = 6 + 4 = 10 // n = 4
acc = acc + n = 10 + 5 = 15 // n = 5
acc = acc + n = 15 + 6 = 21 // n = 6
acc = acc + n = 21 + 7 = 28 // n = 7
acc = acc + n = 28 + 8 = 36 // n = 8
acc = acc + n = 36 + 9 = 45 // n = 9

实际上reduce()操作是一个求和操作

  • reduce()方法传入的对象是BinaryOperator接口,它定义了一个apply()方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果:
@FunctionalInterface
public interface BinaryOperator<T> {
    // Bi操作:两个输入,一个输出
    T apply(T t, T u);
}
  • 如果去掉初始值,我们会得到一个Optional<Integer>
Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent()) {
    System.out.println(opt.get());
}

Stream的元素有可能是0个,Optional对象用于进一步判断结果是否存在

  • 示例:灵活运用map方法以及reduce方法
public class Main {
    public static void main(String[] args) {
        // 按行读取配置文件:
        List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
        Map<String, String> map = props.stream()
                // 把k=v转换为Map[k]=v:
                .map(kv -> { // 这里的kv是一个字符串
                    String[] ss = kv.split("\\=", 2);
                    return Map.of(ss[0], ss[1]);  // 将一个字符串转换为Map<k, v>的形式
                })  
                // 一个字符串对应一个Map因此这里有多个Map,下一步实现把所有Map聚合到一个Map:
                .reduce(new HashMap<String, String>(), (m, kv) -> {
                // 初始值为一个空Map, m表示一个逐渐增加元素的总Map对象,kv表示每一个单元素Map
                    m.putAll(kv);
                    return m;
                });
        // 打印结果:
        map.forEach((k, v) -> {
            System.out.println(k + " = " + v);
        });
    }
}

4.5 输出集合

  • reduce()只是一种聚合操作,如果我们希望把Stream的元素保存到集合,例如List,因为List的元素是确定的Java对象,因此,把Stream变为List不是一个转换操作,而是一个聚合操作,它会强制Stream输出每个元素
  • 输出为List
public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Apple", "", null, "Pear", "  ", "Orange");
        List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
        System.out.println(list);
    }
}

调用collect()并传入Collectors.toList()对象,它实际上是一个Collector实例,通过类似reduce()的操作,把每个元素添加到一个收集器中(实际上是ArrayList)

  • 输出为数组
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
  • 输出为Map
public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
        Map<String, String> map = stream
                .collect(Collectors.toMap(
                        // 把元素s映射为key:
                        s -> s.substring(0, s.indexOf(':')),
                        // 把元素s映射为value:
                        s -> s.substring(s.indexOf(':') + 1)));
        System.out.println(map);
    }
}
  • 分组输出
public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
        Map<String, List<String>> groups = list.stream()
                .collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
        System.out.println(groups);
    }
}

Collectors.groupingBy()需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组;第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List

4.6 其他操作

  • 对Stream的元素进行排序十分简单,只需调用sorted()方法:
public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Orange", "apple", "Banana")
            .stream()
            .sorted()
            .collect(Collectors.toList());
        System.out.println(list);
    }
}

此方法要求Stream的每个元素必须实现Comparable接口。如果要自定义排序,在sourted方法中传入指定的Comparator即可

  • 对一个Stream的元素进行去重,没必要先转换为Set,可以直接用distinct():
List.of("A", "B", "A", "C", "B", "D")
    .stream()
    .distinct()
    .collect(Collectors.toList()); // [A, B, C, D]
  • 截取操作常用于把一个无限的Stream转换成有限的Stream,skip()用于跳过当前Stream的前N个元素,limit()用于截取当前Stream最多前N个元素:
List.of("A", "B", "C", "D", "E", "F")
    .stream()
    .skip(2) // 跳过A, B
    .limit(3) // 截取C, D, E
    .collect(Collectors.toList()); // [C, D, E]
  • 将两个Stream合并 为一个Stream可以使用Stream的静态方法concat():
Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]
  • 扁平化操作:如果Stream的元素是集合Stream<List<Integer>>,而我们希望把上述Stream转换为Stream<Integer>,就可以使用flatMap()
Stream<List<Integer>> s = Stream.of(
        Arrays.asList(1, 2, 3),
        Arrays.asList(4, 5, 6),
        Arrays.asList(7, 8, 9));
Stream<Integer> i = s.flatMap(list -> list.stream());

flatMap(),是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream
扁平化

  • 把一个普通Stream转换为可以并行处理的Stream只需要用parallel()进行转换:
Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
                   .sorted() // 可以进行并行排序
                   .toArray(String[]::new);

经过parallel()转换后的Stream只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升

  • 除了reduce()和collect()外,Stream还有一些常用的聚合方法:
1. count():用于返回元素个数;
2. max(Comparator<? super T> cp):找出最大元素;
3. min(Comparator<? super T> cp):找出最小元素。
  • 针对IntStream、LongStream和DoubleStream,还额外提供了以下聚合方法:
1. sum():对所有元素求和;
2. average():对所有元素求平均数。
  • 测试Stream的元素是否满足以下条件:
1. boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件;
2. boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。
  • forEach()方法可以循环处理Stream的每个元素,我们经常传入System.out::println来打印Stream的元素
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-11-12 19:27:07  更:2021-11-12 19:27:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 1:18:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码