.clone() 或 Arrays.copyOf()?

49
在努力减少可变性方面,我们是否应该使用 finalconst
public void setValues(String[] newVals) {

     this.vals = ( newVals == null ? null : newVals.clone() );
}
或者
public void setValues(String[] newVals) {

     this.vals = ( newVals == null ? null : Arrays.copyOf(newVals, newVals.length) );
}

2
除非valsObject vals,否则以上内容无法编译。否则,这些方法是可比较的。clone是一个更简单的调用(除了丑陋的向下转换)。 - Marko Topolnik
1
System.arrayCopy()怎么样?(这比你的两个选项更快) - assylias
1
@assylias 你确定吗?clone应该至少和它一样快,它会对实例数据进行逐字节的复制。 - Marko Topolnik
相关:https://dev59.com/JEnSa4cB1Zd3GeqPLitr - assylias
可能是重复的问题,参考链接:https://dev59.com/CG025IYBdhLWcg3wvIu2 - nhahtdh
@MarkoTopolnik 看看我的回答 - 你是对的。 - assylias
4个回答

44

使用jmh进行更新

使用jmh,我得到了类似的结果,只是clone似乎略微更好一些。

原始帖子

我进行了一个快速的性能测试:cloneSystem.arrayCopyArrays.copyOf的性能非常相似(jdk 1.7.06,服务器VM)。

具体细节(以毫秒为单位),经过JIT后:

clone:68
arrayCopy:68
Arrays.copyOf:68

测试代码:

public static void main(String[] args) throws InterruptedException,
        IOException {
    int sum = 0;
    int[] warmup = new int[1];
    warmup[0] = 1;
    for (int i = 0; i < 15000; i++) { // triggers JIT
        sum += copyClone(warmup);
        sum += copyArrayCopy(warmup);
        sum += copyCopyOf(warmup);
    }

    int count = 10_000_000;
    int[] array = new int[count];
    for (int i = 0; i < count; i++) {
        array[i] = i;
    }

    // additional warmup for main
    for (int i = 0; i < 10; i++) {
        sum += copyArrayCopy(array);
    }
    System.gc();
    // copyClone
    long start = System.nanoTime();
    for (int i = 0; i < 10; i++) {
        sum += copyClone(array);
    }
    long end = System.nanoTime();
    System.out.println("clone: " + (end - start) / 1000000);
    System.gc();
    // copyArrayCopy
    start = System.nanoTime();
    for (int i = 0; i < 10; i++) {
        sum += copyArrayCopy(array);
    }
    end = System.nanoTime();
    System.out.println("arrayCopy: " + (end - start) / 1000000);
    System.gc();
    // copyCopyOf
    start = System.nanoTime();
    for (int i = 0; i < 10; i++) {
        sum += copyCopyOf(array);
    }
    end = System.nanoTime();
    System.out.println("Arrays.copyOf: " + (end - start) / 1000000);
    // sum
    System.out.println(sum);
}

private static int copyClone(int[] array) {
    int[] copy = array.clone();
    return copy[copy.length - 1];
}

private static int copyArrayCopy(int[] array) {
    int[] copy = new int[array.length];
    System.arraycopy(array, 0, copy, 0, array.length);
    return copy[copy.length - 1];
}

private static int copyCopyOf(int[] array) {
    int[] copy = Arrays.copyOf(array, array.length);
    return copy[copy.length - 1];
}

18
结论是clone()是最佳选择,因为它是一个零元方法(没有参数可以搞砸),并且在数组类型上由于返回类型协变而尽可能地类型安全。 - gustafc
有一个来自Carrot Search Labs的JUnit插件可以用于进行基准测试,名为JunitBenchmarks。它使得创建微基准测试变得非常容易。 - Christian Trimble
@C.Trimble 我过去使用过caliper,它的表现很好。 - assylias
@assylias,我刚刚阅读了卡尺文档,看起来非常有趣。 - Christian Trimble
4
Arrays.copyOf 内部使用 System.arrayCopy,因此它们之间无需进行比较。 - Bharat DEVre

