【问题标题】:Java Collection performance questionJava Collection 性能问题
【发布时间】:2010-05-27 07:59:28
【问题描述】:

我创建了一个方法,它接受两个Collection<String> 作为输入并将一个复制到另一个。

但是,我不确定是否应该在开始复制之前检查集合是否包含相同的元素,或者是否应该无论如何都应该复制。方法是这样的:

 /**
  * Copies from one collection to the other. Does not allow empty string. 
  * Removes duplicates.
  * Clears the too Collection first
  * @param src
  * @param dest
  */
 public static void copyStringCollectionAndRemoveDuplicates(Collection<String> src, Collection<String> dest) {
  if(src == null || dest == null)
   return;

  //Is this faster to do? Or should I just comment this block out
  if(src.containsAll(dest))
   return;

  dest.clear();
  Set<String> uniqueSet = new LinkedHashSet<String>(src.size());
  for(String f : src) 
   if(!"".equals(f)) 
    uniqueSet.add(f);

  dest.addAll(uniqueSet);
 }

也许直接删除会更快

if(src.containsAll(dest))
    return;

因为这个方法无论如何都会遍历整个集合。

【问题讨论】:

  • 只是一个小评论,与您的问题无关:target 和 des 具有相似的含义。由于您正在将非空字符串从目标复制到目标,也许它可以重命名为src?

标签: java performance collections


【解决方案1】:

我会说:删除它!它是重复的“代码”,Set 正在执行相同的“包含()”操作,因此无需在此处对其进行预处理。除非你有一个庞大的输入集合和对 containsAll() 的出色 O(1) 测试;-)

Set 足够快。它具有基于输入大小的 O(n) 复杂度(一个 contains() 和(可能)每个字符串的一个 add() 操作),如果 target.containsAll() 测试失败,则 contains() 执行两次对于每个字符串 -> 性能较差。

编辑

一些伪代码来可视化我的答案

void copy(source, dest) {
  bool:containsAll = true;
  foreach(String s in source) {  // iteration 1
    if (not s in dest) {         // contains() test
       containsAll=false
       break
    }
  }
  if (not containsAll) {
    foreach(String s in source) { // iteration 2
      if (not s in dest) {        // contains() test
        add s to dest
      }
    }
  }
}

如果所有源元素都在 dest 中,则为每个源元素调用一次 contains()。如果除了最后一个源元素之外的所有元素都在 dest 中(最坏情况),则 contains() 被调用 (2n-1) 次(n=源集合的大小)。 但是 with 额外测试的 contains() 测试总数总是等于或大于没有额外测试的相同代码。

编辑 2 假设,我们有以下集合:

source = {"", "a", "b", "c", "c"}
dest = {"a", "b"}

首先, containsAll 测试失败,因为 source 中的空字符串不在 dest 中(这是您代码中的一个小设计缺陷;))。然后创建一个临时集,它将是{"a", "b", "c"}(空字符串和第二个“c”被忽略)。最后你添加一切到 dest 并假设 dest 是一个简单的 ArrayList,结果是{"a", "b", "a", "b", "c"}。是这个意图吗?更短的选择:

void copy(Collection<String> in, Collection<String> out) {
  Set<String> unique = new HashSet<String>(in);
  in.remove("");
  out.addAll(unique);
}

【讨论】:

  • 假设我们删除了 Set 并创建了一个带有 Collection&lt;T&gt; 的副本。在添加之前检查是否相等是可行的吗?
【解决方案2】:

如果target 的元素多于destcontainsAll() 将无济于事:
目标:[a,b,c,d]
dest: [a,b,c]
target.containsAll(dest) 为真,所以 dest 是 [a,b,c] 但应该是 [a,b,c,d]。

我觉得下面的代码更优雅:

Set<String> uniqueSet = new LinkedHashSet<String>(target.size());
uniqueSet.addAll(target);
if(uniqueSet.contains(""))
    uniqueSet.remove("");

dest.addAll(uniqueSet);

【讨论】:

  • 同意...我什至会跳过对contains的呼叫。
  • 谢谢,我没想到。实际上,目标的元素很可能比目标多
【解决方案3】:

如果它那么重要,您可以对其进行基准测试。我认为对 containsAll() 的调用可能没有帮助,尽管它可能取决于两个集合具有相同内容的频率。

但这段代码令人困惑。它正在尝试向dest 添加新项目?那为什么要先清除呢?只需将您的新 uniqueSet 返回给调用者,而不是打扰。你的containsAll() 支票不是倒过来了吗?

【讨论】:

  • 很可能集合内容相同,至少被调用10次
【解决方案4】:
  1. 太多令人困惑的参数名称。 desttarget 的含义几乎相同。你最好选择像destsource 这样的东西。即使对您来说,它也会让事情变得更加清晰。

  2. 我有一种感觉(不确定是否正确)您以错误的方式使用集合 API。接口Collection 没有说明其元素的唯一性,但您将这种质量添加到它。

  3. 修改作为参数传递的集合并不是最好的主意(但像往常一样,这取决于)。在一般情况下,可变性是有害且不必要的。此外,如果传递的集合是不可修改/不可变的怎么办?最好返回新集合然后修改传入集合。

  4. Collection 接口有方法addAllremoveAllretainAll。你先尝试了吗?您是否对以下代码进行了性能测试:

    Collection<String> result = new HashSet<String> (dest);
    result.addAll (target);
    

    target.removeAll (dest);
    dest.addAll (target);
    

