将通用列表转换为数组。为什么需要使用克隆?

3

昨天我在写作业时遇到一个问题。我完成了作业,但仍然不太理解为什么我的代码能够工作。我必须编写一个排序函数,该函数接受任何可比较的通用对象的varargs作为参数,并返回该参数。问题在于我必须返回已排序的对象数组。因此,我必须学习更多有关varargs列表和数组的知识。

该函数定义如下。

public <T extends Comparable<T>> T[] stableSort(T ... items)

在函数内部,我创建了一个列表,对其进行排序并执行所有操作。

List<T> list = new ArrayList<T>(Arrays.asList(items));

在函数末尾,我返回了列表toArray,以使其匹配输出类型T[]。

list.toArray(items.clone());

我的问题是,既然我已经从可变参数(varargs)制作了列表,为什么我还要在toArray函数中执行items.clone()。这似乎对我来说是做同样的两件事情。我认为arrays.asList()会将数组值克隆到列表中,我不明白为什么我在代码末尾的toArray()中又要这么做一遍。我知道这是编写它的正确方式,因为昨天我完成了作业并从班级论坛中找到了这种方法,但我仍然不明白为什么。
编辑
该任务要求我创建一个新的已排序文件数组并返回它。由于类型擦除,不可能实例化一个泛型类型的数组,而没有适合泛型的类的引用。但是,可变参数数组具有类型T,因此我应该克隆一个符合通用约束条件的类型的数组。由于时间紧迫,我不知道如何做,所以决定使用列表来使我的时间更轻松,直到截止日期。

2
区别在于 asList 返回 List,而 toArray 返回数组。 - Andremoniy
1
@Andremoniy 我认为问题是“为什么克隆?” - Chetan Kinger
@Andremoniy 我原以为arrays.asList()会将数组的值克隆到列表中,我不明白为什么我在代码末尾又要再次执行toArray()。 - Chetan Kinger
1
@Andremoniy,请看我的回答以了解为什么需要这样做。 - Chetan Kinger
1
为什么要关闭投票?这个问题只需要仔细阅读,并将自己移入一个仍在学习理解可能存在困惑的人的思维中。此外,@PeterLawrey 如果认为某些步骤是不必要的,就显示出对某些方法的理解不足。比如,asList 实际上是基础数组的视图,并且会传递变化,而 toArray 可能(但不总是)返回提供的参数数组。 - G_H
显示剩余3条评论
4个回答

3
我的问题是,既然我已经从可变参数中创建了列表,为什么我还需要执行items.clone()?
你是正确的。不幸的是,如果你简单地使用toArray()方法,编译器将无法确定数组的类型。你应该会得到一个编译错误,说“无法从Object[]转换为T[]”。调用item.clone()是为了帮助编译器进行类型推断。另一种方法是使用“return (T[])list.toArray”。
话虽如此,我不建议使用这两种方法。首先,将数组转换为列表,然后再将其转换回数组并没有太多意义。我认为你甚至不会从这段代码中获得任何重要的启示。

是时候让所有无评论的投票公开了。看起来有一群非常不适当的用户这样做了。 - Andremoniy
能否请那位点踩的人更加礼貌一些,留下评论解释一下我的回答有什么问题?在我看来,这样做有点不道德。 - Chetan Kinger
另一种方法是使用return (T[])list.toArray。如果你的意思是return (T[])list.toArray();,那么这是一个可怕的想法,因为这会造成堆污染,并且在返回到期望T的具体类型的上下文中时会导致类转换异常。 - newacct
@newacct 整个问题本身就是一个可怕的想法,正如我在问题的最后一段所提到的那样。我们甚至不需要谈论堆污染,因为那只是次要的。我解释了“为什么”部分而不是“如何正确地做”的部分。随意发布一个满足您渴望的答案。 - Chetan Kinger
@CKing,正如他所说,OP正在做作业。重点是实现排序算法,因此这不会成为生产代码。他只是在选择使用List作为中介时遇到了困惑的情况。为了优化代码,他只需要克隆数组(因为要求返回一个新数组),并在该克隆上实现排序,而不是在List上实现。 - G_H

2
我觉得这里有几个问题,可能会让人们对为什么需要做什么产生一些困惑。
“我以为数组的asList()方法会将数组的值克隆到列表中,但我不明白为什么在代码结尾的toArray()方法中要再次这样做。”
这可能只是打字时的问题,但应该明确指出,您并没有克隆数组中的对象,而只是创建了一个新的列表,并将其中的对象引用指向数组中的对象。这些对象本身将是数组和列表中相同的。我认为这可能是您的意思,但术语在这里可能会比较棘手。
“我以为数组的asList()方法会将数组的值克隆到列表中......”
实际上并不是这样。使用Arrays.asList(T[] items)将提供一个视图,该视图实现了java.util.List接口,可以查看items数组。这是一个固定大小的列表。您无法向其中添加任何元素。对它所做的更改(例如替换元素或就地排序)将传递到底层数组。因此,如果您这样做:
List<T> l = Arrays.asList(T[] items);
l.set(0, null);

