在 Java 中查看 ArrayList 是不是包含对象的最有效方法

Posted

技术标签:

【中文标题】在 Java 中查看 ArrayList 是不是包含对象的最有效方法【英文标题】:Most efficient way to see if an ArrayList contains an object in Java在 Java 中查看 ArrayList 是否包含对象的最有效方法 【发布时间】:2010-10-08 05:19:56 【问题描述】:

我在 Java 中有一个对象的 ArrayList。这些对象有四个字段,其中两个我会用来认为对象等于另一个。鉴于这两个字段,我正在寻找最有效的方法来查看数组是否包含该对象。

关键是这些类是基于 XSD 对象生成的,所以我不能修改这些类本身来覆盖.equals

有没有比循环遍历并手动比较每个对象的两个字段然后在找到时中断更好的方法?就是这么乱,找个更好的办法。

编辑: ArrayList 来自未编组为对象的 SOAP 响应。

【问题讨论】:

也许 ArrayList.indexOf() 是最清晰有效的方法。 【参考方案1】:

这取决于您需要的效率。简单地遍历列表以查找满足特定条件的元素是 O(n),但如果可以实现 Equals 方法,ArrayList.Contains 也是如此。如果您不是在循环或内部循环中执行此操作,那么这种方法可能就可以了。

如果您确实需要不惜一切代价实现非常高效的查找速度,您需要做两件事:

    解决该类的事实 生成:编写一个适配器类 可以包装生成的类和 实现基于equals() 在这两个领域(假设他们 是公开的)。别忘了还有 实现hashCode() (*) 用该适配器包装每个对象,然后 把它放在一个HashSet中。 HashSet.contains() 有常数 访问时间,即 O(1) 而不是 O(n)。

当然,构建这个 HashSet 仍然需要 O(n) 成本。如果与您需要执行的所有 contains() 检查的总成本相比,构建 HashSet 的成本可以忽略不计,您只会获得任何收益。试图建立一个没有重复的列表就是这样的情况。


* ()实现 hashCode() 最好通过异或(^ 运算符)您用于 equals 实现的相同字段的 hashCode 来完成(但 multiply by 31 以减少 XOR 产生 0 的机会)时间>

【讨论】:

"HashSet.contains() 具有恒定的访问时间,即 O(1)"——你能指出一个证明吗?它不是大量依赖于散列函数吗?如果没有,为什么不直接说“实践快”?否则,我认为您正在传播错误信息(尽管可能是出于好意:)) @Jonas Kölker:来自文档:“假设哈希函数将元素正确地分散在桶中,此类为基本操作(添加、删除、包含和大小)提供了恒定的时间性能。” @Jonas,虽然一个糟糕的 hashCode() 实现会导致访问时间变慢,但任何算法文本(尤其是许多 Collections 数据结构所构建的 CLR(S) 文本 - amazon.com/Introduction-Algorithms-Third-Thomas-Cormen/dp/… ) 会告诉你基于散列的数据结构是 O(1) 的查找。重要的是要意识到 O(1) 并不表示一步查找,而是与数据结构的大小无关的查找。因此,即使 hashCode()s 很差,查找时间也是 O(1)。 Wim 没有散布任何错误信息,事实上他是正确的。【参考方案2】:

您可以使用带有 Java 内置方法的 Comparator 进行排序和二分搜索。假设您有一个这样的类,其中 a 和 b 是您要用于排序的字段:

class Thing  String a, b, c, d; 

您将定义您的比较器:

Comparator<Thing> comparator = new Comparator<Thing>() 
  public int compare(Thing o1, Thing o2) 
    if (o1.a.equals(o2.a)) 
      return o1.b.compareTo(o2.b);
    
    return o1.a.compareTo(o2.a);
  
;

然后对您的列表进行排序:

Collections.sort(list, comparator);

最后进行二分查找:

int i = Collections.binarySearch(list, thingToFind, comparator);

【讨论】:

