【问题标题】:Collection#removeIf different behavior between JDKsCollection#removeIf JDK 之间的不同行为
【发布时间】:2021-07-17 10:25:28
【问题描述】:

Tomcat 9.0.39 在 JDK 11.0.10 上运行。

或在 JDK 1.8.0_271 上运行 Tomcat 8.5.65。

以下代码使用 JDK 1.8.0_271 编译,运行良好。

final Set<A> a = _a.stream().collect(Collectors.toSet());

a.removeIf(b -> 
    Objects.nonNull(b.getOne()) 
    && Objects.nonNull(b.getTwo()) 
    && Objects.nonNull(b.getThree())
    && Objects.nonNull(b.getFour())
);

但是,removeIf 部分未使用以下配置执行(下一个操作已在 _a 列表的所有元素上执行):

  • Tomcat 8.5.59 在 JDK 1.8.0_202-b08 上运行。
  • Tomcat 8.5.34 在 JDK 1.8.0_191 上运行。

为了在任何地方达到预期的结果,removeIf 已被替换为过滤器:

final Set<A> a = _a.stream()
    .filter(b -> 
        Objects.isNull(b.getOne()) 
        && Objects.isNull(b.getTwo()) 
        && Objects.isNull(b.getThree())
        && Objects.isNull(b.getFour())
    )       
    .collect(Collectors.toSet());

或与供应商合作:

final Set<A> a = _a.stream()
    .collect(Collectors.toCollection(TreeSet::new));

a.removeIf(b -> 
    Objects.nonNull(b.getOne()) 
    && Objects.nonNull(b.getTwo()) 
    && Objects.nonNull(b.getThree())
    && Objects.nonNull(b.getFour())
);

我读过Collectors#toSetCollectors#toList

返回一个收集器,它将输入元素累积到一个新的集合/列表中。不保证返回的 Set / List 的类型、可变性、可序列化性或线程安全性;如果需要对返回的 Set / List 进行更多控制,请使用 toCollection(Supplier)。

但是这里没有并行流,流是通过终端操作在最终集合中收集的,Collection#removeIf 是在 JDK 1.8 中引入的。

所以我的问题是:

为什么它在不同的 JDK 次要版本上表现不同?

