【问题标题】:Cons'ing a List in Java在 Java 中构造一个列表
【发布时间】:2009-05-05 18:21:41
【问题描述】:

假设我有一个java.util.List list,我想通过在list 的开头添加一个元素e 来创建一个新的List(即,我想缺点 @987654325 @ 和 list)。例如,如果list

[1,2,3,4]

e5,那么cons(e,list) 将是

[5,1,2,3,4]

listcons(e,list)的元素可以共享,但list不能修改。

实现cons 的最简单和/或最有效的方法是什么?结果不可修改是可以的。允许使用 Google Collections Library。

如果listcom.google.common.collect.ImmutableList 怎么办?

【问题讨论】:

  • 对于我们这些不熟悉 Lisp 的人来说,什么是缺点?
  • 我定义了行为并举了一个例子。你还想要什么?

标签: java collections list guava


【解决方案1】:
public static<T> List<T> cons(List<T> list, T t) {
    ArrayList<T> result = new ArrayList<T>(list);
    result.add(0, t);
    return result;
}

针对 cme​​ts 编辑: 由于问题要求“实现缺点的最简单和/或最有效的方法”,因此我选择了“最简单”。得知有更有效的方法,我不会感到惊讶。将元素放在列表之前是另一种有效的方法,最初分配正确的大小可能会提高性能。过早的优化是万恶之源。

【讨论】:

  • 有谁知道这在实践中的表现如何?根据 Java API:'在此列表中的指定位置插入指定元素。将当前位于该位置的元素(如果有)和任何后续元素向右移动(将其索引加一)。 ' 这是否意味着'list' 的元素只迭代一次?
  • 糟糕。忘了提到我引用了'add(int index,Object o)'方法的文档
  • 创建具有所需容量的 ArrayList、添加第一个元素然后添加所有列表可能更有效。但我认为最初的问题希望将元素添加到列表的末尾,这并不重要。
  • (不,最初的问题只是有列表和元素的合理方式。)
  • 这将遍历列表中的元素两次;一次将所有内容从列表复制到结果,然后再次移动添加中的元素。使用 LinkedList 和 addFirst 消除了第二次数组遍历。
【解决方案2】:

Clojure 提供了那种 Lisp-y 的东西。虽然大多数人都认为将 Clojure 用于该语言(就像我一样),但 Clojure 库都是真正的 Java 代码,如果您愿意,您可以将 Java 中的数据结构用作一个特殊的库。这样,您将获得执行 cons 等操作的能力,并且获得 Clojure 使用的不变性。 Clojure 数据结构也实现了等效的 Java 类型。

只是一个不同方向的想法。

【讨论】:

  • 这个想法一定会到来。事实上,STM 可能是 Clojure 对 JVM 生态圈的更持久贡献。
【解决方案3】:

我相信您真正想要的答案是:

http://functionaljava.googlecode.com/svn/artifacts/2.20/javadoc/fj/data/List.html

该方法甚至被称为cons

我对这个库没有经验。前几天刚听说。希望一切顺利!