这是阻力最小的路径。 HashSet 需要的时间很难分析。此解决方案等效于 STL 集 为什么 HashSet 更难分析?你知道渐近运行时间。您可以对其进行概要分析。有什么不那么可分析的? 另一个很好的答案。在构建包装类之前,我倾向于这样做。特别是如果您正在查看非常大的数据集,我怀疑这可能会更有效(它当然是空间方面的)。 这样你比只使用包含慢,O(N) 平均 N/2,因为排序是 O(N logN),你仍然有二进制搜索 O(log N) .如果列表是静态的且重复搜索,则这种方法很好,因此您可以将其排序一次并搜索多次。【参考方案3】:

考虑到您的限制,您会遇到蛮力搜索(或者如果要重复搜索,则创建索引)。您能否详细说明ArrayList 是如何生成的——也许那里有一些回旋余地。

如果您正在寻找更漂亮的代码,请考虑使用 Apache Commons Collections 类,特别是 CollectionUtils.find(),作为现成的语法糖:

ArrayList haystack = // ...
final Object needleField1 = // ...
final Object needleField2 = // ...

Object found = CollectionUtils.find(haystack, new Predicate() 
   public boolean evaluate(Object input) 
      return needleField1.equals(input.field1) && 
             needleField2.equals(input.field2);
   
);

【讨论】:

Guava 的Iterators.find() 非常相似,但支持泛型。【参考方案4】:

如果列表是sorted,您可以使用binary search。如果没有,那就没有更好的办法了。

如果您经常这样做,那么第一次对列表进行排序几乎肯定是值得的。由于您无法修改类,因此您必须使用Comparator 进行排序和搜索。

【讨论】:

这可能不会比手动搜索快,因为听起来好像他的收藏没有排序 可悲的是,它是按我不关心的两个字段之一排序的。我可以使用自定义比较器根据一个字段进行排序,这在二分搜索的情况下会有所帮助,但我觉得在整体速度方面没有多大帮助:| @Parrots:是否可以排序一次,然后进行所有搜索?如果是这样,并且列表中有相当数量的对象(比如 50 个),二分查找肯定会更快。 在一个完全不相关的主题上,我希望 html sanitizer 没有使用贪婪的正则表达式。第一个链接应该是两个不同的链接,但是中间的 和 被吞了。 二分搜索肯定会比正常的线性搜索快得多。这是假设您获得整个列表并且只需对其进行一次排序,否则您将失去使用二分搜索获得的速度优势。有 10,000 个元素的二分搜索 = 14 次比较,没有 = 10000 次比较【参考方案5】:

即使 equals 方法正在比较这两个字段,那么从逻辑上讲,它与您手动执行的代码相同。好吧,它可能是“乱七八糟”,但它仍然是正确的答案

【讨论】:

【参考方案6】:

如果您是我的ForEach DSL 的用户,可以使用Detect 查询来完成。

Foo foo = ...
Detect<Foo> query = Detect.from(list);
for (Detect<Foo> each: query) 
    each.yield = each.element.a == foo.a && each.element.b == foo.b;
return query.result();

【讨论】:

【参考方案7】:

有没有比循环遍历并手动比较每个对象的两个字段然后在找到时中断更好的方法?只是看起来很乱,正在寻找更好的方法。

如果您关心的是可维护性,您可以执行F*** Steeg 建议的操作(这就是我会做的),尽管它可能不是“最有效的”(因为您必须先对数组进行排序,然后再执行二进制搜索)但肯定是最干净和更好的选择。

如果您真的关心效率,您可以创建一个自定义 List 实现,将对象中的字段用作散列并使用 HashMap 作为存储。但这可能太多了。

然后您必须将填充数据的位置从 ArrayList 更改为 YourCustomList。

喜欢:

 List list = new ArrayList();

 fillFromSoap( list );

收件人:

 List list = new MyCustomSpecialList();

 fillFromSoap( list );

实现将类似于以下内容:

class MyCustomSpecialList extends AbstractList   
    private Map<Integer, YourObject> internalMap;

    public boolean add( YourObject o )  
         internalMap.put( o.getThatFieldYouKnow(), o );
    

    public boolean contains( YourObject o )  
        return internalMap.containsKey( o.getThatFieldYouKnow() );
    

很像 HashSet,这里的问题是 HashSet 依赖于 hashCode 方法的良好实现,而这可能你没有。相反,您将“您知道的那个字段”用作散列,它使一个对象等于另一个对象。

当然,从头开始实现 List 比我上面的 sn-p 更棘手,这就是为什么我说 F*** Steeg 建议会更好、更容易实现(尽管这样会更有效)