“...你刚刚将实际数组items的索引0处的元素设置为null。”
“你的代码中执行此操作的部分”
List<T> list = new ArrayList<T>(Arrays.asList(items));

这句话可以翻译为:“可以写成这样:”
List<T> temp = Arrays.asList(items);
List<T> list = new ArrayList<T>(temp);

第一行是“视图”,第二行将有效地创建一个新的java.util.ArrayList,并按它们的迭代器返回的顺序填充它们的视图值(即数组中的顺序)。因此,您现在对list所做的任何更改都不会更改数组items,但请记住,它仍然只是引用列表。 itemslist引用相同的对象,只是它们自己的顺序而已。

我的问题是,既然我已经从varargs中制作了列表,为什么我还要在toArray函数中使用items.clone()。

这里可能有两个原因。第一个原因就像CKing在他/她的答案中所说的那样。由于类型擦除和Java中数组的实现方式(根据是基本类型数组还是引用类型数组有不同的数组类型),如果你只在列表上调用toArray()方法,JVM将不知道要创建什么类型的数组,这就是为什么该方法具有Object[]返回类型的原因。因此,为了获得特定类型的数组,必须提供一个数组给该方法,在运行时可以从该数组中确定类型。这是Java API的一部分,其中泛型通过类型擦除工作,不会在运行时保留,并且数组的特定工作方式都会使开发人员感到惊讶。有点抽象正在泄漏
但可能还有第二个原因。如果你去查看Java API中的toArray(T[] a)方法,你会注意到这部分内容:
如果列表适合指定的数组,则返回该列表。否则,将分配一个新数组,其运行时类型为指定数组的类型,并具有此列表的大小。
假设另一个开发人员编写的代码使用您的stableSort方法,如下所示:
T[] items;
// items is created and filled...
T[] sortedItems = stableSort(items);

如果你没有进行克隆,你的代码将会是这样的:
List<T> list = new ArrayList<T>(Arrays.asList(items));
// List is now a new ArrayList with the same elements as items
// Do some things with list, such as sorting
T[] result = list.toArray(items);
// Seeing how the list would fit in items, since it has the same number of elements,
// result IS in fact items

现在,您的代码的调用者会得到sortedItems,但该数组与他传入的数组相同,即items。您看,可变参数仅仅是带有数组参数的方法的语法糖,并且是以这种方式实现的。也许调用者没有预料到他传入的数组会被改变,可能仍然需要具有原始顺序的数组。首先进行克隆将避免这种情况并使方法的效果更少令人惊讶。在这种情况下,良好的方法文档非常重要。
测试您的任务实现代码的可能需要不同的数组作为返回值,这是您的方法必须遵守的实际需求。
编辑:
实际上,您的代码可以更简单。您可以通过以下方式实现相同的效果:
T[] copy = items.clone();
Arrays.sort(copy);
return copy;

但是你的任务可能是要自己实现一个排序算法,所以这一点可能无关紧要。

是的,我的任务是实现排序算法。本身很简单,但是我没有遇到过的泛型和可变参数让我感到困惑。 - GreedyAi

0

顺便说一下,你不必使用return list.toArray(items.clone());。例如,你可以使用return list.toArray(Arrays.copyOf(items, 0));,在这里你将一个不包含items参数的空数组传递给list.toArray()

向带有参数的list.toArray()版本传递参数的整个目的是提供一个实际运行时类为要返回的数组对象的实际运行时类的数组对象。这可以通过items.clone()items本身(尽管这会导致list.toArray()将结果元素写入指向items的原始数组中,这可能不是你想要发生的),或者如我上面所示的具有相同运行时类的空数组来实现。

顺便提一下,将参数传递给list.toArray()并不是泛型类型问题。即使您使用了预先生成的Java编写了此代码,您也必须执行相同的操作。这是因为没有参数的List::toArray()版本始终返回一个实际运行时类为Object[]的数组对象,因为在运行时,List不知道其组件类型是什么。要让它返回一个实际运行时类为其他类型的数组对象,您必须给它一个正确运行时类的示例数组对象来跟随。这就是为什么预先生成的Java也有一个带有一个参数的List::toArray()版本的原因;即使在预先生成的情况下,这两种方法都被声明为返回Object[],但它们是不同的,因为实际返回的运行时类是不同的。


0
你需要使用这个:
List<T> list = new ArrayList<T>(Arrays.asList(items));

当你想要进行 内联声明 时。

例如:

List<String> list = new ArrayList<String>(Arrays.asList("aaa", "bbb", "ccc"));

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