昨天我在写作业时遇到了一个问题。我完成了作业,但我仍然不明白为什么我的代码能工作。我必须编写一个排序函数,它将任何可比较的泛型对象的varargs作为参数并返回该参数。问题是我必须返回一个排序对象的数组。所以我必须了解更多关于varargs列表和数组的信息。
函数是这样定义的。
public <T extends Comparable<T>> T[] stableSort(T ... items)
在函数中,我列出了一个列表,我会对其进行排序并完成所有的工作
List<T> list = new ArrayList<T>(Arrays.asList(items));
在函数的末尾,我将list返回给Array,使其与输出类型T[]匹配。
list.toArray(items.clone());
我的问题是,既然我已经从varargs中列出了这个列表,为什么我必须在toArray函数中执行items.clone()。对我来说,这似乎是在做同样的两件事。我以为arrays.asList()会克隆要列出的数组的值,但我不明白为什么我要在toArray()中的代码末尾再次这样做。我知道这是正确的写法,因为我昨天完成了作业,从班上的论坛上找到了这个写法,但我仍然不明白为什么。
编辑
该任务要求我用排序后的文件创建一个新数组,然后返回它。由于类型擦除,如果不引用适合泛型的类,就不可能实例化泛型类型的数组。但是,varargs数组的类型是T,所以我应该克隆一个符合通用约束的类型的数组。我当时不知道该怎么做。所以我决定使用列表,让我的时间更轻松,直到最后期限。
我的问题是,既然我已经从varargs中列出了列表,为什么我必须做items.clone()
你说得对。不幸的是,如果只使用toArray()
方法,编译器将无法确定数组的类型。您应该得到一个编译错误,说明无法从Object[]转换为T[]。需要调用item.clone()
来帮助编译器进行类型推断。另一种方法是说return (T[])list.toArray
也就是说,我不会推荐这两种方法。首先将数组转换为列表并将其转换回数组是没有意义的。我看不到任何重要的收获,你甚至可以从这段代码中理解。
在我看来,这里有几个问题,这些问题可能会让我对为什么需要做什么感到困惑。
我以为arrays.asList()会克隆要列出的数组的值,但我不明白为什么我要在toArray()中的代码末尾再次这样做。
这可能只是它的类型,但应该明确的是,您不会克隆数组中的对象,而是只使用对数组中对象的引用创建一个新的列表。数组中的对象本身将与列表中的对象相同。我相信这可能就是你的意思,但这里的术语可能很棘手。
我以为arrays.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
,但请记住,它仍然只是一个引用列表。items
和list
引用相同的对象,只是使用它们自己的顺序。
我的问题是,既然我已经从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
)是相同的数组。你看,varargs只不过是一个带有数组参数的方法的语法糖,并且是这样实现的。也许调用者没有期望他作为参数传入的数组被更改,并且可能仍然需要具有原始顺序的数组。首先进行克隆可以避免这种情况,并使该方法的效果不那么令人惊讶。在这种情况下,良好的方法文档是至关重要的。
测试您的任务实现的代码可能想要返回一个不同的数组,而您的方法遵守该约定是一个实际的收获。
编辑:
实际上,您的代码可以简单得多。您将通过实现同样的效果
T[] copy = items.clone();
Arrays.sort(copy);
return copy;
但你的任务可能是自己实际实现排序算法,所以这一点可能没有意义。
您需要使用以下内容:
List<T> list = new ArrayList<T>(Arrays.asList(items));
当您想要执行内联声明时。
例如:
List<String> list = new ArrayList<String>(Arrays.asList("aaa", "bbb", "ccc"));
顺便说一句,您不必使用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[]
,但它们是不同的,因为实际返回的运行时类是不同的。