【问题标题】:How do I perform an action on each element of a List and return the result (without affecting the original of course)?如何对 List 的每个元素执行操作并返回结果(当然不影响原始结果)?
【发布时间】:2011-09-30 01:38:05
【问题描述】:

如何在 Java 中编写一个静态方法,它将接受一个 List,对每个元素执行一个操作,并返回结果(当然不影响原来的)?

例如,如果我想为每个元素添加 2,那么这里的 ... 是什么?具体的返回类型必须相同,例如如果我的列表是一个值为 1、2、3 的 LinkedList,我应该取回一个值为 3、4、5 的 LinkedList。 ArrayList、Vector、Stack等也是类似的,都是List。

我可以看到如何使用多个 if (lst instanceof LinkedList) ... 等来做到这一点...还有更好的方法吗?

import java.util.List;

public class ListAdd {       

    static List<Integer> add2 (List<Integer> lst) {

    ...

        return result;
    }
}

【问题讨论】:

  • @Brandon:不过,他将如何复制列表?
  • @Luigi:使用反射是一个可接受的解决方案吗?
  • @thkala:他应该可以使用 Collections.copy(List,List) 方法。
  • @Brandon: Collections.copy(List,List) 要求您提供目标列表 - 您需要的类的实际实例。
  • @thkala:明白。而且......我明白你的意思。感谢您指出这一点。

标签: java list generics


【解决方案1】:

从根本上说,List 接口并不能保证您可以复制它。

您可能会在各种技术上获得一些运气:

  • 在传入的List 上使用clone(),虽然它可能会抛出,或者(因为它在Object 中受到保护)根本无法访问
  • 使用反射在传入的List 上查找公共无参数构造函数
  • 尝试对其进行序列化和反序列化以执行“深度克隆”
  • 创建某种工厂,并了解如何复制您的代码可能遇到的每种不同类型的列表(如果它是由unmodifiableList() 创建的包装器,或者由RandomAccessFile 支持的一些奇怪的自定义实现呢?)
  • 如果一切都失败了,要么抛出,要么返回 ArrayListVector,因为没有更好的选择