(或者至少它在 JDK


问/答

如果替换 _a.stream().collect(Collectors.toSet()); 会发生什么使用新的 HashSet(_a)?

同样的事情,在工作配置上工作,而不是在另一个工作上,打印不应该留在集合中的元素。

final Set<A> a = new HashSet<>(_a);

这种行为是“随机的”吗(如标题所示),这意味着这种情况有时会发生,但并非总是如此;或者只是出乎意料,这意味着这总是在特定的 JDK 版本上发生,但您不确定为什么?

这总是在特定的 JDK 版本上发生,但我不知道为什么。在 1.8.0_271 之前,removeIf 永远不会被执行(多次尝试确定),在此版本之后或使用此版本时,它总是过滤元素(也尝试多次)。

A 是否正确实现了 equals 和 hashCode?​​strong>

A 类正在像这样实现java.lang.Comparable&lt;A&gt;

/*
 * (non-Javadoc)
 * @see java.lang.Object#equals(java.lang.Object)
 */
@Override
public boolean equals(Object o) {
    boolean result = false;
    if (o instanceof A) {
        A a = (A o;
        result = 
            getId1() == a.getId1()
            && getId2() == a.getId2()
            && getId3() == a.getId3();
    }
    return result;
}
/*
 * (non-Javadoc)
 * @see java.lang.Object#hashCode()
 */
@Override
public int hashCode() {
    StringBuilder sb = new StringBuilder();
    sb.append(getId1());
    sb.append(getId2());
    sb.append(getId3());
    return sb.hashCode();
}
/* 
 * (non-Javadoc)
 * @see java.lang.Comparable#compareTo(java.lang.Object)
 */
@Override
public int compareTo(A a) {
    return 
        getId1() == a.getId1() 
        && getId2() == a.getId2() 
        && getId3() == a.getId3() 
        ? 1 : 0;
}

如何判断代码没有被执行?

我使用的第一种方法是生产数据库中的表,它更新了所有元素,而它应该只更新缩减集的元素。这向我显示了错误。 然后,我曾经确定的第二种方法是像这样的简单printf,在注释更新表的数据库方法之后。

a.stream()
    .filter(b -> 
        Objects.nonNull(b.getOne()) 
        || Objects.nonNull(b.getTwo()) 
        || Objects.nonNull(b.getThree()) 
        || Objects.nonNull(b.getFour())
    )
    .forEach(a -> 
        System.out.printf(
            "%nbad elements remaining in the set : %s%n", 
            a.getId1()
        )
    );

并且打印的结果在它不起作用的配置(生产)上是不同的:有时更新了 300 个项目,有时更新了 400 个。 在工作配置(开发)上,没有打印任何内容。这指出了它在 JDK 上的工作方式不同。

只需使用 Objects.hash(getId1(), getId2(), getId3()); 没有理由使用昂贵的字符串连接。但是您的 compareTo 方法也被破坏了。比较方法必须是对称的(你的不是),传递的(你的不是),为所有对象创建一个总顺序(你的甚至没有尝试)。你确定这个中间步骤是否做了它应该做的事情的方式也是不够的。在应用过滤器之前,您甚至没有验证该集合是否具有预期的内容。

我已经替换了java.lang.Comparable&lt;A&gt;#compareTojava.lang.Object#hashCode 的以下实现。

hashCode

/*
 * (non-Javadoc)
 * @see java.lang.Object#hashCode()
 */
/*@Override
public int hashCode() {
    StringBuilder sb = new StringBuilder();
    sb.append(getId1());
    sb.append(getId2());
    sb.append(getId3());
    return sb.hashCode();
}*/

/*
 * (non-Javadoc)
 * @see java.lang.Object#hashCode()
 */
@Override
public int hashCode() {
    return Objects.hash(getId1(), getId2(), getId3());
}

compareTo

/* 
 * (non-Javadoc)
 * @see java.lang.Comparable#compareTo(java.lang.Object)
 */
/*@Override
public int compareTo(A a) {
    return 
        getId1() == a.getId1() 
        && getId2() == a.getId2() 
        && getId3() == a.getId3() 
        ? 1 : 0;
}*/

/* 
 * (non-Javadoc)
 * @see java.lang.Comparable#compareTo(java.lang.Object)
 */
@Override
public int compareTo(A a) {
    int result;
    if (a.getId2() != getId2()) {
        throw new ClassCastException(
            String.format(
                "a A with id2 euqals to %s cannot be compared with "
                + "another A with id2 equals to %s", 
                getId2(), 
                a.getId2()
            )
        );
    }
    if (
        getId1() == a.getId1() 
        && getId2() == a.getId2()
        && getId3() == a.getId3()
    ) {
        result = 0;
    }
    else if (getId1() < a.getId1()) {
        result = 1;
    }
    else {
        result = -1;
    }
    return result;
}

我可以确认它现在在每个 JDK(1.8.0_191、1.8.0_202、1.8.0_271、11.0.10)和每个版本的 Tomcat(8.5.34、8.5.59、8.5.65、 9.0.39)。

虽然这并不能解释为什么它在 JDK 1.8.0_271 及更高版本上工作(如果我可以这么说的话),但这种奇怪行为的答案是不好的java.lang.Object#hashCodejava.lang.Comparable#compareTo 方法的实现。

【问题讨论】:

  • A 是否正确实现了equalshashCode?如果不这样做阻止HashSet 能够迭代其所有元素(或删除元素),我会感到惊讶;但这是HashSetTreeSet 之间最明显的区别。
  • 另外:这种行为是“随机的”(如标题所示),这意味着这种情况有时会发生,但并非总是如此;或者只是出乎意料,这意味着这种情况总是发生在特定的 JDK 版本上,但您只是不确定为什么?
  • Collectors.toSet() 不保证返回的集合是可变的。但是由于在这些实现中它总是HashSet,所以实际的问题是:“你如何确定代码没有被执行?”。而您的filter 谓词不是removeIf 谓词的否定。
  • 貌似A::hashCode依赖于StringBuilder::hashCode,其实就是继承自Object::hashCode的身份哈希码。也就是说,SB 的 hashCode 是基于对象身份的,而不是基于其内容的。由于 A::hashCode 每次调用时都会分配一个新的 SB,因此每次它可能会返回不同的值。身份哈希码可能会因运行和 JDK 版本而异,这可能会导致 HashSet 出现一堆奇怪的行为。
  • 只需使用Objects.hash(getId1(), getId2(), getId3());没有理由使用昂贵的字符串连接。但是你的 compareTo 方法也坏了。比较方法必须是对称的(你的不是),传递的(你的不是),为所有对象创建一个总顺序(你的甚至没有尝试)。你确定这个中间步骤是否做了它应该做的事情的方式也是不够的。在应用文件管理器之前,您甚至没有验证集合是否具有预期的内容。

标签: java collections java-8


【解决方案1】:

您的过滤器和 removeIf 谓词在同一组条件下不匹配。

a.removeIf(b -> 
    Objects.nonNull(b.getOne()) 
    && Objects.nonNull(b.getTwo()) 
    && Objects.nonNull(b.getThree())
    && Objects.nonNull(b.getFour())
);

当您从 removeIf(nonNull &amp;&amp; nonNull) 更改为您需要 filter(isNull || isNull) 以匹配相同的值时:

stream.filter(b -> 
    Objects.isNull(b.getOne()) 
    || Objects.isNull(b.getTwo()) 
    || Objects.isNull(b.getThree())
    || Objects.isNull(b.getFour())
)

您也可以将谓词分配给一个变量,过滤器中使用的是Predicate.not

Predicate<A> predicate = ...
a.removeIf(predicate) <=> stream.filter(Predicate.not(predicate));

【讨论】:

  • 感谢您指出这一点,虽然这实际上是正确的,但这并不能解释为什么行为因一个 JDK 不同而有所不同。
  • 是的,这是一个小问题。您必须注意@Holger 和@StuartMarks cmets。首先,hashCode() 实现只不过是一个随机数生成器,这意味着您破坏了所有 Set 操作:您可以为同一个对象多次 set.add(因为哈希码与前一个对象不匹配),set.remove(obj) 不会从集。然后将传递问题修复为compareTo(a,b) != -compareTo(b,a)
猜你喜欢
  • 2020-09-05
  • 2015-07-04
  • 2015-11-12
  • 2023-03-20
  • 2022-08-19
  • 2020-11-07
  • 2014-11-01
  • 1970-01-01
  • 2018-05-07
相关资源
最近更新 更多