0

0

持久且不可变的 Java LinkedList

王林

王林

发布时间:2024-07-24 11:58:40

|

727人浏览过

|

来源于dev.to

转载

持久且不可变的 java linkedlist

在本文中,我们将使用 java 实现 linkedlist 的持久且不可变变体
部分结构共享可提高时间和空间效率。

介绍

什么是链表

链表是一种由节点集合组成的数据结构,其中每个节点包含一个值和对序列中下一个节点的引用。向列表头部添加元素或从头部删除元素等操作都是 o(1) 操作。但是,向列表末尾添加元素或从末尾删除元素等操作是 o(n) 操作,其中 n 是列表中元素的数量。

为什么我们需要一个不可变的 linkedlist

在函数式编程中,不变性是一个关键概念。不变性意味着一旦创建了数据结构,它
无法修改。相反,通过修改创建一个新的数据结构,原始数据结构保持不变。

此属性为我们带来了多项好处:

  1. 线程安全:由于数据结构是不可变的,因此可以在多个线程之间共享,无需同步。
  2. 可预测性:由于数据结构是不可变的,我们可以推断数据结构在任何时间点的状态。
  3. 撤消:由于数据结构是不可变的,我们总是可以通过使用之前版本的数据结构来恢复到之前的状态。
  4. 调试:不变性使调试更容易,因为数据结构无法修改。

然而,java 中的集合以及由于本文的重点是 linkedlist,默认情况下是可变的。原因可能有很多
从设计集合时不被考虑到内在的性能原因到
不可变的数据结构。

不可修改的 jdk 集合和此 linkedlist 之间的区别

jdk 提供了不可修改的集合,它们是原始集合的包装器。它们支持不变性,但它们不是持久性的,也没有提供真正的类型安全方式。它们是原始系列的包装,而且它们
调用修改操作时抛出异常。这与不变性不同,不变性中数据结构根本无法修改,同时确保在运行时很难获得 unsupportedoperationexception。

持久与不可变

虽然持久性和不可变这两个术语经常互换使用,但它们具有不同的含义。不变性不允许修改数据结构,而持久性允许在修改数据结构时共享数据结构。这意味着当修改数据结构(即创建新版本)时,旧数据结构的部分内容可以与新数据结构共享,从而实现时间和空间效率的提高。这种技术称为结构共享.

有多种方法可以实现数据结构的持久化。数据结构范围从简单到复杂,例如使用平衡树(如 avl 或红黑树),到更复杂的树(如手指树和基于基数的平衡树)。

立即学习Java免费学习笔记(深入)”;

在本文中,我们将实现一个具有部分结构共享的持久且不可变的 linkedlist 的简单版本。原因是 linkedlist 是一种简单的数据结构,它将帮助我们更好地理解不变性和持久性的概念,而通常更复杂的数据结构的实现本质上是一项具有挑战性的任务。

执行

下面我们将一步步用java实现一个持久且不可变的单链表。

对于完整的实现以及额外的 monad 和实用程序来帮助您进行 java 函数式编程之旅,您可以查看这个很棒的小型库functionalutils。

我们为 linkedlist 指定的名称将是 seqlist,它将是一个泛型类。

首先,我们需要考虑我们要在列表中支持的操作。

  1. 添加到列表的头部,这将是一个 o(1) 的操作。
  2. 从列表中删除一个元素,如果该元素位于末尾,则最坏的情况是 o(n) 操作。
  3. 添加到列表中的任意位置。
  4. 过滤操作,用于过滤给定谓词的元素。
  5. map 和 flatmap 操作将我们的 list 转换为 monad,以便更轻松地进行函数组合。

我们可以将 linkedlist 视为由节点组成的结构,其中每个节点包含:

  1. head 持有一个值。
  2. tail 保存列表的其余部分,而列表又是一个由头和尾组成的 linkedlist,直到列表末尾。
  3. 列表的末尾由一个空的 linkedlist 表示,这意味着头和尾都为空。

完整的实现可以在这里找到

ModelGate
ModelGate

一站式AI模型管理与调用工具

下载

鉴于列表的最后一个元素是一个空的 linkedlist,并且每个元素都是一个有头和尾的节点,我们可以将我们的 linkedlist 表示为由两个类组成的递归数据结构:

public record empty<t>() implements seqlist<t> {
}

