拥有可以动态更改订单的Java Comparator是否可以?

时间:2009-05-26 20:05:29

标签: java collections comparator

我有一组带时间戳的值,我想将它放在一个有序的集合中。

public class TimedValue {
    public Date time;
    public double value;

    public TimedValue(Date time, double value) {
        this.time = time;
        this.value = value;
    }
}

对此集合进行排序的业务逻辑表示,必须按降序排序值,除非它比最新值早7天以上。

所以作为测试,我想出了以下代码......

DateFormat dateFormatter = new SimpleDateFormat("MM/dd/yyyy");
TreeSet<TimedValue> mySet = new TreeSet<TimedValue>(new DateAwareComparator());
mySet.add(new TimedValue(dateFormatter.parse("01/01/2009"), 4.0 )); // too old
mySet.add(new TimedValue(dateFormatter.parse("01/03/2009"), 3.0)); // Most relevant
mySet.add(new TimedValue(dateFormatter.parse("01/09/2009"), 2.0));

正如您所看到的,最初第一个值比第二个值更相关,但是一旦将最终值添加到集合中,第一个值已经过期且应该是最不相关的。

我的初步测试表明这应该有效...... TreeSet会在添加更多值时动态重新排序整个列表。

但即使我看到它,我也不确定我是否相信。

在添加每个元素时,排序的集合会重新排序整个集合吗?有没有以这种方式使用排序集合(即性能)的问题?添加完所有值后手动对列表进行排序会不会更好(我猜它会是这样)?



随访: 尽可能多的(甚至在某种程度上)怀疑,排序的集合不支持这种“动态重新排序”的方式。我相信我的初步测试是“偶然”工作。随着我在集合中添加更多元素,“订单”迅速崩溃。感谢所有出色的回复,我重构了我的代码,使用了许多人建议的方法。

8 个答案:

答案 0 :(得分:10)

我没有看到你的比较器甚至可以检测到这种变化,除非它记得它当前看到的最新值 - 这听起来像是一种必然以泪水结束的方法。

我建议你按照以下几点做点什么:

  • 以无序集(或列表)
  • 收集数据
  • 找到最新值
  • 根据该值创建一个比较器,这样所有使用该比较器的比较都将得到修复(即它永远不会根据相同的输入值返回不同的结果;比较器本身是不可变的,尽管它取决于最初在构造函数中提供的值)
  • 使用该比较器创建一个已排序的集合(无论以哪种方式看起来最好,取决于您想要用它做什么)

答案 1 :(得分:4)

我会建议不要这样做有几个原因:

  1. 由于它基本上是幕后的红黑树(在每次插入时都不一定要从头开始重建),因此您可能很容易在树的错误部分找到值(使大多数树无效) TreeSet API)。
  2. 行为未在规范中定义,因此即使现在正在运行,也可能会稍后更改。
  3. 将来,当任何远程触及此代码的任何事情出现奇怪错误时,您都会花时间怀疑这是原因。
  4. 我建议在搜索之前重新创建/使用TreeSet,或者(我的偏好)在搜索之前迭代整个集合并删除任何太旧的对象。你甚至可以,如果你想换一些内存来提高速度,保留按日期排序的第二个列表并由相同的对象支持,这样你就可以根据时间从树集中删除对象了。排序列表。

答案 2 :(得分:3)

我不相信编写JDK库甚至第三方库来处理结果不一致的比较器。我不会依赖这项工作。如果你的比较器在调用一次时可以返回不等于两个值,并且如果稍后调用则可以返回相同的两个值,我会更担心。

仔细阅读Comparator.compare()的合同。您的比较器是否满足这些约束条件?

详细说明,如果你的比较器在你调用它时返回两个值不相等,但后来又返回两个值相等,因为后来的值被添加到集合中并且更改了比较器的输出, “Set”(没有重复)的定义被撤消。

