【问题标题】:Converting a generic List to an Array. Why do I need use clone?将通用列表转换为数组。为什么我需要使用克隆?
【发布时间】:2017-02-05 15:03:54
【问题描述】:

我昨天在写作业时遇到了一个问题。我完成了作业,但我仍然不明白为什么我的代码有效。我必须编写一个排序函数,它将任何可比较的通用对象的可变参数作为参数并返回参数。问题是我必须返回一个排序对象数组。所以我不得不学习更多关于可变参数列表和数组的知识。

函数是这样定义的。

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

在函数内部,我创建了一个列表,我会对它进行排序并完成所有工作。

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

在函数的最后,我返回了 list toArray 以便它匹配输出类型 T[]。

list.toArray(items.clone());

我的问题是,既然我已经从可变参数中创建了列表,为什么我必须在 toArray 函数中执行 items.clone() 。这似乎对我做了两件事。我认为arrays.asList() 会将数组的值克隆到列表中,但我不明白为什么要在 toArray() 中的代码末尾再次这样做。我知道这是正确的写法,因为我昨天完成了作业,从班上的论坛上找到了这种写法,但我仍然不明白为什么。

编辑

该任务要求我创建一个包含已排序文件的新数组并将其返回。由于类型擦除,如果没有对适合泛型的类的引用,就无法实例化泛型类型的数组。但是,可变参数数组的类型为 T,所以我应该克隆一个符合泛型约束的类型的数组。我不知道该怎么做。所以我决定使用列表来让我在截止日期前的时间更轻松。

【问题讨论】:

  • 区别在于asList返回List,而toArray返回数组。
  • @Andremoniy 我认为问题是为什么要克隆?
  • 这似乎毫无意义。该数组被包装为一个不需要的列表,但只是为了确保它被包装在另一个列表中,也不需要,然后克隆该数组,该数组创建一个包含所有项目的数组,但将所有项目复制到其中刚刚创建的列表,即相同的项目。
  • @Andremoniy 查看我的回答以了解为什么需要这样做。
  • 为什么是近距离投票?这个问题只需要仔细阅读并将自己移入仍在学习理解为什么会出现混乱的人的脑海中。此外,@PeterLawrey 认为其中一些步骤是不必要的,这表明对其中一些方法缺乏了解。就像 asList 实际上是底层数组的视图并传递更改,而 toArray 可能(但不总是)返回作为参数提供的数组。

标签: java arrays list generics variadic-functions


【解决方案1】:

我的问题是,既然我已经从 varargs 中列出了列表,为什么我必须做 items.clone()

你是对的。不幸的是,如果您只使用toArray() 方法,编译器将无法确定数组的类型。您应该得到一个编译错误,提示 Cannot convert from Object[] to T[]。需要调用item.clone() 来协助编译器进行类型推断。另一种方法是说return (T[])list.toArray

也就是说,我不会推荐任何一种方法。将数组转换为列表并首先将其转换回数组并没有什么意义。我没有看到任何你甚至可以从这段代码中理解的重要内容。

【讨论】:

  • 是时候让所有不带评论的投票都开放了。看起来确实有一群不合适的用户这样做了
  • 投反对票的人能否更礼貌一些并发表评论以解释我的回答有什么问题?这似乎有点不道德。
  • "另一种方法是说return (T[])list.toArray" 如果你的意思是return (T[])list.toArray();,这是一个可怕的想法,因为这是堆污染,当它返回到需要T 的具体类型的上下文。
  • @newacct 整个问题首先是一个可怕的想法,正如我在问题的最后一段中已经提到的那样。我们甚至不要谈论堆污染,因为那是次要的。我解释了为什么部分而不是如何正确地做部分。随意发布满足您渴望的答案。
  • @CKing 正如他所说,OP 正在做作业。重点是排序算法的实现,所以这不像是生产代码。在选择使用List作为中介时,他只是碰巧遇到了一个令人困惑的情况。为了优化代码,他只需要克隆到数组(因为要求是返回一个新数组)并在该克隆而不是列表上实现排序。