7
请考虑使用"clone()"的安全性。一类已知攻击使用覆盖对象"clone()"方法的类来注入恶意代码。例如,CVE-2012-0507(针对Mac OS的"Flashback"攻击)通过基本上将".clone()"调用替换为".copyOf"调用来解决。有关"clone()"过时性的其他讨论可在StackOverflow上找到,链接如下:object cloning with out implementing cloneable interface

晚了点,但出于好奇,是否可以合成扩展一个数组类型。当然,你不能使用常规的Java语法来做到这一点。 - Valentin Ruano
看你链接的那个案例,似乎很奇怪它会起作用...也许是过于激进的编译器优化导致没有检查传递给E[]的值是否实际上是一个数组,因为内联代码只执行了clone,而所有对象都支持它。 - Valentin Ruano
我明白了,这实际上只能通过低级别的无效操作对象指针(称为“类型混淆”)来实现。 - Valentin Ruano

4
我编写了一个简单的程序来检查差异。
public static void main(String[] args) throws IOException, InterruptedException,
        PrinterException
{
  //Verify array remains immutable.

  String[] str =  {"a","b","c"};
  String[] strings  = str.clone();
  //change returned array
  strings[2]= "d";
  System.out.println(Arrays.toString(str));
  System.out.println(Arrays.toString(strings));

  String[] stringsCopy = Arrays.copyOf(str, str.length);
  stringsCopy[2]= "d";
  System.out.println(Arrays.toString(str));
  System.out.println(Arrays.toString(stringsCopy));

  //peformance
  long before = System.currentTimeMillis();
  for(int i=0;i<Integer.MAX_VALUE;i++)
  {
      str.clone();
  }
  System.out.println("Time Required for Clone: "+ (System.currentTimeMillis()-before));

  //peformance
  long beforeCopy = System.currentTimeMillis();
  for(int i=0;i<Integer.MAX_VALUE;i++)
  {
      Arrays.copyOf(str, str.length);
  }
  System.out.println("Time Required for Copy of: "+ (System.currentTimeMillis()-beforeCopy));

}

并且它输出

[a, b, c]
[a, b, d]
[a, b, c]
[a, b, d]
Time Required for Clone: 26288
Time Required for Copy of: 25413

所以在这两种情况下,String[]都是不可变的,并且性能几乎相同,尽管在我的机器上Arrays.copyOf()稍微快一些。
更新:
我将程序更改为创建一个大数组[100个字符串],而不是一个小数组。
  String[] str =  new String[100];

  for(int i= 0; i<str.length;i++)
  {
      str[i]= Integer.toString(i);
  }

clone 方法之前移动了 copy of 方法。以下是结果。

 Time Required for Copy of: 415095
 Time Required for Clone: 428501

这些都是同样的内容。请不要让我再次运行测试,因为需要一段时间 :(

更新2

对于字符串数组1000000和迭代次数10000

Time Required for Copy of: 32825
Time Required for Clone: 30138

copy ofclone 花费更多的时间。


我的意思是你已经运行了整个基准测试多少次?结果可能因每次运行而异 - 你也确定垃圾回收没有干扰你的结果了吗? - assylias
@AmitD 如果您交换循环(先使用Arrays.copyOf,然后使用clone),是否会得到相同的结果? - assylias
@Marko Topolnik,@assylias 我已经更新了程序以解决两个建议,但由于数组长度更长,所以需要一些时间。程序完成后我会更新结果。 - Amit Deshpande
@MarkoTopolnik 更新了数组大小为百万的第二个版本。 - Amit Deshpande
非常感谢您的回答和对这个主题的详细工作。 - Bruno Grieder
显示剩余7条评论

1
就可变性而言,它们将提供完全相同的数据浅拷贝。

3
浅复制不可变对象是一种完全有效的方法。 - Marko Topolnik
1
@BGR 的 copyOf 方法使用了 System.arraycopy,我想 clone 也应该是使用它。因此它们应该有相同的性能(尽管 copyOf 对于类型有一些额外的检查,所以可能稍微慢一些)。 - jdevelop
只有在指定结果数组类型时,“copyOf”才需要类型检查。System.arraycopy同理。 - Marko Topolnik

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