【讨论】:

  • 问题是clone()List(或其任何超类型)中不公开。所以我们仍然必须转换为某种支持它的类型,或者使用反射。
  • 我尝试过克隆,但似乎不可能。如果您将静态方法设为通用,则采用&lt;T extends List, AbstractList&gt; 类型时,您会遇到AbstractList 和任何超类型都没有实现clone() 方法的问题... AbstractList 不可克隆,而 Object 则不能。
  • ... 尽管集合 API 中 List 的所有具体类实际上都是可克隆的 :(
  • @Luigi Plinge:所有公开的
  • @Luigi Plinge:实现 Cloneable 并不一定意味着它支持clone()
【解决方案2】:

使用接口的全部意义,在本例中为List,是为了抽象出实现隐藏在接口后面的事实。

不过,您的意图对我来说很清楚:Clonable 接口支持创建具有相同状态的新实例。您的List 上可能未定义此接口。

通常重新考虑这种情况是个好主意:为什么需要在这个地方克隆 List 这个类?您的列表创建者不应该负责克隆列表吗?或者知道类型的调用者不应该确保他传递了他的列表的克隆?

可能,如果您查找定义的语义,您可以实现所有受支持的Lists:

static Vector<Integer> addTwo(Vector<Integer> vector) {
    Vector<Integer> copy = null; // TODO: copy the vector
    return addTwo_mutable(copy);
}
static ArrayList<Integer> addTwo(ArrayList<Integer> aList) {
    ArrayList<Integer> copy = null; // TODO: copy the array list
    return addTwo_mutable(copy);
}
static LinkedList<Integer> addTwo(LinkedList<Integer> lList) {
    LinkedList<Integer> copy = null; // TODO: copy the linked list
    return addTwo_mutable(copy);
}

private <T extends List<Integer>> static T addTwo_mutable(T list) {
    return list; // TODO: implement
}

即使您不支持数据类型,您也会收到一个很好的编译器错误,即指定的方法不存在。

(代码未测试)

【讨论】:

  • Cloneable 实际上并没有使clone() 方法可访问——它只是意味着protected 方法clone() 执行浅拷贝而不是抛出。
  • “按照惯例,实现此接口的类应使用公共方法覆盖 Object.clone(受保护)。”我记得它是“你应该覆盖Object.clone”,但你是绝对正确的,杰弗里!
  • 这当然是一种方法。我一直想知道为什么 Arrays.sort() 会改变你当前的列表而不是返回一个新的排序列表——也许这就是原因。
  • 那里的三个类都实现了一个公共的克隆方法,但是声明它返回Object而不是它自己的类型。 :-( 在每个上使用复制构造函数可能更容易(但这会忽略最终的子类)。
  • 使用不可变数据结构通常不是 Java 中的 标准。在函数式编程语言中,它们更为常见(参见 Clojure)。
【解决方案3】:

您可以使用反射在 lst.getClass() 的结果上查找公共零参数构造函数,然后调用它来获取您将放置结果的列表。 Java Collections Framework 建议 Collection 的任何派生类都提供零参数构造函数。这样,您的结果与参数属于同一运行时类。

【讨论】:

  • 任何通用集合,而不是每个集合。 (比如地图集合视图的类不会有这样的构造函数,实现List.subList的内部类也不会。)
  • 听起来不错。不过,我对反射并不是很熟悉。如果有人可以展示这是如何完成的,那就太好了。
【解决方案4】:

这是一个既不复制也不修改原始列表的变体。相反,它用另一个对象包装了原始列表。

public List<Integer> add2(final List<Integer> lst) {
    return new AbstractList<Integer>() {
        public int size() {
            return lst.size();
        }
        public Integer get(int index) {
            return 2 + lst.get(index);
        }
    };
}

返回的列表不可修改,但会随着原始列表的变化而变化。 (这实现了基于索引访问的迭代器,因此链表会很慢。然后更好地基于AbstractSequentialList实现。)

当然,结果列表显然与原始列表不在同一类

仅当您真的只需要原始列表的只读两个添加视图时才使用此解决方案,而不是如果您想要具有类似属性的修改副本。

【讨论】:

  • 好主意!但是,为了安全起见,我会在其中添加 边界检查
  • @Pindatjuh:边界检查由lst.get(index)完成,这里不需要复制。
  • 查看列表的有趣方式!谢谢。虽然不完全是我所追求的。
【解决方案5】:

好吧,有人提到了反思。这似乎是一个优雅的解决方案:

import java.util.*;
public class ListAdd {

    static List<Integer> add2 (List<Integer> lst) throws Exception {

        List<Integer> result = lst.getClass().newInstance();
        for (Integer i : lst) result.add(i + 2);

        return result;
    }
}

简洁,但它会抛出一个已检查的异常,这不是很好。

另外,如果我们也可以在具体类型上使用该方法不是更好,例如如果a 是一个值为1、2、3 的ArrayList,我们可以调用add2(a) 并返回一个ArrayList?所以在改进的版本中,我们可以使签名通用:

static <T extends List<Integer>> T add2 (T lst) {
    T res;
    try {
        res = (T) lst.getClass().newInstance();

    } catch (InstantiationException e) {
        throw new IllegalArgumentException(e);
    } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
    }

    for (Integer i : lst) res.add(i + 2);
    return res;
}    

我认为,如果传入一个没有空构造函数的列表,则抛出运行时异常是最不糟糕的选择。我没有办法确保它确实如此。 (也许是 Java 8 类型的注释?)返回 null 会有点没用。

使用此签名的缺点是我们不能将ArrayList 等作为默认值返回,因为我们可以这样做来替代抛出异常,因为返回类型是保证与传入的类型相同。但是,如果用户确实想要返回 ArrayList(或其他一些默认类型),他可以制作 ArrayList 副本并使用该方法。

如果有 API 设计经验的人读到这篇文章,我很想知道您对哪个选项更可取的想法:1)返回一个需要显式转换回原始类型的 List,但允许返回一个不同的具体类型,或者 2) 确保返回类型相同(使用泛型),但是如果(例如)传入没有空构造函数的单例对象,则会冒异常的风险?