Jon Skeet的advice在他的回答中提供了很好的建议,并且无需担心这些问题。确实,如果您的比较者没有返回与equals()一致的值,那么您可能会遇到大问题。每次添加某些内容时,排序集是否会重新排序,我不会依赖,但 order 更改时最糟糕的事情是您的设置不会保持排序。

答案 3 :(得分:2)

我99%肯定这不起作用。如果Set中的值突然改变其比较行为,则很可能(很可能,实际上)它将不再被发现;即set.contains(value)将返回false,因为搜索算法将在某一点进行比较并继续使用错误的子树,因为该比较现在返回的结果与插入值时的结果不同。

答案 4 :(得分:2)

不,这不起作用。

如果您在集合中使用可比较的键,则两个键之间的比较结果必须保持不变。

在二叉树中存储密钥时,路径中的每个分支都被选为比较操作的结果。如果稍后的比较返回不同的结果,则将采用不同的分叉,并且将找不到先前存储的密钥。

答案 5 :(得分:1)

我认为比较器的不变性本质应该是按类别进行的,所以只要你在给定的排序操作的持续时间内保持一致,你就可以了(只要没有物品越过7天的边界中间排序。

但是,您可能希望更明显地询问有关TreeSet的具体内容,我想这会重新使用以前各种类型的信息来节省添加新项目的时间,因此这有点特殊情况。 TreeSet javadocs特别遵循Comparator语义,因此您可能没有得到官方支持,但您必须阅读代码以了解您是否安全。

我认为当你需要对数据进行排序时,你最好做一个完整的排序,使用一个时间作为“现在”,这样你就不会冒险跳过这个边界,如果你的排序需要足够长的时间才能使它成为可能

答案 6 :(得分:1)

记录可能会从排序中间的<7天变为> 7天,因此您所做的事情违反了比较器的规则。当然,这并不意味着它不起作用:如果你确切地知道内部发生了什么,很多被记录为“不可预测”的东西实际上都有效。

我认为教科书的答案是:这对于内置的排序是不可靠的。你必须编写自己的排序函数。

至少,我会说当日期越过边界时,你不能依赖TreeSet或任何“排序结构”神奇地求助自己。如果您在显示之前重新排序,并且不依赖于更新之间保持正确的任何内容,那么这最好可能有效。

最糟糕的是,不一致的比较可能会严重打破这种情况。你不能保证这不会让你陷入无限循环或其他致命的黑洞。

所以我要说:从Sun计划使用的任何类或函数中读取源代码,看看你能否弄清楚会发生什么。测试很好,但有些棘手的案例难以测试。最明显的是:如果在排序过程中,记录滚过日期边界会怎么样?也就是说,它可能会查看一次记录并说它<7但是下次它看到它时它是&gt; 7。这可能是坏消息,坏消息。

我遇到一个明显的伎俩:将日期转换为将记录添加到结构时的年龄,而不是动态。这样它就不能在排序中改变。如果结构将存活超过几分钟,请在适当的时间重新计算年龄,然后重新排序。我怀疑有人会说你的程序是不正确的,因为你说记录不到7天,真的是7天,0小时,0分钟和2秒。即使有人注意到,他们的手表有多准确?

答案 7 :(得分:1)

如前所述,比较者无法为您执行此操作,因为违反了传递性。基本上,为了能够对项目进行排序,您必须能够比较它们中的每一项(独立于其余项目),这显然是您无法做到的。所以你的场景基本上要么不起作用,要么会产生不一致的结果。

也许更简单的东西对你来说足够好了:

  • 应用根据需要使用值的简单比较器
  • 并简单地从列表/集合中删除比最新版本早7天的所有元素。基本上每当添加一个新项目时,你会检查它是否是最新项目,如果是,则删除那些比它早7天的项目。

如果您还从列表中删除项目,这将无法工作,在这种情况下,您需要将所有删除的项目保留在单独的列表中(按照您按日期排序的方式)并将这些项目添加回删除后MAX(日期)较小的原始列表。