首页 > 编程语言 >vavr Java的函数式编程神器-Part1

vavr Java的函数式编程神器-Part1

时间:2024-10-09 21:50:21浏览次数:1  
标签:Queue Vavr Java 函数 vavr 链表 Part1 数据结构

微信公众号:阿俊的学习记录空间
小红书:ArnoZhang
wordpress:arnozhang1994
博客园:arnozhang
CSDN:ArnoZhang1994

1. 介绍

Vavr(前称 Javaslang)是一个为 Java 8+ 提供的函数式库,提供持久数据类型和函数控制结构。

1.1. Vavr 中的 Java 8 函数式数据结构

Java 8 的 lambda(λ)使我们能够创建出色的 API,大大增强了语言的表现力。Vavr 利用 lambda 创建了许多基于函数式模式的新特性,其中之一是一个旨在替代 Java 标准集合的函数集合库。

Vavr 集合

LOGO

(这只是鸟瞰图,下面会有可读版本。)

1.2. 函数式编程

在深入数据结构的细节之前,我想先讨论一些基础知识。这将明确我为何创建 Vavr,尤其是新的 Java 集合。

1.2.1. 副作用

Java 应用程序通常充满副作用。它们会改变某种状态,可能是外部世界。常见的副作用包括在原地更改对象或变量、打印到控制台、写入日志文件或数据库。如果副作用以不希望的方式影响程序的语义,则被视为有害。

例如,如果一个函数抛出异常并对此进行解释,那么这被视为影响程序的副作用。此外,异常类似于非局部的 goto 语句,它们打破正常的控制流。然而,现实世界的应用确实会执行副作用。

int divide(int dividend, int divisor) {
    // 如果除数为零则抛出异常
    return dividend / divisor;
}

在函数式环境中,我们处于一个有利的局面,可以在 Try 中封装副作用:

// = Success(result) 或 Failure(exception)
Try<Integer> divide(Integer dividend, Integer divisor) {
    return Try.of(() -> dividend / divisor);
}

这个版本的 divide 不再抛出任何异常。我们通过使用类型 Try 使可能的失败变得明确。

1.2.2. 引用透明性

如果可以用其值替换调用而不影响程序的行为,则称一个函数或更一般的表达式为引用透明。简单来说,给定相同的输入,输出总是相同的。

// 不是引用透明的
Math.random();

// 引用透明的
Math.max(1, 2);

如果所有相关表达式都是引用透明的,则该函数被称为纯函数。由纯函数组成的应用程序只要编译就很可能能正常工作。我们能够对此进行推理。单元测试易于编写,调试变成了过去的遗物。

1.2.3. 价值思维

Clojure 的创造者 Rich Hickey 进行了关于“价值的价值”的精彩演讲。最有趣的值是不可变值。主要原因是不可变值

  • 本质上是线程安全的,因此不需要同步
  • 在 equals 和 hashCode 方面是稳定的,因此是可靠的哈希键
  • 不需要克隆
  • 在未经检查的协变转换中表现出类型安全(Java 特有)

更好的 Java 的关键是使用不可变值配合引用透明的函数。Vavr 提供了必要的控制和集合,以实现这一目标,在日常 Java 编程中。

1.3. 数据结构概述

Vavr 的集合库由一组丰富的基于 lambda 的函数式数据结构组成。它们与 Java 原始集合唯一共享的接口是 Iterable。主要原因是 Java 集合接口的修改方法不返回底层集合类型的对象。

我们将通过查看不同类型的数据结构来了解这一点为何至关重要。

1.3.1. 可变数据结构

Java 是一种面向对象的编程语言。我们将状态封装在对象中,以实现数据隐藏,并提供修改方法来控制状态。Java 集合框架(JCF)建立在这个思想之上。

interface Collection<E> {
    // 从该集合中删除所有元素
    void clear();
}

今天,我把 void 返回类型视为一种气味。这是副作用发生的证据,状态被改变。共享可变状态是失败的重要来源,不仅在并发环境中。

1.3.2. 不可变数据结构

不可变数据结构在创建后不能被修改。在 Java 的上下文中,它们通常以集合包装器的形式使用。

List<String> list = Collections.unmodifiableList(otherList);

// 爆炸!

list.add("why not?");

有各种库为我们提供类似的实用方法。结果总是特定集合的不可修改视图。通常在调用修改方法时会在运行时抛出异常。

1.3.3. 持久数据结构

持久数据结构在被修改时保留其之前的版本,因此实际上是不可变的。完全持久的数据结构允许对任何版本进行更新和查询。

许多操作只执行小的变化。仅仅复制之前的版本效率不高。为了节省时间和内存,识别两个版本之间的相似性并尽可能共享数据至关重要。

此模型不强加任何实现细节。函数式数据结构在此发挥作用。

1.4. 函数式数据结构

也称为纯函数数据结构,这些是不可变且持久的。函数式数据结构的方法是引用透明的。

Vavr 具有广泛使用的函数式数据结构。以下示例将深入解释。