【解决方案2】:

在我看来,这里有几个问题,这些问题可能会一起造成一些关于为什么需要做的混乱。

我认为 arrays.asList() 会将数组的值克隆到列表中,但我不明白为什么我要在 toArray() 中的代码末尾再次这样做。

这可能只是它的键入方式,但应该明确的是,您不会克隆数组中的 对象,而只是使用对对象的引用创建一个新 List在数组中。对象本身将与数组中的对象与列表中的对象相同。我相信这可能就是您的意思,但这里的术语可能很棘手。

我认为 arrays.asList() 会将数组的值克隆到列表中...

不是真的。使用Arrays.asList(T[] items) 将为数组items 提供一个视图,该数组实现了java.util.List 接口。这是一个固定大小的列表。你不能添加它。对它的更改,例如替换元素或就地排序,将传递到底层数组。所以如果你这样做

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 引用了相同的对象,只是按照它们自己的顺序。

我的问题是,既然我已经从可变参数中创建了列表,为什么我必须在 toArray 函数中执行 items.clone()。

这里可能有两个原因。第一个是CKing在他/她的回答中所说的。由于类型擦除和数组在 Java 中实现的方式(有不同的数组类型,具体取决于它是基元数组还是引用数组),如果您只是在列表中调用 toArray(),JVM 将不知道要创建哪种类型的数组,这就是为什么该方法的返回类型为Object[]。因此,为了获取特定类型的数组,您必须向方法提供一个数组,该数组可在运行时用于确定来自的类型。这是 Java API 的一部分,其中泛型通过类型擦除工作,不会在运行时保留,而数组工作的特殊方式结合在一起让开发人员感到惊讶。 A bit of abstraction is leaking there.

但可能还有第二个原因。如果你去查看toArray(T[] a) method in the Java API,你会注意到这部分:

如果列表适合指定的数组,则在其中返回。否则,将使用指定数组的运行时类型和此列表的大小分配一个新数组。

假设另一个开发人员的某些代码正在使用您的 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;

但您的任务可能是自己实际实现排序算法,所以这一点可能没有实际意义。

【讨论】:

  • 是的,我的任务是实现排序算法。这本身非常简单,但是我没有遇到过的泛型类型和可变参数让我很困惑。
【解决方案3】:

你需要使用这个:

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

当你想做一个内联声明时

例如:

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

【讨论】:

    【解决方案4】:

    顺便说一句,您不必使用return list.toArray(items.clone()); 例如,您可以使用return list.toArray(Arrays.copyOf(items, 0));,在那里您将传递给list.toArray() 一个不包含来自items 的任何参数的空数组.

    将参数传递给接受参数的list.toArray() 版本的全部意义在于提供一个数组对象,其实际运行时类是它想要返回的数组对象的实际运行时类。这可以通过items.clone()items 本身实现(尽管这会导致list.toArray() 将结果元素写入items 指向的原始数组中,您可能不希望发生这种情况),或者,正如我在上面展示的那样,一个具有相同运行时类的空数组。

    顺便说一句,需要将参数传递给list.toArray() 根本不是泛型类型问题。即使您使用前泛型 Java 编写了此代码,您也必须做同样的事情。这是因为不带参数的List::toArray() 版本总是返回一个数组对象,其实际运行时类是Object[],因为List 在运行时不知道它的组件类型是什么。要让它返回一个实际运行时类不同的数组对象,你必须给它一个正确的运行时类的示例数组对象以跟随。这就是为什么预泛型 Java 也有带有一个参数的 List::toArray() 版本;尽管在预泛型中,两种方法都声明返回Object[],但它们是不同的,因为返回的实际运行时类不同。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-25
      • 1970-01-01
      • 2011-09-25
      相关资源
      最近更新 更多