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 表示,这意味着头和尾都为空。

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

鼎峰企业智能建站系统0.1.5(开源版)
鼎峰企业智能建站系统0.1.5(开源版)

鼎峰企业智能建站系统是一个非常灵活的企业建站工具(简称:dfeiew),网页设计师可以使用dfeiew来快速建立企业网站。dfeiew采用adodo作为数据库持久层,采用smarty模板引擎,美工灵活,而且smarty是编译型的,访问快速。鼎峰拥有php+mysql,asp+access/ms sql版本,并且都是开源、免费的!快速提供企业建站传统的cms体系结构过于复杂,不适合做企业站点,而鼎峰

下载

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

public record empty() implements seqlist {
}

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

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

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

public sealed interface seqlist permits empty, cons {
  /**
   * creates an empty list.
   *
   * @param  the type of the elements
   * @return an empty list
   */
  static  seqlist 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       the type of the elements
   * @return a new list with the elements added
   */
  @safevarargs
  static  seqlist of(t... elements) {
    seqlist 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 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 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 head, seqlist tail) implements seqlist {

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

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

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

public record empty() implements seqlist {

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

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

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

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

  @override
  public optional 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.
   * it does not use structural sharing as it requires advanced data structures to achieve
   * it.
   *
   * @param fn  the map function
   * @param  the type of the elements of the new list
   * @return a new list with the elements mapped
   * @throws stackoverflowerror for infinite lists
   */
  default  seqlist map(function 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.
   * it does not use structural sharing as it requires advanced data structures to achieve
   * it.
   *
   * @param fn  the flat map function
   * @param  the type of the elements of the new list
   * @return a new list with the elements flat mapped
   * @throws stackoverflowerror for infinite lists
   */
  default  seqlist flatmap(function> fn) {
    if (isempty()) {
      return empty();
    }
    seqlist mappedhead = fn.apply(head());
    seqlist 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    the type of the elements
   * @return a new list with the elements of the two lists concatenated
   * @throws stackoverflowerror for infinite lists
   */
  static  seqlist concat(seqlist list1, seqlist list2) {
    if (list1.isempty()) {
      return list2;
    }
    return new cons<>(list1.head(), concat(list1.tail(), list2));
  }

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

使用示例

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

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

缺点

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

结论

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

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

825

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

725

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

731

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

396

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

445

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

429

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16881

2023.08.03

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

74

2025.12.31

热门下载

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

精品课程

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

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