【讨论】:

  • Functional Java 看起来很酷,但我已经非常依赖 Google Collections 并且没有在市场上寻找新的库依赖项 :-(
  • Awwww...然后放弃谷歌的废话。
【解决方案4】:

一个有效的解决方案是创建自己的 List 实现,懒惰地委托。如果不可修改,那也不会太麻烦。如果通过工厂方法创建,那么您甚至可以使用两种不同的底层实现之一,具体取决于参数 List 是否实现 RandomAccess。

对于最简单的情况(没有检查是否编译,没有完整性检查等):

class ConsList<E> extends AbstractList<E>
{
    private final E first;
    private final List<E> rest;

    ConsList( E first, List<E> rest )
    {
        this.first = first;
        this.rest = rest;
    }

    public int get( int index )
    {
        return (index == 0) ? first : rest.get( index - 1 );
    }

    public int size()
    {
        return rest.size() + 1;
    }
}

我确信其他一些方法可以提高效率,而通过扩展 AbstractSequentialList 可以更好地满足顺序情况。

【讨论】:

  • 当你做第二个缺点时首先会发生什么。
  • 我看不出有什么问题。每个 cons 返回一个新列表。 cons(a, cons(b, c)) 返回一个列表,其“first”是 a,“rest”是一个列表,其“first”是 b,“rest”是 c。 完全使用 cons 和 EMPTY_LIST 构造的列表本质上只是一个链表,但这个版本可以随时切换到任何其他实现,如果您不知道 rest 的实现,这很有用。
【解决方案5】:

因为您提到了cons 函数,所以我假设您正在使用由cons 单元格组成的链表概念模型来解决这个问题。具体来说,我假设您认为每个列表都有一个 car(第一个元素)和一个 cdr(包含所有后续元素的子列表)。

Java 支持链接列表为java.util.LinkedList。这些对于线性遍历很有用,并且可以非常有效地插入元素。这些与我上面提到的链表最相似。

Java 还提供java.util.ArrayList。这种列表适合随机访问,但在插入元素时可能会很慢。事实上,在列表开头插入元素时,它们的速度最慢。因为ArrayLists 在幕后作为数组实现,所以每个元素都必须在列表中向前复制一个位置,以便为新的第一个元素腾出空间。现在,如果您还需要一个更大的数组,ArrayList 将分配一个新数组,复制所有元素,等等。

(Google 的 ImmutableList 被称为“随机访问”,因此它可能更类似于后者。)

如果您打算经常使用cons 方法,我建议将它与链表一起使用。 cons 操作本质上是在列表的开头添加一个元素。这对于诸如数组之类的线性结构几乎没有意义。出于这个原因,我建议不要使用数组列表:它们在概念上不适合这项工作。

对于非常挑剔的人:因为每次调用 cons 时都会返回一个新列表,所以无论该列表是 LinkedList 还是 ArrayList,都必须进行复制。但是,cons 操作的全部原理是它在链表上进行操作。

public <E> LinkedList<E> cons(E car, List<E> cdr) {
    LinkedList<E> destination = new LinkedList<E>(cdr);
    destination.addFirst(car);
    return destination;
}

请注意,以上代码是在阅读以上答案后编写的,因此对于任何意外抄袭,我深表歉意。如果您看到请告诉我,我会正确确认。

假设您对返回 LinkedList 感到满意,您可以在此示例中使用 ImmutableList 作为 cdr

【讨论】:

  • 使用destination.addFirst(car) 而不是destination.add(0, car)。它不会对性能产生巨大影响(虽然可能有一些),但我认为它更清晰。
  • 这看起来好多了 - 谢谢!我在写那个 sn-p 的时候一直在看 java.util.List 接口,所以我错过了。
【解决方案6】:

这不是众所周知的,但是 Sun javac 编译器 API 包含一个不可变列表实现,它使用 append()prepend() 等方法返回新的不可变列表。要使用它,您需要在 classpath 上使用tools.jar,并且这个 import 语句:

import com.sun.tools.javac.util.List;

示例代码:

final List<Integer> list = List.of(6, 7);
System.out.println(list);

final List<Integer> newList =
    list.prepend(5)                       // this is a new List
        .prependList(List.of(1, 2, 3, 4)) // and another new List
        .append(8)                        // and another
        .reverse();                       // and yet another
System.out.println(newList);

System.out.println(list == newList);

System.out.println(list.getClass());
System.out.println(newList.getClass());

输出:

6,7
8,7,6,5,4,3,2,1

com.sun.tools.javac.util.List 类
com.sun.tools.javac.util.List 类

我知道这是一个内部 API,不应该在生产中使用,但它非常有趣(最后是一个感觉不错的 java 集合)。

注意:来自CollectionList 接口的所有标准可变方法(如add()addAll())显然都会抛出UnsupportedOperationException

参考:

【讨论】:

    【解决方案7】:

    你能用CompositeCollection吗?

    public Collection cons(Collection c1, Collection c2)
    {
        CompositeCollection cons = new CompositeCollection();
        cons.addComposited(c1);
        cons.addComposited(c2);
        return cons;
    }
    

    这不会受到参数之一是否不可变以及是否仍由原始集合 c1 和 c2 支持的影响。

    如果您需要List,我可能会执行以下操作:

    public List cons(Collection c1, Collection c2)
    {
        ArrayList cons = new ArrayList(c1.size() + c2.size());
        cons.addAll(c1);
        cons.addAll(c2);
        return cons;
    }
    

    【讨论】:

    • 生成的集合是列表吗?
    【解决方案8】:

    我将投入我的 2 美分,然后看看是否有人想出更优雅的方法。一般情况下:

    <E> List<E> cons(E e, List<E> list) {
        List<E> res = Lists.newArrayListWithCapacity(list.size() + 1);
        res.add(e);
        res.addAll(list);
        return res;
    }
    

    ImmutableList(不知道效率如何):

    <E> ImmutableList<E> cons(E e, ImmutableList<E> list) {
        return ImmutableList.<E>builder()
                            .add(e)
                            .addAll(list)
                            .build();
    }
    

    【讨论】:

    • Lists.newArrayListWithExpectedSize 真的比new ArrayList&lt;E&gt; 更容易输入吗?
    • 我猜,这是为了避免可能的调整大小操作。但你可以使用java.sun.com/javase/6/docs/api/java/util/…。然而,上面的代码正在处理“预期大小”。不知道用“已知”容量实例化有什么区别。
    • @mmyers:在这种情况下,不,但我保持在列表中使用静态方法尽可能实例化我的列表的惯例。这通常是一场胜利。
    • @mmyers:Lists.new* 让您在声明/初始化变量时避免两次编写泛型参数。
    • 怎么样:List&lt;SomeGenericFancyLongTypeName&lt;WithTypeParameter&gt;&gt; list = new ArrayList&lt;SomeGenericFancyLongTypeName&lt;WithTypeParameter&gt;&gt;(100);List&lt;SomeGenericFancyLongTypeName&lt;WithTypeParameter&gt;&gt; list = newArrayListWithExpectedSize(100); 相比?
    【解决方案9】:

    LinkedList 肯定是在列表头部插入项目的最有效方式吗?

    只要使用Java自带的LinkedList

    【讨论】:

    • 您假设将原始列表复制到 LinkedList 的成本可以忽略不计。
    • 您没有指定 List 实现。我曾在某些情况下使用 LinkedList,其中最初的创建成本不是问题,但后续插入 的问题。
    【解决方案10】:

    如果您只有 Iterable,则另一种变体。

    public static <E> List<E> cons(E e, Iterable<E> iter) {
       List<E> list = new ArrayList<E>();
       list.add(e);
       for(E e2: iter) list.add(e2);
       return list;
    }
    
    public static <E> List<E> cons(Iterable<E>... iters) {
       List<E> list = new ArrayList<E>();
       for(Iterable<E> iter: iters) for(E e1: iter1) list.add(e1);
       return list;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-05-18
      • 1970-01-01
      • 2010-09-22
      • 2016-07-03
      • 2018-10-13
      相关资源
      最近更新 更多