告诉我们你最后做了什么。

【讨论】:

【参考方案8】:

也许列表不是你需要的。

也许TreeSet 会是一个更好的容器。您可以获得 O(log N) 的插入和检索,以及有序的迭代(但不允许重复)。

LinkedHashMap 可能更适合您的用例,也请检查一下。

【讨论】:

【参考方案9】:

从性能的角度来看,基于字段值作为键构建这些对象的 HashMap 可能是值得的,例如一次填充地图并非常有效地查找对象

【讨论】:

仅在多次搜索时。【参考方案10】:

如果您需要在同一个列表中搜索多次,建立索引可能会有所回报。

迭代一次,并构建一个 HashMap,其中您要查找的等值作为键,适当的节点作为值。如果您需要所有而不是任何一个给定的等值,则让地图具有列表的值类型并在初始迭代中构建整个列表。

请注意,您应该在执行此操作之前进行测量,因为构建索引的开销可能会掩盖仅遍历直到找到预期节点的过程。

【讨论】:

【参考方案11】:

共有三个基本选项:

1) 如果检索性能是最重要的并且这样做是可行的,请使用一次构建的哈希表形式(并随着/如果列表发生变化而改变)。

2) 如果 List 排序方便或排序可行且 O(log n) 检索足够,则排序和搜索。

3) 如果 O(n) 检索速度足够快,或者如果操作/维护数据结构或替代方案不切实际,则迭代列表。

在编写比对 List 进行简单迭代更复杂的代码之前,值得思考一些问题。

为什么需要不同的东西? (时间)表现?优雅?可维护性?重用?所有这些都是可以的理由,分开或一起,但它们会影响解决方案。

您对相关数据结构有多少控制权?你能影响它的建造方式吗?稍后管理?

数据结构(和底层对象)的生命周期是什么?它是一下子建立起来的,从未改变过,还是高度动态的?你的代码可以监控(甚至改变)它的生命周期吗?

是否还有其他重要限制,例如内存占用?关于重复的信息重要吗?等等。

【讨论】:

【参考方案12】:

我想说最简单的解决方案是包装对象并将 contains 调用委托给包装类的集合。这类似于比较器,但不会强制您对结果集合进行排序,您可以简单地使用 ArrayList.contains()。

public class Widget 
        private String name;
        private String desc;

        public String getName() 
            return name;
        

        public void setName(String name) 
            this.name = name;
        

        public String getDesc() 
            return desc;
        

        public void setDesc(String desc) 
            this.desc = desc;
        
    



    public abstract class EqualsHashcodeEnforcer<T> 

        protected T wrapped;

        public T getWrappedObject() 
            return wrapped;
        

        @Override
        public boolean equals(Object obj) 
            return equalsDelegate(obj);
        

        @Override
        public int hashCode() 
            return hashCodeDelegate();
        

        protected abstract boolean equalsDelegate(Object obj);

        protected abstract int hashCodeDelegate();
    


    public class WrappedWidget extends EqualsHashcodeEnforcer<Widget> 

        @Override
        protected boolean equalsDelegate(Object obj) 
            if (obj == null) 
                return false;
            
            if (obj == getWrappedObject()) 
                return true;
            
            if (obj.getClass() != getWrappedObject().getClass()) 
                return false;
            
            Widget rhs = (Widget) obj;

            return new EqualsBuilder().append(getWrappedObject().getName(),
                    rhs.getName()).append(getWrappedObject().getDesc(),
                    rhs.getDesc()).isEquals();
        

        @Override
        protected int hashCodeDelegate() 

            return new HashCodeBuilder(121, 991).append(
                    getWrappedObject().getName()).append(
                    getWrappedObject().getDesc()).toHashCode();
        

    

【讨论】:

以上是关于在 Java 中查看 ArrayList 是不是包含对象的最有效方法的主要内容,如果未能解决你的问题,请参考以下文章

Java使用ArrayList实现“群主发红包”功能

何时在 Java 中使用 LinkedList 而不是 ArrayList?

Java基本类型数组转ArrayList

检查Java中的ArrayList中是不是存在Object

关于java import的问题

在c#中用arraylist创建的数组,在用contains查看是不是包含某一个对象时,判