public record cons<t>(t head, seqlist<t> tail) implements seqlist<t> {
}

其中 cons 是一个名为 construct 的函数式编程术语,其历史可以追溯到 lisp 编程语言。

鉴于上述,我们可以实现 seqlist 接口如下:

public sealed interface seqlist<t> permits empty, cons {
  /**
   * creates an empty list.
   *
   * @param <t> the type of the elements
   * @return an empty list
   */
  static <t> seqlist<t> empty() {
    return new empty<>();
  }

  /**
   * creates a new list with the given elements. the complexity of this method is o(n) where n is
   * the number of elements.
   *
   * @param elements the elements to add
   * @param <t>      the type of the elements
   * @return a new list with the elements added
   */
  @safevarargs
  static <t> seqlist<t> of(t... elements) {
    seqlist<t> list = empty();
    for (int i = elements.length - 1; i >= 0; i--) {
      list = list.add(elements[i]);
    }
    return list;
  }

  /**
   * prepends the element to the list. the complexity of this method is o(1).
   *
   * @param element the element to add
   * @return a new list with the element prepended
   */
  default seqlist<t> add(t element) {
    return new cons<>(element, this);
  }
}

让我们分解一下上面写的内容:

  1. 我们创建了一个密封接口 seqlist,它将成为我们 linkedlist 的接口。
  2. empty() 方法创建一个空列表,它是 empty 类的实例。
  3. 方法 add() 将一个元素添加到列表中。此方法的复杂度为 o(1),因为我们只是使用给定元素和当前列表创建一个新节点。此方法使用结构共享,因为新列表共享当前列表的尾部。
  4. of() 方法使用给定元素创建一个新列表。此方法的复杂度为 o(n),其中 n 是元素的数量。很明显,我们从最后一个元素开始并将其添加到列表中。这是因为我们想保留元素的顺序。

我们需要实施剩余的操作。让我们从删除操作开始:

/**
   * removes the first occurrence of the element from the list. if the element is not found, the
   * list is returned as is. the complexity of this method is o(n) where n is the number of
   * elements. it uses structural sharing up to the element to remove. if the element is not found
   * the structural sharing is not utilized.
   *
   * @param element the element to remove
   * @return a new list with the element removed
   * @throws stackoverflowerror for infinite lists
   */
  default seqlist<t> remove(t element) {
    if (isempty()) {
      return this;
    }
    if (head().equals(element)) {
      return tail();
    }
    return new cons<>(head(), tail().remove(element));
  }

另外在我们的子类中实现 tail() 方法和其他一些有用的方法:

public record cons<t>(t head, seqlist<t> tail) implements seqlist<t> {

  @override
  public boolean isempty() {
    return false;
  }

  @override
  public optional<t> headoption() {
    return optional.ofnullable(head);
  }

  @override
  public optional<t> last() {
    if (tail.isempty()) {
      return optional.ofnullable(head);
    }
    return tail.last();
  }
}

public record empty<t>() implements seqlist<t> {

  @override
  public boolean isempty() {
    return true;
  }

  @override
  public t head() {
    throw new unsupportedoperationexception("head() called on empty list");
  }

  @override
  public optional<t> headoption() {
    return optional.empty();
  }

  @override
  public seqlist<t> tail() {
    throw new unsupportedoperationexception("tail() called on empty list");
  }

  @override
  public optional<t> last() {
    return optional.empty();
  }
}

我们可以从删除方法的实现中检查,我们正在使用递归调用来从中删除元素
列表。这是函数式编程中的典型模式,我们使用递归来遍历列表并
删除该元素。应注意避免在无限列表的情况下堆栈溢出。未来的改进可能是使用 java 不支持的尾递归优化,但可以使用蹦床来实现。

最后,让我们实现map和flatmap操作,将我们的list变成monad:

