71

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

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

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

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

4

12 回答 12

103

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

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

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

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


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

于 2009-02-17T22:39:59.747 回答
38

您可以使用带有 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);
于 2009-02-17T22:58:03.963 回答
11

鉴于您的限制,您将陷入蛮力搜索(或者如果搜索将被重复,则创建索引)。您能否详细说明如何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);
   }
});
于 2009-02-17T22:29:47.993 回答
6

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

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

于 2009-02-17T22:22:08.900 回答
4

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

于 2009-02-17T22:22:23.783 回答
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();
于 2009-03-01T19:19:48.243 回答
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的建议会更好、更容易实现(尽管这样的事情会更有效)

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

于 2009-02-18T00:12:18.980 回答
2

也许列表不是您需要的。

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

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

于 2009-02-18T02:01:17.643 回答
1

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

于 2009-02-17T22:30:19.740 回答
1

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

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

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

于 2009-02-17T22:35:44.440 回答
1

有三个基本选项:

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

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

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

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

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

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

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

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

于 2009-02-17T23:18:17.003 回答
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();
        }

    }
于 2009-02-18T00:49:02.463 回答