【讨论】:

    【解决方案6】:

    已经有很多答案了,但我想告诉你一个不同的方式来思考这个问题。

    您要执行的操作在函数式编程领域称为map。这是我们在函数式语言中一直在做的事情。

    M&lt;A&gt; 成为某种容器(在您的情况下,M 将是ListA 将是Integer;但是,容器可以是许多其他东西)。假设您有一个函数,将As 转换为Bs,即f: A -&gt; B。让我们将这个函数写成F&lt;A, B&gt; 类型,使用更接近Java 的符号。请注意,您可以拥有A = B,如您给出的示例(其中A = B = Integer)。

    那么,操作map定义如下:

    M<B> map(M<A>, F<A, B>)
    

    也就是说,该操作将返回一个M&lt;B&gt;,大概是通过将F&lt;A, B&gt; 应用于M&lt;A&gt; 中的每个A

    在实践中...

    Google 开发了一个很棒的库,名为 Guava,它为 Java 带来了很多函数式习语。

    在 Guava 中,map 操作称为transform,它可以对任何Iterable 进行操作。它还有更具体的实现,可以直接在列表、集合等上工作。

    使用 Guava,您要编写的代码如下所示:

    static List<Integer> add2(List<Integer> ns) {
      return Lists.transform(ns, new Function<Integer, Integer>() {
        @Override Integer apply(Integer n) { return n + 2; }
      }
    }
    

    就这么简单。

    这段代码不会触及原始列表,它只会提供一个新列表,根据需要计算其值(也就是说,除非需要,否则不会计算新创建的列表的值——它被称为 懒惰操作)。

    作为最后的考虑,您不可能绝对确定您将能够返回完全相同的 List 实现。正如许多其他人指出的那样,除非有非常具体的原因,否则您不应该真正关心。这就是为什么 List 是一个接口,你不关心实现。

    【讨论】:

    • 非常好,我得试试 Guava。
    【解决方案7】:

    只是为了向您表明在一般情况下您想要做的事情是不可能的,请考虑以下类:

    final class MyList extends ArrayList<Integer> {
      private MyList() {
        super.add(1);
        super.add(2);
        super.add(3);
      }
      private static class SingletonHolder {
        private static final MyList instance = new MyList();
      }
      public static MyList getInstance() {
        return SingletonHolder.instance;
      }
    }
    

    它是一个单例(顺便说一下,也是一个惰性的、线程安全的单例),它的唯一实例可以从MyList.getInstance() 获得。您不能可靠地使用反射(因为构造函数是私有的;要使用反射,您必须依赖专有的、非标准的、不可移植的 API 或可能因 SecurityManager 而中断的代码)。因此,您无法使用不同的值返回此列表的新实例。

    它也是final,所以你不能返回它的孩子。

    此外,还可以覆盖 ArrayList 中会修改列表的每个方法,从而使其真正成为不可变的单例。

    现在,为什么要返回完全相同的 List 实现?

    【讨论】:

    • 在这种情况下我不会,但在一般情况下,列表以不同的方式实现是有原因的(主要是性能)。这个想法是我有 1 种方法可以用于所有 List 类型。我想也许我的方法签名应该是static &lt;T extends List&lt;Integer&gt;&gt; T add2 (T lst)。关于无法将 getInstance() 用于单例,我可以在方法中捕获生成的异常并在这种情况下返回一个 ArrayList。
    猜你喜欢
    • 2020-02-16
    • 1970-01-01
    • 2017-09-25
    • 1970-01-01
    • 2021-10-31
    • 2010-12-02
    • 2011-11-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多