You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@commons.apache.org by "Gilles Sadowski (Jira)" <ji...@apache.org> on 2026/03/06 09:42:00 UTC
[jira] [Updated] (MATH-1686) `Dfp.equals()` returns `false` for `NaN.equals(NaN)`, violating `Object.equals()` reflexivity
[ https://issues.apache.org/jira/browse/MATH-1686?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]
Gilles Sadowski updated MATH-1686:
----------------------------------
Description:
# Summary
`Dfp.equals()` returns `false` when both operands are NaN, violating the reflexivity requirement
of `Object.equals()` (JDK 21 Javadoc):
{noformat}
"The `equals` method implements an equivalence relation on non-null object references: It is
*reflexive*: for any non-null reference value `x`, `x.equals(x)` should return `true`."
{noformat}
# Root cause
In [`Dfp.java` lines 895-907|https://github.com/apache/commons-math/blob/master/commons-math-legacy-core/src/main/java/org/apache/commons/math4/legacy/core/dfp/Dfp.java#L895-L907]:
{code}
@Override
public boolean equals(final Object other) {
if (other instanceof Dfp) {
finalDfpx = (Dfp) other;
if (isNaN() || x.isNaN() || field.getRadixDigits() != x.field.getRadixDigits()) {
returnfalse; // <-- NaN.equals(NaN) returns false
}
returncompare(this, x) == 0;
}
returnfalse;
}
{code}
# Reproducer
{code}
Dfp nan = new DfpField(20).newDfp().newInstance((byte) 1, Dfp.QNAN);
System.out.println(nan.equals(nan)); // false (should be true)
{code}
# Comparison with JDK
The JDK's `Double.equals()` and `Float.equals()` deliberately return `true` for NaN:
{noformat}
`Double.equals()`: "If `d1` represents `+0.0` while `d2` represents `-0.0`, or vice versa,
the `equal` test has the value `false`, even though `+0.0==-0.0` has the value `true`. [...]
If `d1` and `d2` both represent `Double.NaN`, then the `equals` method returns `true`, even
though `Double.NaN==Double.NaN` has the value `false`."
{noformat}
The JDK explicitly chose to break IEEE 754 NaN semantics in `equals()` to preserve the
`Object.equals()` contract. `Dfp` should follow the same convention.
# Impact
Any `Dfp` NaN value placed in a `HashSet`, `HashMap`, or compared with `equals()` will behave
incorrectly:
** `set.add(nan); set.contains(nan)` → may return `false`
** `map.put(nan, value); map.get(nan)` → may return `null`
# How this was found
Detected by an automated JDK conformance oracle (`ObjectOracles.checkEqualsReflexive`).
was:
### Summary
`Dfp.equals()` returns `false` when both operands are NaN, violating the reflexivity requirement
of `Object.equals()` (JDK 21 Javadoc):
> "The `equals` method implements an equivalence relation on non-null object references: It is
> *reflexive*: for any non-null reference value `x`, `x.equals(x)` should return `true`."
### Root cause
In [`Dfp.java` lines 895-907](https://github.com/apache/commons-math/blob/master/commons-math-legacy-core/src/main/java/org/apache/commons/math4/legacy/core/dfp/Dfp.java#L895-L907):
```java
@Override
public boolean equals(final Object other) {
if (other instanceof Dfp) {
finalDfpx = (Dfp) other;
if (isNaN() || x.isNaN() || field.getRadixDigits() != x.field.getRadixDigits()) {
returnfalse; // <-- NaN.equals(NaN) returns false
}
returncompare(this, x) == 0;
}
returnfalse;
}
```
### Reproducer
```java
Dfp nan = new DfpField(20).newDfp().newInstance((byte) 1, Dfp.QNAN);
System.out.println(nan.equals(nan)); // false (should be true)
```
### Comparison with JDK
The JDK's `Double.equals()` and `Float.equals()` deliberately return `true` for NaN:
> `Double.equals()`: "If `d1` represents `+0.0` while `d2` represents `-0.0`, or vice versa,
> the `equal` test has the value `false`, even though `+0.0==-0.0` has the value `true`. [...]
> If `d1` and `d2` both represent `Double.NaN`, then the `equals` method returns `true`, even
> though `Double.NaN==Double.NaN` has the value `false`."
The JDK explicitly chose to break IEEE 754 NaN semantics in `equals()` to preserve the
`Object.equals()` contract. `Dfp` should follow the same convention.
### Impact
Any `Dfp` NaN value placed in a `HashSet`, `HashMap`, or compared with `equals()` will behave
incorrectly:
- `set.add(nan); set.contains(nan)` → may return `false`
- `map.put(nan, value); map.get(nan)` → may return `null`
### How this was found
Detected by an automated JDK conformance oracle (`ObjectOracles.checkEqualsReflexive`).
> `Dfp.equals()` returns `false` for `NaN.equals(NaN)`, violating `Object.equals()` reflexivity
> ---------------------------------------------------------------------------------------------
>
> Key: MATH-1686
> URL: https://issues.apache.org/jira/browse/MATH-1686
> Project: Commons Math
> Issue Type: Bug
> Reporter: Shan Jiang
> Priority: Major
>
> # Summary
> `Dfp.equals()` returns `false` when both operands are NaN, violating the reflexivity requirement
> of `Object.equals()` (JDK 21 Javadoc):
> {noformat}
> "The `equals` method implements an equivalence relation on non-null object references: It is
> *reflexive*: for any non-null reference value `x`, `x.equals(x)` should return `true`."
> {noformat}
> # Root cause
> In [`Dfp.java` lines 895-907|https://github.com/apache/commons-math/blob/master/commons-math-legacy-core/src/main/java/org/apache/commons/math4/legacy/core/dfp/Dfp.java#L895-L907]:
> {code}
> @Override
> public boolean equals(final Object other) {
> if (other instanceof Dfp) {
> finalDfpx = (Dfp) other;
> if (isNaN() || x.isNaN() || field.getRadixDigits() != x.field.getRadixDigits()) {
> returnfalse; // <-- NaN.equals(NaN) returns false
> }
> returncompare(this, x) == 0;
> }
> returnfalse;
> }
> {code}
> # Reproducer
> {code}
> Dfp nan = new DfpField(20).newDfp().newInstance((byte) 1, Dfp.QNAN);
> System.out.println(nan.equals(nan)); // false (should be true)
> {code}
> # Comparison with JDK
> The JDK's `Double.equals()` and `Float.equals()` deliberately return `true` for NaN:
> {noformat}
> `Double.equals()`: "If `d1` represents `+0.0` while `d2` represents `-0.0`, or vice versa,
> the `equal` test has the value `false`, even though `+0.0==-0.0` has the value `true`. [...]
> If `d1` and `d2` both represent `Double.NaN`, then the `equals` method returns `true`, even
> though `Double.NaN==Double.NaN` has the value `false`."
> {noformat}
> The JDK explicitly chose to break IEEE 754 NaN semantics in `equals()` to preserve the
> `Object.equals()` contract. `Dfp` should follow the same convention.
> # Impact
> Any `Dfp` NaN value placed in a `HashSet`, `HashMap`, or compared with `equals()` will behave
> incorrectly:
> ** `set.add(nan); set.contains(nan)` → may return `false`
> ** `map.put(nan, value); map.get(nan)` → may return `null`
> # How this was found
> Detected by an automated JDK conformance oracle (`ObjectOracles.checkEqualsReflexive`).
--
This message was sent by Atlassian Jira
(v8.20.10#820010)