1.4.1. 链表

最流行且最简单的函数式数据结构之一是(单向)链表。它有一个头元素和一个尾链表。链表的行为类似于遵循后进先出(LIFO)方法的堆栈。

在 Vavr 中,我们这样实例化一个链表:

// = List(1, 2, 3)
List<Integer> list1 = List.of(1, 2, 3);

每个链表元素形成一个独立的链表节点。最后一个元素的尾部是 Nil,表示空链表。

LOGO

这使我们能够在不同版本的链表中共享元素。

// = List(0, 2, 3)
List<Integer> list2 = list1.tail().prepend(0);

新头元素 0 链接到原链表的尾部。原链表保持不变。

LOGO

这些操作在常量时间内进行,换句话说,它们与链表大小无关。其他大多数操作需要线性时间。在 Vavr 中,这通过接口 LinearSeq 表达,我们可能已经在 Scala 中了解过。

如果我们需要在常量时间内可查询的数据结构,Vavr 提供 Array 和 Vector。两者均具有随机访问能力。

Array 类型由一个 Java 对象数组支持。插入和删除操作需要线性时间。Vector 在 Array 和 List 之间表现良好,既适用于随机访问,也适用于修改。

实际上,链表也可以用于实现队列数据结构。

1.4.2. 队列

基于两个链表可以实现非常高效的函数式队列。前面的链表保存被出队的元素,后面的链表保存入队的元素。两个操作入队和出队的时间复杂度均为 O(1)。

Queue<Integer> queue = Queue.of(1, 2, 3)
                            .enqueue(4)
                            .enqueue(5);

初始队列由三个元素创建。在后链表上入队两个元素。

LOGO

如果在出队时前链表没有元素,后链表会被反转并成为新的前链表。

LOGO

出队一个元素时,我们得到一个第一个元素和剩余队列的配对。必须返回队列的新版本,因为函数式数据结构是不可变且持久的。原队列不受影响。

Queue<Integer> queue = Queue.of(1, 2, 3);

// = (1, Queue(2, 3))
Tuple2<Integer, Queue<Integer>> dequeued =
        queue.dequeue();

当队列为空时,会抛出 NoSuchElementException。以函数式方式处理时,我们更希望得到一个可选结果。

// = Some((1, Queue()))
Queue.of(1).dequeueOption();

// = None
Queue.empty().dequeueOption();

可选结果可以进一步处理,无论其是否为空。

// = Queue(1)
Queue<Integer> queue = Queue.of(1);

// = Some((1, Queue()))
Option<Tuple2<Integer, Queue<Integer>>> dequeued =
        queue.dequeueOption();

// = Some(1)
Option<Integer> element = dequeued.map(Tuple2::_1);

// = Some(Queue())
Option<Queue<Integer>> remaining =
        dequeued.map(Tuple2::_2);

1.4.3. 排序集合

排序集合是比队列更常用的数据结构。我们使用二叉搜索树以函数式方式对其建模。这些树由每个节点最多有两个子节点和值构成。

我们在有顺序的情况下构建二叉搜索树,由元素比较器表示。任何给定节点的左子树的所有值严格小于

该节点的值,右子树的所有值严格大于该节点。

// = TreeSet(1, 2, 3, 4, 6, 7, 8)
SortedSet<Integer> xs = TreeSet.of(6, 1, 3, 2, 4, 7, 8);

LOGO

对这些树的搜索运行时间为 O(log n)。我们从根节点开始搜索,判断是否找到该元素。由于值的完全排序,我们知道接下来在哪一侧的分支上搜索。

// = TreeSet(1, 2, 3);
SortedSet<Integer> set = TreeSet.of(2, 3, 1, 2);

// = TreeSet(3, 2, 1);
Comparator<Integer> c = (a, b) -> b - a;
SortedSet<Integer> reversed = TreeSet.of(c, 2, 3, 1, 2);

大多数树操作本质上是递归的。插入函数的行为类似于搜索函数。当达到搜索路径的末端时,创建一个新节点,并重新构建整个路径直至根节点。尽可能引用现有子节点。因此,插入操作的时间和空间复杂度均为 O(log n)。

// = TreeSet(1, 2, 3, 4, 5, 6, 7, 8)
SortedSet<Integer> ys = xs.add(5);

LOGO

为了保持二叉搜索树的性能特征,树必须保持平衡。从根到叶子的所有路径必须具有大致相同的长度。

在 Vavr 中,我们基于红黑树实现了二叉搜索树。它使用特定的着色策略来保持插入和删除时树的平衡。有关此主题的更多信息,请参阅 Chris Okasaki 的书《Purely Functional Data Structures》。

### 1.4.4. HashMap

HashMap 是一种常用的不可变数据结构,用于存储键值对。Vavr 的 HashMap 是基于持久性和不可变性构建的。它允许对键值对进行高效的插入、删除和查找操作。

在 Vavr 中,我们可以这样创建一个 HashMap:

