From cb8fa398166011c943b8610dc32d2b4ad5e7c521 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Wed, 17 Jun 2026 05:35:05 +0300 Subject: [PATCH 1/3] feat: add deep-array value-equality helpers in sdk-core Add ValueEquality.contentEquals / contentHashCode to org.dexpace.sdk.core.util for value types that hold array-typed fields. java.util.Objects.equals compares arrays by identity, so structurally-equal ByteArray (or any array) fields are wrongly reported unequal; these helpers compare by content instead. Both helpers handle primitive arrays, object arrays, nulls, and arbitrarily-deep nested / multi-dimensional arrays (via Arrays.deepEquals / deepHashCode), while falling back to ordinary equals/hashCode for non-array values. The two methods are mutually consistent: content-equal values always share a content hash. --- sdk-core/api/sdk-core.api | 6 + .../dexpace/sdk/core/util/ValueEquality.kt | 99 +++++++++ .../sdk/core/util/ValueEqualityTest.kt | 188 ++++++++++++++++++ 3 files changed, 293 insertions(+) create mode 100644 sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt create mode 100644 sdk-core/src/test/kotlin/org/dexpace/sdk/core/util/ValueEqualityTest.kt diff --git a/sdk-core/api/sdk-core.api b/sdk-core/api/sdk-core.api index 7ebcc312..be503dd4 100644 --- a/sdk-core/api/sdk-core.api +++ b/sdk-core/api/sdk-core.api @@ -2418,3 +2418,9 @@ public final class org/dexpace/sdk/core/util/SdkInfo { public final fun getSdkVersion ()Ljava/lang/String; } +public final class org/dexpace/sdk/core/util/ValueEquality { + public static final field INSTANCE Lorg/dexpace/sdk/core/util/ValueEquality; + public static final fun contentEquals (Ljava/lang/Object;Ljava/lang/Object;)Z + public static final fun contentHashCode (Ljava/lang/Object;)I +} + diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt new file mode 100644 index 00000000..dd301d64 --- /dev/null +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2026 dexpace and Omar Aljarrah + * + * Licensed under the MIT License. See LICENSE in the project root. + * SPDX-License-Identifier: MIT + */ + +package org.dexpace.sdk.core.util + +import java.util.Arrays + +/** + * Deep, structural value-equality helpers for value types that hold arrays. + * + * The JDK's [java.util.Objects.equals] and [java.util.Objects.hashCode] compare arrays by + * **identity**, so two structurally-equal `ByteArray` fields (or any array-typed field) are + * reported unequal. A value type generated for a DTO with array fields therefore cannot lean + * on `Objects.equals`/`Objects.hashCode` for a correct `equals`/`hashCode`. + * + * These helpers compare any value **by content**: arrays — including primitive arrays, nested + * object arrays, and arbitrarily-deep multi-dimensional arrays — are compared element-by-element, + * while non-array values fall back to ordinary [Any.equals]/[Any.hashCode]. [contentEquals] and + * [contentHashCode] are mutually consistent: whenever `contentEquals(a, b)` is `true`, + * `contentHashCode(a) == contentHashCode(b)`. + * + * Typical use from a hand-written (or later generated) value type: + * ```kotlin + * override fun equals(other: Any?): Boolean { + * if (this === other) return true + * if (other !is Foo) return false + * return ValueEquality.contentEquals(payload, other.payload) && + * ValueEquality.contentEquals(matrix, other.matrix) + * } + * + * override fun hashCode(): Int { + * var result = ValueEquality.contentHashCode(payload) + * result = 31 * result + ValueEquality.contentHashCode(matrix) + * return result + * } + * ``` + * + * Both methods are null-safe: two `null` references are equal and hash to `0`. + */ +public object ValueEquality { + /** + * Returns `true` if [a] and [b] are equal by content. + * + * - Two `null` references are equal; a `null` and a non-`null` are not. + * - If both are arrays of the same kind, they are compared element-by-element. Object + * arrays recurse, so nested and multi-dimensional arrays are compared structurally; + * primitive arrays are compared by their element values. + * - An object array and a primitive array (e.g. `Array` vs `IntArray`) are **not** + * equal even with matching values, mirroring the JVM's distinct array types. + * - Any other value is compared with [Any.equals]. + */ + @JvmStatic + public fun contentEquals( + a: Any?, + b: Any?, + ): Boolean { + if (a === b) return true + if (a == null || b == null) return false + return when { + a is Array<*> && b is Array<*> -> Arrays.deepEquals(a, b) + a is BooleanArray && b is BooleanArray -> a.contentEquals(b) + a is ByteArray && b is ByteArray -> a.contentEquals(b) + a is CharArray && b is CharArray -> a.contentEquals(b) + a is ShortArray && b is ShortArray -> a.contentEquals(b) + a is IntArray && b is IntArray -> a.contentEquals(b) + a is LongArray && b is LongArray -> a.contentEquals(b) + a is FloatArray && b is FloatArray -> a.contentEquals(b) + a is DoubleArray && b is DoubleArray -> a.contentEquals(b) + else -> a == b + } + } + + /** + * Returns a content-based hash code for [value], consistent with [contentEquals]. + * + * `null` hashes to `0`. Arrays hash by their content — object arrays recurse so nested + * and multi-dimensional arrays contribute a deep hash; primitive arrays hash by their + * element values. Any other value uses its own [Any.hashCode]. + */ + @JvmStatic + public fun contentHashCode(value: Any?): Int = + when (value) { + null -> 0 + is Array<*> -> Arrays.deepHashCode(value) + is BooleanArray -> value.contentHashCode() + is ByteArray -> value.contentHashCode() + is CharArray -> value.contentHashCode() + is ShortArray -> value.contentHashCode() + is IntArray -> value.contentHashCode() + is LongArray -> value.contentHashCode() + is FloatArray -> value.contentHashCode() + is DoubleArray -> value.contentHashCode() + else -> value.hashCode() + } +} diff --git a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/util/ValueEqualityTest.kt b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/util/ValueEqualityTest.kt new file mode 100644 index 00000000..5cfaaab8 --- /dev/null +++ b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/util/ValueEqualityTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2026 dexpace and Omar Aljarrah + * + * Licensed under the MIT License. See LICENSE in the project root. + * SPDX-License-Identifier: MIT + */ + +package org.dexpace.sdk.core.util + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +class ValueEqualityTest { + // ---- null handling ------------------------------------------------------------------- + + @Test + fun `two nulls are equal and hash to zero`() { + assertTrue(ValueEquality.contentEquals(null, null)) + assertEquals(0, ValueEquality.contentHashCode(null)) + } + + @Test + fun `null and non-null are not equal in either order`() { + assertFalse(ValueEquality.contentEquals(null, byteArrayOf(1))) + assertFalse(ValueEquality.contentEquals(byteArrayOf(1), null)) + } + + // ---- identity shortcut --------------------------------------------------------------- + + @Test + fun `same reference is equal`() { + val array = intArrayOf(1, 2, 3) + assertTrue(ValueEquality.contentEquals(array, array)) + } + + // ---- scalars / non-arrays ------------------------------------------------------------ + + @Test + fun `non-array values use ordinary equals and hashCode`() { + assertTrue(ValueEquality.contentEquals("abc", "abc")) + assertFalse(ValueEquality.contentEquals("abc", "xyz")) + assertEquals("abc".hashCode(), ValueEquality.contentHashCode("abc")) + + assertTrue(ValueEquality.contentEquals(42, 42)) + assertEquals(42.hashCode(), ValueEquality.contentHashCode(42)) + } + + // ---- primitive arrays ---------------------------------------------------------------- + + @Test + fun `byte arrays compare by content`() { + val a = byteArrayOf(1, 2, 3) + val b = byteArrayOf(1, 2, 3) + assertTrue(ValueEquality.contentEquals(a, b)) + assertEquals(ValueEquality.contentHashCode(a), ValueEquality.contentHashCode(b)) + + assertFalse(ValueEquality.contentEquals(a, byteArrayOf(1, 2, 4))) + assertFalse(ValueEquality.contentEquals(a, byteArrayOf(1, 2))) + } + + @Test + fun `each primitive array kind compares by content`() { + assertContentEqualPair(booleanArrayOf(true, false), booleanArrayOf(true, false)) + assertContentEqualPair(charArrayOf('a', 'b'), charArrayOf('a', 'b')) + assertContentEqualPair(shortArrayOf(1, 2), shortArrayOf(1, 2)) + assertContentEqualPair(intArrayOf(1, 2), intArrayOf(1, 2)) + assertContentEqualPair(longArrayOf(1L, 2L), longArrayOf(1L, 2L)) + assertContentEqualPair(floatArrayOf(1.5f, 2.5f), floatArrayOf(1.5f, 2.5f)) + assertContentEqualPair(doubleArrayOf(1.5, 2.5), doubleArrayOf(1.5, 2.5)) + } + + @Test + fun `differing primitive arrays are not equal`() { + assertFalse(ValueEquality.contentEquals(intArrayOf(1, 2), intArrayOf(1, 3))) + assertFalse(ValueEquality.contentEquals(longArrayOf(1L), longArrayOf(2L))) + assertFalse(ValueEquality.contentEquals(doubleArrayOf(1.0), doubleArrayOf(2.0))) + } + + // ---- object arrays ------------------------------------------------------------------- + + @Test + fun `object arrays compare by content`() { + val a = arrayOf("x", "y", "z") + val b = arrayOf("x", "y", "z") + assertTrue(ValueEquality.contentEquals(a, b)) + assertEquals(ValueEquality.contentHashCode(a), ValueEquality.contentHashCode(b)) + + assertFalse(ValueEquality.contentEquals(a, arrayOf("x", "y"))) + } + + @Test + fun `object arrays containing nulls compare by content`() { + val a = arrayOf("x", null, "z") + val b = arrayOf("x", null, "z") + assertTrue(ValueEquality.contentEquals(a, b)) + assertEquals(ValueEquality.contentHashCode(a), ValueEquality.contentHashCode(b)) + + assertFalse(ValueEquality.contentEquals(a, arrayOf("x", "y", "z"))) + } + + // ---- nested / multi-dimensional arrays ---------------------------------------------- + + @Test + fun `nested object arrays compare structurally`() { + val a = arrayOf(arrayOf("a", "b"), arrayOf("c")) + val b = arrayOf(arrayOf("a", "b"), arrayOf("c")) + assertTrue(ValueEquality.contentEquals(a, b)) + assertEquals(ValueEquality.contentHashCode(a), ValueEquality.contentHashCode(b)) + + val different = arrayOf(arrayOf("a", "b"), arrayOf("d")) + assertFalse(ValueEquality.contentEquals(a, different)) + } + + @Test + fun `nested arrays of primitive arrays compare structurally`() { + val a = arrayOf(intArrayOf(1, 2), intArrayOf(3, 4)) + val b = arrayOf(intArrayOf(1, 2), intArrayOf(3, 4)) + assertTrue(ValueEquality.contentEquals(a, b)) + assertEquals(ValueEquality.contentHashCode(a), ValueEquality.contentHashCode(b)) + + val different = arrayOf(intArrayOf(1, 2), intArrayOf(3, 5)) + assertFalse(ValueEquality.contentEquals(a, different)) + assertNotEquals(ValueEquality.contentHashCode(a), ValueEquality.contentHashCode(different)) + } + + @Test + fun `deeply nested multi-dimensional arrays compare structurally`() { + val a = arrayOf(arrayOf(arrayOf(1, 2), arrayOf(3)), arrayOf(arrayOf(4))) + val b = arrayOf(arrayOf(arrayOf(1, 2), arrayOf(3)), arrayOf(arrayOf(4))) + assertTrue(ValueEquality.contentEquals(a, b)) + assertEquals(ValueEquality.contentHashCode(a), ValueEquality.contentHashCode(b)) + } + + @Test + fun `nested object arrays holding custom value types compare by element equals`() { + val a = arrayOf(Point(1, 2), Point(3, 4)) + val b = arrayOf(Point(1, 2), Point(3, 4)) + assertTrue(ValueEquality.contentEquals(a, b)) + assertEquals(ValueEquality.contentHashCode(a), ValueEquality.contentHashCode(b)) + + assertFalse(ValueEquality.contentEquals(a, arrayOf(Point(1, 2), Point(9, 9)))) + } + + // ---- cross-kind mismatches ----------------------------------------------------------- + + @Test + fun `object array and primitive array with matching values are not equal`() { + val boxed = arrayOf(1, 2, 3) + val primitive = intArrayOf(1, 2, 3) + assertFalse(ValueEquality.contentEquals(boxed, primitive)) + assertFalse(ValueEquality.contentEquals(primitive, boxed)) + } + + @Test + fun `arrays of different primitive kinds are not equal`() { + assertFalse(ValueEquality.contentEquals(intArrayOf(1, 2), longArrayOf(1L, 2L))) + } + + @Test + fun `array and non-array are not equal`() { + assertFalse(ValueEquality.contentEquals(intArrayOf(1), "not-an-array")) + } + + // ---- equals / hashCode consistency --------------------------------------------------- + + @Test + fun `equal empty arrays of the same kind agree on hashCode`() { + assertTrue(ValueEquality.contentEquals(intArrayOf(), intArrayOf())) + assertEquals(ValueEquality.contentHashCode(intArrayOf()), ValueEquality.contentHashCode(intArrayOf())) + } + + private fun assertContentEqualPair( + a: Any, + b: Any, + ) { + assertTrue(ValueEquality.contentEquals(a, b), "expected $a and $b to be content-equal") + assertEquals( + ValueEquality.contentHashCode(a), + ValueEquality.contentHashCode(b), + "content hashes must agree for content-equal values", + ) + } + + private data class Point(val x: Int, val y: Int) +} From 41636147f52b1b8e7d8b33e7b9c8fd864ed6ead0 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Sun, 21 Jun 2026 00:00:47 +0300 Subject: [PATCH 2/3] refactor: delegate contentEquals to Objects.deepEquals and pin float semantics contentEquals reimplemented java.util.Objects.deepEquals branch for branch, so delegate to it directly instead. Document that array comparison follows Arrays.equals/Double.equals semantics rather than ==: NaN compares equal and 0.0 != -0.0, for both primitive and boxed arrays. Note that contentHashCode mirrors Arrays.deepHashCode by hand and is kept in lockstep with contentEquals. Add tests for NaN and signed zero across primitive and boxed float/double arrays, plus nested arrays, mixed elements, and non-canonical NaN bit patterns. --- .../dexpace/sdk/core/util/ValueEquality.kt | 29 +++--- .../sdk/core/util/ValueEqualityTest.kt | 99 +++++++++++++++++++ 2 files changed, 112 insertions(+), 16 deletions(-) diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt index dd301d64..0dc6203c 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt @@ -8,6 +8,7 @@ package org.dexpace.sdk.core.util import java.util.Arrays +import java.util.Objects /** * Deep, structural value-equality helpers for value types that hold arrays. @@ -51,28 +52,19 @@ public object ValueEquality { * primitive arrays are compared by their element values. * - An object array and a primitive array (e.g. `Array` vs `IntArray`) are **not** * equal even with matching values, mirroring the JVM's distinct array types. + * - Floating-point elements follow `Arrays.equals`/`Double.equals` semantics rather than + * `==`: two `NaN`s compare **equal**, while `0.0` and `-0.0` (and `0.0f`/`-0.0f`) compare + * **unequal**. This holds for both primitive (`DoubleArray`/`FloatArray`) and boxed + * (`Array`/`Array`) arrays, and [contentHashCode] hashes to match. * - Any other value is compared with [Any.equals]. + * + * This is exactly the contract of `java.util.Objects.deepEquals`, to which it delegates. */ @JvmStatic public fun contentEquals( a: Any?, b: Any?, - ): Boolean { - if (a === b) return true - if (a == null || b == null) return false - return when { - a is Array<*> && b is Array<*> -> Arrays.deepEquals(a, b) - a is BooleanArray && b is BooleanArray -> a.contentEquals(b) - a is ByteArray && b is ByteArray -> a.contentEquals(b) - a is CharArray && b is CharArray -> a.contentEquals(b) - a is ShortArray && b is ShortArray -> a.contentEquals(b) - a is IntArray && b is IntArray -> a.contentEquals(b) - a is LongArray && b is LongArray -> a.contentEquals(b) - a is FloatArray && b is FloatArray -> a.contentEquals(b) - a is DoubleArray && b is DoubleArray -> a.contentEquals(b) - else -> a == b - } - } + ): Boolean = Objects.deepEquals(a, b) /** * Returns a content-based hash code for [value], consistent with [contentEquals]. @@ -80,6 +72,11 @@ public object ValueEquality { * `null` hashes to `0`. Arrays hash by their content — object arrays recurse so nested * and multi-dimensional arrays contribute a deep hash; primitive arrays hash by their * element values. Any other value uses its own [Any.hashCode]. + * + * [contentEquals] delegates to `java.util.Objects.deepEquals`, whereas this method mirrors + * `java.util.Arrays.deepHashCode` element-wise — the two do not share an implementation and + * are kept deliberately in lockstep, so any change to either must preserve the contract that + * content-equal values hash equal. */ @JvmStatic public fun contentHashCode(value: Any?): Int = diff --git a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/util/ValueEqualityTest.kt b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/util/ValueEqualityTest.kt index 5cfaaab8..1486879d 100644 --- a/sdk-core/src/test/kotlin/org/dexpace/sdk/core/util/ValueEqualityTest.kt +++ b/sdk-core/src/test/kotlin/org/dexpace/sdk/core/util/ValueEqualityTest.kt @@ -164,6 +164,105 @@ class ValueEqualityTest { assertFalse(ValueEquality.contentEquals(intArrayOf(1), "not-an-array")) } + // ---- floating-point special values --------------------------------------------------- + + @Test + fun `NaN elements compare equal in primitive floating-point arrays`() { + assertTrue(ValueEquality.contentEquals(doubleArrayOf(Double.NaN), doubleArrayOf(Double.NaN))) + assertTrue(ValueEquality.contentEquals(floatArrayOf(Float.NaN), floatArrayOf(Float.NaN))) + assertEquals( + ValueEquality.contentHashCode(doubleArrayOf(Double.NaN)), + ValueEquality.contentHashCode(doubleArrayOf(Double.NaN)), + ) + assertEquals( + ValueEquality.contentHashCode(floatArrayOf(Float.NaN)), + ValueEquality.contentHashCode(floatArrayOf(Float.NaN)), + ) + } + + @Test + fun `NaN elements compare equal in boxed floating-point arrays`() { + assertTrue(ValueEquality.contentEquals(arrayOf(Double.NaN), arrayOf(Double.NaN))) + assertTrue(ValueEquality.contentEquals(arrayOf(Float.NaN), arrayOf(Float.NaN))) + assertEquals( + ValueEquality.contentHashCode(arrayOf(Double.NaN)), + ValueEquality.contentHashCode(arrayOf(Double.NaN)), + ) + assertEquals( + ValueEquality.contentHashCode(arrayOf(Float.NaN)), + ValueEquality.contentHashCode(arrayOf(Float.NaN)), + ) + } + + @Test + fun `positive and negative zero are unequal in primitive floating-point arrays`() { + assertFalse(ValueEquality.contentEquals(doubleArrayOf(0.0), doubleArrayOf(-0.0))) + assertFalse(ValueEquality.contentEquals(floatArrayOf(0.0f), floatArrayOf(-0.0f))) + assertNotEquals( + ValueEquality.contentHashCode(doubleArrayOf(0.0)), + ValueEquality.contentHashCode(doubleArrayOf(-0.0)), + ) + assertNotEquals( + ValueEquality.contentHashCode(floatArrayOf(0.0f)), + ValueEquality.contentHashCode(floatArrayOf(-0.0f)), + ) + } + + @Test + fun `positive and negative zero are unequal in boxed floating-point arrays`() { + assertFalse(ValueEquality.contentEquals(arrayOf(0.0), arrayOf(-0.0))) + assertFalse(ValueEquality.contentEquals(arrayOf(0.0f), arrayOf(-0.0f))) + assertNotEquals( + ValueEquality.contentHashCode(arrayOf(0.0)), + ValueEquality.contentHashCode(arrayOf(-0.0)), + ) + assertNotEquals( + ValueEquality.contentHashCode(arrayOf(0.0f)), + ValueEquality.contentHashCode(arrayOf(-0.0f)), + ) + } + + @Test + fun `NaN survives recursion through nested arrays`() { + // arrayOf(doubleArrayOf(...)) drives the deepEquals/deepHashCode recursion seam, so this + // proves NaN/-0.0 semantics carry through the nested path, not just at the top level. + val a = arrayOf(doubleArrayOf(1.0, Double.NaN), floatArrayOf(Float.NaN)) + val b = arrayOf(doubleArrayOf(1.0, Double.NaN), floatArrayOf(Float.NaN)) + assertTrue(ValueEquality.contentEquals(a, b)) + assertEquals(ValueEquality.contentHashCode(a), ValueEquality.contentHashCode(b)) + + val different = arrayOf(doubleArrayOf(1.0, Double.NaN), floatArrayOf(2.0f)) + assertFalse(ValueEquality.contentEquals(a, different)) + } + + @Test + fun `NaN composes with ordinary elements in the same array`() { + assertTrue( + ValueEquality.contentEquals( + doubleArrayOf(1.0, Double.NaN, 2.0), + doubleArrayOf(1.0, Double.NaN, 2.0), + ), + ) + assertFalse( + ValueEquality.contentEquals( + doubleArrayOf(1.0, Double.NaN, 2.0), + doubleArrayOf(1.0, Double.NaN, 3.0), + ), + ) + } + + @Test + fun `non-canonical NaN bit patterns compare equal and hash equal`() { + // A signaling-NaN bit pattern canonicalizes under doubleToLongBits, so it must compare + // equal to the canonical Double.NaN — a guarantee a generated value type relies on. + val signaling = Double.fromBits(0x7ff0000000000001L) + assertTrue(ValueEquality.contentEquals(doubleArrayOf(signaling), doubleArrayOf(Double.NaN))) + assertEquals( + ValueEquality.contentHashCode(doubleArrayOf(signaling)), + ValueEquality.contentHashCode(doubleArrayOf(Double.NaN)), + ) + } + // ---- equals / hashCode consistency --------------------------------------------------- @Test From bb9d4f1007aa50004ce6906ad76f66f231be0413 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Mon, 22 Jun 2026 01:24:37 +0300 Subject: [PATCH 3/3] docs: explain public surface, delegation, and unsigned-array limit on ValueEquality Expand the ValueEquality KDoc to answer the three questions a reader is most likely to have about it: - Why it is public with no in-tree caller: it is a deliberate, forward-looking primitive that hand-written (or later generated) value types target for their array-typed fields. - Why contentEquals is a thin wrapper over Objects.deepEquals: to pair symmetrically with contentHashCode, for which the JDK offers no Objects.deepHashCode counterpart, giving callers one contract-paired API. - That Kotlin unsigned arrays (UByteArray, etc.) are not recognized as arrays and fall through to identity-based equals/hashCode, with the asXxxArray() workaround for content semantics. Doc-only; no signature or behavior change (apiCheck unchanged). --- .../org/dexpace/sdk/core/util/ValueEquality.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt index 0dc6203c..cd086b18 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ValueEquality.kt @@ -40,6 +40,16 @@ import java.util.Objects * } * ``` * + * No `sdk-core` type depends on these helpers today: they are published as a deliberately public, + * forward-looking primitive that a hand-written value type — or, later, a DTO generator — targets + * directly for its array-typed fields. The public surface is intentional, not incidental. + * + * Kotlin's unsigned arrays (`UByteArray`, `UShortArray`, `UIntArray`, `ULongArray`) are **not** + * recognized as arrays: boxed to `Any?` they are value-class boxes, not JVM primitive arrays, so + * they fall through to identity-based `equals`/`hashCode` and will not compare by content. Convert + * to the signed counterpart (`asByteArray()`, `asIntArray()`, …) for any field that needs content + * semantics. + * * Both methods are null-safe: two `null` references are equal and hash to `0`. */ public object ValueEquality { @@ -59,6 +69,11 @@ public object ValueEquality { * - Any other value is compared with [Any.equals]. * * This is exactly the contract of `java.util.Objects.deepEquals`, to which it delegates. + * + * It is a thin wrapper over that JDK method, kept so it pairs symmetrically with + * [contentHashCode] — for which the JDK offers no `Objects.deepHashCode` equivalent — so callers + * reach one discoverable, contract-paired entry point instead of mixing `Objects.deepEquals` + * with a hand-rolled hash. */ @JvmStatic public fun contentEquals(