【问题标题】:Efficiency of the way comparator works比较器工作方式的效率
【发布时间】:2023-03-09 07:19:01
【问题描述】:

我正在尝试使用比较器来帮助对对象列表进行排序。我对比较器的工作原理以及在以下示例中的具体作用有疑问:

private static Comparator<Student> comparator()
{
        return (Student a, Student b) ->
        {  
                return Integer.compare(complexOperation(a), complexOperation(b));
        }
}

正如您在上面看到的,需要根据complexOperation() 方法返回的整数排名对学生进行比较和排序。顾名思义,这是一项繁重的操作。上述方法会是最有效的吗?或者最好基本上遍历我要排序的列表中的每个学生,对每个学生执行 complexOperation() 并将结果存储在 Student 对象的字段中。然后比较器会做一个:

Integer.compare(a.getRank(), b.getRank())

这两种方法是否具有可比性,或者,由于比较器的工作方式(可能将同一个对象与其他对象多次比较,因此在比较期间每个学生运行 complexOperation() 多次),这样做会更快吗? complexOperation() 的预计算结果在学生字段中?

上面会这样调用:

Collections.sort(students, comparator());

希望这很清楚!

编辑: 可以说,为了它,不可能向 Student 对象添加字段(对于更复杂的情况,这是一个玩具问题,我不能随意修改 Student 对象)。也许创建一个自定义对象,让 Student 坐在里面并添加另一个字段,而不是在比较器中执行 complexOperation() 会更好吗?还是有另一种解决问题的方法?我可以考虑创建一个 Hashmap,将学生 id 作为键,将 complexOperation() 的结果作为值,然后在比较器中创建/访问该记录?

【问题讨论】:

  • @HovercraftFullOfEels 我专门使用比较器作为排序机制,并希望它尽可能高效。
  • (perhaps compares the same object more than once with others hence running complexOperation() multiple times per Student during the compare - 将 System.out.println(...) 语句添加到比较器以查看它被调用的频率。或者添加一个可以在比较器完成后显示的计数器。如果调用次数大于被排序的元素,那么如果多次调用,您就会知道复杂的操作。显示一些输出的基本问题解决技术。
  • 然后您询问的是 JVM 的优化工作原理,如果它确定这会使事情更高效地运行,它通常会为您做这种事情。

标签: java performance sorting comparator


【解决方案1】:

基本上,您希望通过比较每个学生映射到的一些值来比较学生。这通常由

    static Comparator<Student> comparator()
    {
        return Comparator.comparing( Foo::complexOperation );
    }

但是,由于函数complexOperation 过于昂贵,我们想要缓存它的结果。我们可以有一个通用的实用方法Function cache(Function)

    static Comparator<Student> comparator()
    {
        return Comparator.comparing( cache(Foo::complexOperation) );
    }

一般来说,调用者最好能提供一个Map作为缓存

public static <K,V> Function<K,V> cache(Function<K,V> f, Map<K,V> cache)
{
    return k->cache.computeIfAbsent(k, f);
}

我们可以使用IdentityHashMap作为默认缓存

public static <K,V> Function<K,V> cache(Function<K,V> f)
{
    return cache(f, new IdentityHashMap<>());
}

【讨论】:

  • 我想知道为什么 Java 的 Comparator.comparing 默认不这样做(或者至少是可选的)。在 Python 中,sortedkey 函数会执行此操作。
【解决方案2】:

平均而言,您的排序算法将为 N 个学生的数组调用大约 log2N 次 complexOperation() 方法。如果操作真的很慢,最好为每个学生运行一次。这可以为 1000 名学生带来一个数量级的改进。

但是,您不必明确地这样做:您可以让complexOperation(...) 存储每个学生的结果,然后在后续请求中返回缓存值:

private Map<Student,Integer> cache = new HashMap<Student,Integer>();

private int complexOperation(Student s) {
    // See if we computed the rank of the student before
    Integer res = cache.get(s);
    if (res != null) {
        // We did! Just return the stored result:
        return res.intValue();
    }
    ... // do the real computation here
    // Save the result for future invocations
    cache.put(s, result);
    return result;
}

请注意,为了使这种方法起作用,Student 类需要实现 hashCodeequals

【讨论】:

  • @bayou.io 需要明确地或通过丢弃拥有缓存的对象来清除缓存。
  • comparator() 方法的用户可能不想被这个细节困扰:)
  • @JohnBaum 是的,最好用一个额外的int 字段来做一个“持有人”,特别是对于大量学生,调用次数会增加十倍或更多。与潜在的 CPU 节省相比,对象开销的成本很小。
  • @JohnBaum 这几乎是我上面建议的,除了你的方法使用学生 ID 作为密钥,而我直接使用 Student,而不提取其 ID(在引擎盖下 equal 和 @987654332 @ 可以很好地依赖于 ID)。除此之外,这两种方法是相同的。
  • 仅供参考这都是理论上的,是的,缓存会提高性能,但代价是代码复杂化,所以除非你真的性能问题,不要打扰。这是一句古老的格言,即在遇到问题之前不要调整代码,因为您最终可能会在错误的问题上浪费时间,因此您没有任何时间来实际找到并解决真正的问题,即使您有一。见Premature optimization
猜你喜欢
  • 1970-01-01
  • 2012-03-29
  • 1970-01-01
  • 1970-01-01
  • 2013-04-03
  • 2023-03-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多