使用Java创建一个集合,其中包含n个对象的克隆。

7

在Java中,是否有一种一行代码的方法来创建一个集合,并使用一个对象的n个克隆进行初始化?

我想要的等价物是:

  • foo = vector<vector<int> >(10); C++,创建10个不同的空向量
  • [ [] for i in range(10) ] Python,包含10个不同的空数组的数组
  • Array.new(10) { [] } Ruby,与Python相同

在Java中,我只发现了

new ArrayList<ArrayList<Integer> >(Collections.nCopies(10, new ArrayList<Integer>()))

然而,这与其他示例不同,因为列表是别名。
有没有一种方法可以创建一个不使用for循环的独特对象克隆数组,并且最好不要使用外部库
4个回答

13
如果您正在使用Java 8,则可以使用其流:
Stream.generate(ArrayList<Integer>::new)
    .limit(10).collect(Collectors.toList());

The Stream.generate() 方法接受一个能够产生值的Supplier,并生成一个无限流(每个值都是通过再次调用供应商获得的,因此它们都是不同的,与Collections.nCopies()不同)。在流上放置一个limit(),然后将结果收集到列表中,从而得到一个不同条目的列表。
请注意,从Java 16开始,Stream有一个toList()方法,因此这可能会变得更加简洁:
Stream.generate(ArrayList<Integer>::new).limit(10).toList();

3

对于那些想要传递构造函数参数的人(这在被接受的答案和供应商中是不可能的,如此处所述) - 您可以使用以下方法(我不知道是否有更好的解决方案,但至少满足了我的需求):

final List<MyObject> result = IntStream.range(0, n)
   .mapToObj(index -> new MyObject(...))
   .collect(Collectors.toList());

您需要将n替换为要填充列表的元素数量,将MyObjectnew MyObject(...)分别替换为您的类型和构造函数调用。

这将创建一个整数流,范围从0到n(不包括n),将每个“索引”映射到mapToObj中lambda表达式返回的任何对象,并最终将流转换为包含n个不同实例的列表MyObject


3
如果你需要索引来完成某件事情,那么这肯定是最好的选择,即使不需要索引也是如此。但值得注意的是,被接受的答案可以使用其他参数。Stream.generate(() -> new MyObject(someFinalVar)).limit(n).collect(Collectors.toList()) - Matthew Read

1
即使Java 8引入了Supplier,但不幸的是,并没有像nCopies那样简洁明了的一行代码。老实说,我不知道为什么。(尽管@DavidConrad已经证明Stream可以做到这一点。)
你可以轻松地自己创建一个,例如:
public static <E, L extends List<? super E>> L fill(
        L list, Supplier<E> sup, int n) {
    for(; n > 0; --n)
        list.add(sup.get());
    return list;
}

调用方式如下:

List<List<Integer>> list = ArrayUtils.fill(
    new ArrayList<>, ArrayList<Integer>::new, 10
);

对于数组,现在有一个名为Arrays#setAll的新方法:
Integer[] oneToTen = new Integer[10];
Arrays.setAll(oneToTen, i -> i + 1);
List<Integer> asList = Arrays.asList(oneToTen);

但它是一个void方法,因此不能在单个语句中使用。(个人备注:为什么Java API不能流畅地使用?)
在Java 8之前,没有库方法来执行此操作,创建一个更加繁琐。由于clone是受保护的,因此无法通用调用。反射可以做到这一点,但反射非常繁琐。

啊,我没想到你可以写ArrayList<Integer>::new。这简化了我的代码中的Supplier。+1,谢谢。 - David Conrad
@DavidConrad 随便拿走,不用客气。;) - Radiodef
1
刚刚完成了。这就是我喜欢StackOverflow的原因。 - David Conrad

0
  • vector<vector<int> > = new vector<vector<int> >(10); 语法不正确,但假设你的意思是 vector<vector<int> > foo(10);。你正在使用填充构造函数,它将初始化容器大小,然后将每个元素初始化为value_type参数的副本(如果您没有指定任何内容,则使用默认构造函数)。这将使用循环。

  • [ [] for i in range(10) ]Array.new(10) { [] } 只是在一行上执行循环并复制一个空列表类型结构。

正如您所指出的,nCopies 方法并不等同,因为结果是不可变的,而且您没有创建副本(或克隆)。当访问每个索引时,使用对同一元素的引用。请参见openjdk copies implementation 进行参考。

Java 的一些困难在于没有像 C++ 中那样保证默认构造函数,并且语法与大多数脚本语言有所不同。这可能是一个很好的机会,可以花点时间了解底层发生了什么,以确保您的解决方案不会做更多不必要的工作。一些后续问题需要自问:

  • 除了循环之外,您还考虑过其他结构吗?我认为在某个层面上会有一个循环。
  • 您真正想要如何初始化外部ArrayList中的ArrayList对象?例如,您希望它们的大小是多少?它们是否需要最初填充任何内容?您期望它们增长到多大?它们需要是统一大小还是有些更大/更小?延迟初始化这些列表是否有意义?

为了回答这些问题,编写自己的通用静态初始化程序可能是一个好的实践。在您掌握简单情况后,如果您的用例有所变化,使用工厂模式初始化内部列表可能会使您的解决方案更加通用。正如您所看到的,有许多问题需要考虑,在简单情况下,您可能只会得到类似以下的东西:

public static <T> List<List<T>> newListofLists(int outerSize, int innerSize, T value) {
    List<List<T>> outer = new ArrayList<List<T>>(outerSize);
    for (int i = 0; i < outer.size(); ++i) {
        List<T> inner = new ArrayList<T>(innerSize);
        outer.add(inner);
        for (int j = 0; j < inner.size(); ++j) {
            inner.add(value);
        }
    }
    return outer;
}

这样可以用一行代码初始化你的列表:

List<List<Integer>> myList = newListofLists(10, 5, -1);

1
谢谢!我已经纠正了我的C++语法。关于你的其他评论:虽然列表推导和循环在语义上是等价的,但我并不一定认为列表推导就是循环。而且C++版本可能会在幕后使用memcpy,这是一个循环(或者是一种特殊的CPU指令?),但也有所不同。无论如何,这更多地涉及到简洁的代码而不是其他方面的考虑...但你提出这些问题是好的。 - Sjlver
你可以使用Arrays.fill作为内部循环的替代方案。然而,这种方法的好处可能会受到堆内存的限制。使用直接ByteBuffer可能允许直接使用像memcpy这样的操作。 - Scott Mitchell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接