【讨论】:

    【解决方案5】:

    代码难以阅读,效率不高。 “dest”参数令人困惑:它作为参数传递,然后被清除并将结果添加到其中。它是一个参数有什么意义?为什么不简单地返回一个新集合?我能看到的唯一好处是调用者可以确定集合类型。有必要吗?

    我认为这段代码可以写得更清楚,也可能更高效:

    public static Set<String> createSet(Collection<String> source) {
        Set<String> destination = new HashSet<String>(source) {
            private static final long serialVersionUID = 1L;
    
            public boolean add(String o) {
                if ("".equals(o)) {
                    return false;
                }
                return super.add(o);
            }
        }; 
        return destination;
    }
    

    另一种方法是创建自己的集合类型:

    public class NonEmptyStringSet extends HashSet<String> {
        private static final long serialVersionUID = 1L;
    
        public NonEmptyStringSet() {
            super();
        }
    
        public NonEmptyStringSet(Collection<String> source) {
            super(source);
        }
    
        public boolean add(String o) {
            if ("".equals(o)) {
                return false;
            }
            return super.add(o);
        }
    }
    

    用法:

    createSet(source);
    new NonEmptyStringSet(source);
    

    返回集合的性能更高,因为您不必先创建一个临时集合,然后将所有集合添加到 dest 集合中。

    NonEmptyStringSet 类型的好处是您可以继续添加字符串,并且仍然可以检查空字符串。

    EDIT1:

    删除“if(src.containsAll(dest)) return;”代码在使用 source == dest 调用方法时引入了一个“错误”;结果是源将为空。示例:

    Collection<String> source = new ArrayList<String>();
    source.add("abc");
    copyStringCollectionAndRemoveDuplicates(source, source);
    System.out.println(source);
    

    EDIT2:

    我做了一个小型基准测试,结果表明我的实现比您的初始实现的简化版本快了大约 30%。此基准是您初始实施的最佳情况,因为 dest 集合为空,因此不必清除它。也不要认为我的实现使用 HashSet 而不是 LinkedHashSet,这使我的实现更快一些。

    基准代码:

    public class SimpleBenchmark {
    public static void main(String[] args) {
        Collection<String> source = Arrays.asList("abc", "def", "", "def", "", 
                "jsfldsjdlf", "jlkdsf", "dsfjljka", "sdfa", "abc", "dsljkf", "dsjfl", 
                "js52fldsjdlf", "jladsf", "dsfjdfgljka", "sdf123a", "adfgbc", "dslj452kf", "dsjfafl", 
                "js21ldsjdlf", "jlkdsvbxf", "dsfjljk342a", "sdfdsa", "abxc", "dsljkfsf", "dsjflasd4" );
    
        int runCount = 1000000;
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < runCount; i++) {
            copyStringCollectionAndRemoveDuplicates(source, new ArrayList<String>());
        }
        long time1 = (System.currentTimeMillis() - start1);
        System.out.println("Time 1: " + time1);
    
    
        long start2 = System.currentTimeMillis();
        for (int i = 0; i < runCount; i++) {
            new NonEmptyStringSet(source);
        }
        long time2 = (System.currentTimeMillis() - start2);
        System.out.println("Time 2: " + time2);
    
        long difference = time1 - time2;
        double percentage = (double)time2 / (double) time1;
    
        System.out.println("Difference: " + difference + " percentage: " + percentage);
    }
    
    public static class NonEmptyStringSet extends HashSet<String> {
        private static final long serialVersionUID = 1L;
    
        public NonEmptyStringSet() {
        }
    
        public NonEmptyStringSet(Collection<String> source) {
            super(source);
        }
    
        @Override
        public boolean add(String o) {
            if ("".equals(o)) {
                return false;
            }
            return super.add(o);
        }
    }
    
    public static void copyStringCollectionAndRemoveDuplicates(
            Collection<String> src, Collection<String> dest) {
        Set<String> uniqueSet = new LinkedHashSet<String>(src.size());
        for (String f : src)
            if (!"".equals(f))
                uniqueSet.add(f);
    
        dest.addAll(uniqueSet);
    }
    }
    

    【讨论】:

      【解决方案6】:

      我真的不认为我理解你为什么想要这个方法,但假设它是值得的,我会按如下方式实现它:

      public static void copyStringCollectionAndRemoveDuplicates(
              Collection<String> src, Collection<String> dest) {
          if (src == dest) {
               throw new IllegalArgumentException("src == dest");
          }
          dest.clear();
          if (dest instanceof Set) {
              dest.addAll(src);
              dest.remove("");
          } else if (src instance of Set) {
              for (String s : src) {
                  if (!"".equals(s)) {
                      dest.add(s);
                  }
              }
          } else {
              HashSet<String> tmp = new HashSet<String>(src);
              tmp.remove("");
              dest.addAll(tmp);
          }
      }
      

      注意事项:

      1. 这不会在所有情况下都保留 src 参数中元素的顺序,但方法签名暗示这无关紧要。
      2. 我故意不检查空值。如果提供 null 作为参数,这是一个错误,正确的做法是允许抛出 NullPointerException
      3. 尝试将集合复制到自身也是一个错误。

      【讨论】:

        猜你喜欢
        • 2011-04-27
        • 2011-12-06
        • 2014-08-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多