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


71

我在Java中有一个ArrayList对象。这些对象有四个字段,我将其中两个用于考虑该对象与另一个字段相等。给定这两个字段,我正在寻找最有效的方法,以查看数组是否包含该对象。

棘手的是,这些类是基于XSD对象生成的,因此我无法修改这些类本身以覆盖.equals

有没有比遍历并手动比较每个对象的两个字段然后在找到时中断的更好的方法了?寻找更好的方法,这看起来太混乱了。

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


也许ArrayList.indexOf()是最清晰,最有效的方法。
Siamak SiaSoft,

Answers:


102

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

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

  1. 解决生成类的事实:编写一个适配器类,该类可以包装所生成的类,并基于这两个字段(假设它们是公共的)实现equals()。别忘了还要实现hashCode()(*)
  2. 用该适配器包装每个对象,并将其放入HashSet中。 HashSet.contains()具有恒定的访问时间,即O(1)而不是O(n)。

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


*()实现hashCode()最好通过XOR'ing(^运算符)来实现与equals实现相同的字段的hashCode(但要乘以31以减少XOR产生0的机会)


1
“ HashSet.contains()具有恒定的访问时间,即O(1)”-您能否指出一个证明?难道不是依赖严重的哈希函数?如果不是,为什么不说“快速实践”呢?否则,我认为您传播的是错误的信息(虽然可能是
出于

4
@JonasKölker:从文档中:“该类为基本操作(添加,删除,包含和大小)提供恒定的时间性能,假设哈希函数将元素正确地分散在存储桶中。”
Wim Coenen

12
@Jonas,虽然糟糕的hashCode()实现会导致访问时间变慢,但是任何算法文本(尤其是许多Collections数据结构均建立在CLR(S)文本上)-amazon.com/Introduction-Algorithms-Third-Thomas -Cormen / dp /…)将告诉您基于哈希的数据结构为O(1)进行查找。重要的是要意识到O(1)并不表示单步查找,而是与数据结构的大小无关的查找。因此,即使使用较差的hashCode(),查找时间也为O(1)。Wim并没有散布任何错误信息,事实上他很在意。
dimo414'5

37

您可以将Comparator与Java的内置方法结合使用以进行排序和二进制搜索。假设您有一个类似的类,其中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);

1
这是阻力最小的路径。HashSet花费的时间很难分析。此解决方案等效于STL集
溢出

为什么HashSet很难分析?您知道渐近的运行时间。您可以对其进行配置。关于它的分析较少?
Wim Coenen

另一个很好的答案。在构造包装器类之前,我倾向于这样做。特别是如果您正在查看非常大的数据集,我怀疑这可能会更有效(它肯定是空间明智的)。
dimo414'5

这样,您的速度比仅使用包含O(N)且平均值为N / 2的O(N)慢,因为排序为O(N logN),并且您仍然具有二分查找O(log N)。如果列表是静态的且重复搜索,则此方法很好,因此您可以对其进行排序并多次搜索。
RobMcZag

10

考虑到您的限制,您将无法进行强力搜索(如果重复搜索,则将创建索引)。您能否详细说明如何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);
   }
});

2
番石榴的Iterators.find()非常相似,但支持泛型。
Ed Staub

6

如果列表已排序,则可以使用二进制搜索。如果没有,那就没有更好的办法了。

如果您经常执行此操作,则几乎可以肯定,第一次进行列表排序是值得的。由于您无法修改类,因此必须使用aComparator进行排序和搜索。


这似乎比手动搜索要快,因为听起来好像不是他的收藏集排序那样
oxbow_lakes

可悲的是,它按我不在乎的两个字段之一进行排序。我可以使用自定义比较器根据一个字段进行排序,这在二进制搜索的情况下会有所帮助,但我觉得对整体速度没有太大帮助:|
鹦鹉

@Parrots:是否可以将其排序一次,然后进行所有搜索?如果是这样,并且如果列表中有相当数量的对象(例如50个),则二进制搜索肯定会更快。
迈克尔·迈尔斯

对于一个完全无关的主题,我希望HTML清理器不要使用贪婪的正则表达式。该第一个链接应该是两个不同的链接,但是中间的</a>和<a>被吞噬了。
迈克尔·迈尔斯

二进制搜索肯定会比普通线性搜索快得多。这是假设您获得了整个列表,而只需要对其排序一次,否则您将失去通过使用二进制搜索获得的速度优势。具有10,000个元素的二进制搜索= 14个比较,没有= 10000个比较
MahlerFive

4

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


4

如果您是我的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();

2

有没有比遍历并手动比较每个对象的两个字段然后在找到时中断的更好的方法了?寻找更好的方法,这看起来太混乱了。

如果您关心的是可维护性,则可以执行Fabian 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的难度比上面的代码段要难得多,这就是为什么我要说Fabian Steeg建议会更好,更容易实现(尽管这样会更有效)

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


2

也许列表不是您所需要的。

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

对于您的用例,LinkedHashMap可能更好,请检查一下。


1

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


只有多次搜索。
cletus

1

如果您需要在同一列表中搜索很多时间,则可能需要建立一个索引。

迭代一次,并使用您要查找的equals值作为键并使用适当的节点作为值来构建HashMap。如果您需要全部而不是给定的equals值中的任何一个,那么让映射具有列表的值类型,并在初始迭代中构建整个列表。

请注意,在执行此操作之前,您应该先进行测量,因为建立索引的开销可能会遍历直到找到所需的节点为止。


1

有三个基本选项:

1)如果检索性能是最重要的并且实际可行,则使用一次构建的哈希表形式(并在// List更改时更改)。

2)如果列表方便地排序或对其进行排序并且O(log n)检索就足够了,则进行排序和搜索。

3)如果O(n)检索足够快,或者操作/维护数据结构或备用结构不切实际,请遍历List。

在编写比列表上的简单迭代复杂的代码之前,值得考虑一些问题。

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

  • 您对所讨论的数据结构有多少控制权?您能影响它的建造方式吗?以后管理?

  • 数据结构(和基础对象)的生命周期是什么?它是一次建立起来却永不改变,还是高度动态化?您的代码可以监视(甚至更改)其生命周期吗?

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


0

我会说最简单的解决方案是包装对象并将包含调用委托给包装的类的集合。这类似于比较器,但不强制您对结果集合进行排序,您只需使用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();
        }

    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.