```java
// = HashMap(1 -> "one", 2 -> "two")
HashMap<Integer, String> map = HashMap.of(1, "one", 2, "two");

每次插入或删除操作都会返回一个新的 HashMap 实例,原始 HashMap 不会被修改。

1.4.5. 其他函数式数据结构

除了上面提到的链表、队列和 HashMap,Vavr 还提供了其他多种常用的函数式数据结构,包括:

  • :如 TreeSet 和 TreeMap,它们提供了基于排序的集合和映射。
  • 集合:如 HashSet,它是一种基于哈希表的不可变集合。
  • 数组:如 Array 和 Vector,提供高效的随机访问能力。

这些数据结构使得在 Java 中使用函数式编程模式变得更加简单和高效。

1.5. 总结

Vavr 是一个强大的库,通过提供持久和不可变的数据结构,使得函数式编程在 Java 中变得可行。通过使用 Vavr,开发者可以减少副作用,提高代码的可读性和可维护性,从而在日常开发中实现更高的效率和可靠性。

LOGO

Vavr 使我们能够更深入地探索函数式编程的世界,为 Java 开发者提供了更多的工具和思路。

标签:Queue,Vavr,Java,函数,vavr,链表,Part1,数据结构
From: https://www.cnblogs.com/arnozhang/p/18455238

相关文章

  • JAVA——常见算法
    查找算法基本查找从0索引开始查找是否找到packagecom.itheima.search;importjava.security.KeyStore;publicclassBasicSearchDemo1{publicstaticvoidmain(String[]args){int[]arr={23,34,54,24,43,46};intnumber=43;......
  • 新一代 Java 代码审计工具—铲子 SAST
     .产品定位铲子SAST是一款简单易用的JAVASAST(静态应用程序安全测试)工具,旨在为安全工程师提供一款简单、好用、价格厚道的代码安全扫描产品。一分钟即可完成安装|一个按钮创建扫描任务|一键查看漏洞数据流支持主流的java开发框架| 采用轻量、快捷的污点分析|支......
  • Java日总结24-10-9:约束&&数据库设计
    约束的概念及分类:主键的自动增长:在PRIMARYKEY之后添加auto_increment外键约束:2.数据库设计:表的关系(3种):1、一对一;2、一对多(多对一);3、多对多。表的关系之一对多:例:员工和部门之间,一个部门可对应多个员工实现方式:在多的一方建立外键,指向一的一方的主键。表的关系之......
  • java面向对之象类的继承与多态
    目录1.类的继承图解案例:创建一个动物类和一个猫类1.代码1)动物类2)猫类3.测试类2.效果2.父类方法的重写案例:如何重写父类的方法        1.代码1)Animal类2)Dog类3)测试类2.效果3.super关键字案例:如何在子类中调用父类的方法,或属性1.代码1)Animal类......
  • 庖丁解java(一篇文章学java)
    (大家不用收藏这篇文章,因为这篇文章会经常更新,也就是删除后重发) 一篇文章学java,这是我滴一个执念...当然,真一篇文章就写完java基础,java架构,java业务实现,java业务扩展,根本不可能.所以,这篇文章,就是一个索引,索什么呢?  请看下文...关于决定开始写博文的介绍......
  • Java对象内存图
    Java的对象内存图一、Java内存分配介绍Java虚拟机(JVM)在执行Java程序时会使用多个内存区域栈:方法运行时所进入的内存,变量也是在这里堆:new出来的东西会在这块内存中开辟空间并产生地址方法区:字节码文件加载时进入的内存(class类、main方法等)本地方法栈寄存器1.堆区(Heap......
  • 0基础学Java之Day02(上午完整版)
    一、扩展Hello.java1.详解代码/**作者:zzj时间:2024年10月9日*/​//公有的类叫做HellopublicclassHello{​//公有的静态的无返回值的方法叫做main--这种固定写法的方法叫做主方法publicstaticvoidmain(String[]args){​//输出语句(......
  • 0基础学Java之Day02(下午完整版)
    五、Java的体系划分JavaSE--J2SE--核心版本JavaEE--J2EE--企业版本(做服务器)JavaME--J2ME--微型版本(做移动端--已弃用)学习路线:后端程序员:JavaSE、JavaEE移动端程序员:JavaSE、JavaME-------(已弃用)Android移动端程序员:JavaSE、AndroidSDKIOS移动端程序......
  • 【Java】反射
    Java中的反射机制动态代理反射允许对封装类的字段,方法和构造函数的信息进行编程访问==》反射允许对成员变量,成员方法和构造方法的信息进行编程访问基本操作:获取(获取class对象【字节码对象】)+解剖成员变量Field——修饰符、名字、类型、赋值构造方法Constructor......
  • java计算机毕业设计保险业务管理系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着经济的飞速发展和人民生活水平的日益提高,保险作为一种风险转移和保障机制,在现代社会中扮演着愈发重要的角色。传统的保险业务处理方式往往依赖于......