/**
   * applies a map function to the elements of the list. the complexity of this method is o(n) where
   * n is the number of elements.
   * <b>it does not use structural sharing</b> as it requires advanced data structures to achieve
   * it.
   *
   * @param fn  the map function
   * @param <u> the type of the elements of the new list
   * @return a new list with the elements mapped
   * @throws stackoverflowerror for infinite lists
   */
  default <u> seqlist<u> map(function<? super t, ? extends u> fn) {
    if (isempty()) {
      return empty();
    }
    return new cons<>(fn.apply(head()), tail().map(fn));
  }

  /**
   * applies a flat map function to the elements of the list. the complexity of this method is o(n)
   * where n is the number of elements.
   * <b>it does not use structural sharing</b> as it requires advanced data structures to achieve
   * it.
   *
   * @param fn  the flat map function
   * @param <u> the type of the elements of the new list
   * @return a new list with the elements flat mapped
   * @throws stackoverflowerror for infinite lists
   */
  default <u> seqlist<u> flatmap(function<? super t, ? extends seqlist<u>> fn) {
    if (isempty()) {
      return empty();
    }
    seqlist<u> mappedhead = fn.apply(head());
    seqlist<u> newtail = tail().flatmap(fn);
    return concat(mappedhead, newtail);
  }

  /**
   * concatenates two lists. the complexity of this method is o(n) where n is the number of
   * elements.
   *
   * @param list1 the first list
   * @param list2 the second list
   * @param <t>   the type of the elements
   * @return a new list with the elements of the two lists concatenated
   * @throws stackoverflowerror for infinite lists
   */
  static <t> seqlist<t> concat(seqlist<t> list1, seqlist<t> list2) {
    if (list1.isempty()) {
      return list2;
    }
    return new cons<>(list1.head(), concat(list1.tail(), list2));
  }

从 map 和 flatmap 方法的实现中可以看出,我们使用递归调用来遍历列表并将函数应用于每个元素。 flatmap 方法有点复杂,因为它需要函数返回一个新列表,我们需要将其与列表的其余部分连接起来。由于其众所周知的难度和使用高级数据结构的重要性,这两种方法都不使用结构共享。未来的改进将在以后的文章中进行探讨。

使用示例

让我们看看 seqlist 的一些使用示例。

  • 想象我们有一个整数列表,我们想要过滤掉偶数,然后将它们乘以 2 的幂,但具有不变性和持久性。
seqlist<integer> list = seqlist.of(1, 2, 3, 4, 5, 6);
seqlist<double> updatedlist = list
   .filterout(number -> number % 2 == 0)
   .map(number -> math.pow(number, 2));
  • 想象我们有一个字符串列表,我们想用前缀和后缀将它们连接起来。
seqlist<string> list = seqlist.of("a", "b", "c", "d", "e");
seqlist<string> updatedlist = list
   .map(letter -> "prefix" + letter + "suffix");
  • 想象我们有一个列表列表,我们想要将它们展平。
seqlist<seqlist<integer>> list = seqlist.of(seqlist.of(1, 2), seqlist.of(3, 4), seqlist.of(5, 6));
seqlist<integer> updatedlist = list
   .flatmap(seqlist -> seqlist);
  • 另一个例子是使用 jdk 21 开关表达式并利用编译​​器检查的模式匹配。
SeqList<Integer> list = SeqList.of(1, 2, 3, 4, 5, 6);
switch (list) {
  case Empty() -> {
    // do something
  }
  case Cons<Integer> cons -> {
    //do something else
  }
}

缺点

  1. 性能:如果列表主要用于从列表头部获取元素的前置元素,那么性能很好。在所有其他情况下,此实现至少需要 o(n)。
  2. 复杂性:持久且不可变的 linkedlist 的实现比其可变的对应物更复杂。
  3. 内存:由于为每个操作创建新列表,持久且不可变的 linkedlist 的实现比其可变的对应项需要更多的内存。通过结构共享,这种情况会得到缓解,但不会消除。

结论

在本文中,我们用 java 实现了一个具有部分结构共享的持久且不可变的 linkedlist。我们演示了不变性和持久性的好处以及如何在 java 中实现它们。我们还展示了如何将 linkedlist 转换为 monad 以便更轻松地进行函数组合。我们讨论了持久性和不可变数据结构的优点和缺点以及如何在实践中使用它们。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

761

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

221

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1568

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

651

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

1228

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

1205

2024.04.29

go语言字符串相关教程
go语言字符串相关教程

本专题整合了go语言字符串相关教程,阅读专题下面的文章了解更多详细内容。

193

2025.07.29

c++字符串相关教程
c++字符串相关教程

本专题整合了c++字符串相关教程,阅读专题下面的文章了解更多详细内容。

131

2025.08.07

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号