Skip to content

Make Matrix equals and hashCode obey the Object contract - #1945

Merged
kevinherron merged 2 commits into
eclipse-milo:mainfrom
alxkm:fix/matrix-equals-hashcode-contract
Sep 7, 2026
Merged

Make Matrix equals and hashCode obey the Object contract#1945
kevinherron merged 2 commits into
eclipse-milo:mainfrom
alxkm:fix/matrix-equals-hashcode-contract

Conversation

@alxkm

@alxkm alxkm commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Matrix violates the Object.equals/Object.hashCode contract in two ways, so an equal
Matrix is not found in a HashSet and cannot be used as a HashMap key. Variant and
DataValue carrying a Matrix inherit it, because Variant.valueHash() falls through to the
value's own hashCode() for anything that is not an array.

1. hashCode() hashes the array identity, not its contents

Objects.hash(flatArray, dataType) calls flatArray.hashCode(), and flatArray is declared
Object, so for a primitive or object array that is the identity hash:

Matrix m1 = new Matrix(new int[][] {{1, 2}, {3, 4}});
Matrix m2 = new Matrix(new int[][] {{1, 2}, {3, 4}});

m1.equals(m2);                    // true
m1.hashCode() == m2.hashCode();   // false
Set.of(m1).contains(m2);          // false

equals() boxes primitive arrays before comparing, so new Matrix(int[][]) and
new Matrix(Integer[][]) are equal (pinned by the existing primitiveBoxedEquality test) but
also hash differently today.

2. equals() shortcuts past dimensions and dataType

The thisArray == thatArray shortcut returns true before either is compared, while
hashCode() does account for both — the same contract violation from the other side:

int[] elements = {1, 2, 3, 4};

new Matrix(elements, new int[] {2, 2})
    .equals(new Matrix(elements, new int[] {4, 1}));              // true

new Matrix(elements, new int[] {2, 2}, OpcUaDataType.Int32)
    .equals(new Matrix(elements, new int[] {2, 2}, OpcUaDataType.UInt32));  // true

Matrix does not copy the elements it is given and getElements() hands the backing array out
directly, so a shared backing array is reachable through the public Matrix(Object, int[])
constructors.

The two are entangled: fixing only hashCode() leaves case 2 violating the contract, and fixing
only equals() leaves case 1. Neither half restores it on its own, so both are here.

Change

  • Hash the elements by value, boxing primitive arrays first so a Matrix of primitives hashes the
    same as the equal Matrix of the boxed type, matching how equals() compares them.
    Arrays.deepHashCode(box(prim[])) equals Arrays.hashCode(prim[]) for all eight primitive
    types, so this is consistent in both directions.
  • Include dataType in the hash alongside dimensions, both of which equals() compares.
  • Compare dimensions and dataType up front in equals(), which also removes the duplicated
    comparison from its two branches.

No behaviour change for unequal matrices, and no in-tree caller relies on the old behaviour.

Tests

Four tests added to MatrixTest. All four fail before the change and pass after:

MatrixTest.equalMatricesHashTheSame                       expected: <679162395> but was: <-1506169415>
MatrixTest.primitiveBoxedHashEquality                     expected: <-1070723957> but was: <-1468407034>
MatrixTest.sharedElementsWithDifferentDimensionsAreNotEqual  expected: not equal but was: dimensions=[4, 1]
MatrixTest.sharedElementsWithDifferentDataTypesAreNotEqual   expected: not equal but was: dataType=UInt32

Verified on JDK 17 with mvn -pl opc-ua-sdk/integration-tests -am test: stack-core, transport,
sdk-core, sdk-client, dtd-core, dtd-reader, sdk-server, encoding-json, encoding-xml and the
integration tests all pass — 2283 tests, 0 failures, 0 errors. mvn -pl opc-ua-stack/stack-core verify passes, including spotless:check.


  • You have signed an Eclipse Contributor Agreement and are committing using the same email address
  • Your code contains any tests relevant to the problem you are solving
  • All new and existing tests passed
  • Code follows the style guidelines and Checkstyle passes

Matrix.hashCode hashed the elements with Objects.hash(flatArray, dataType).
flatArray is declared Object, so for an array that takes the identity hash
rather than the contents, and two Matrices that are equal hash differently: a
Matrix is not found in a HashSet and cannot be used as a HashMap key. A Variant
or DataValue carrying a Matrix inherits this, because Variant.valueHash falls
through to the value's own hashCode for anything that is not an array.

equals had a second, related defect. The thisArray == thatArray shortcut
returned true before dimensions and dataType were compared, so two Matrices
sharing a backing array compared equal even with different dimensions or a
different data type, while hashCode did account for both. That is the same
contract violation from the other side: equal values, unequal hashes. Matrix
does not copy the elements it is given, so a shared backing array is reachable
through the public Matrix(Object, int[]) constructors.

Hash the elements by value, boxing primitive arrays first so that a Matrix of
primitives hashes the same as the equal Matrix of the boxed type, which is how
equals already compares them. Compare dimensions and dataType up front in
equals, which also removes the duplicated comparison from both branches.

Co-authored-by: Oleksandr Klymenko <19151554+alxkm@users.noreply.github.com>
Signed-off-by: alxkm <alexanderklmn@gmail.com>
Hash primitive arrays directly while preserving equality and hash
compatibility with boxed arrays. This avoids allocating a boxed copy
on every Matrix hash calculation.

Cover floating-point edge cases, nested values, and hash lookups through
Variant and DataValue. Document backing-array ownership and the need to
keep shared values unchanged and nested values acyclic.
@kevinherron
kevinherron merged commit e4ca7ae into eclipse-milo:main Sep 7, 2026
3 checks passed
@kevinherron kevinherron added this to the 1.1.7 milestone Sep 9, 2026
@alxkm
alxkm deleted the fix/matrix-equals-hashcode-contract branch September 9, 2026 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants