From 3b3b77162e82d7b3720f7ec41fdcdb356dc779c0 Mon Sep 17 00:00:00 2001 From: Stefan Haustein Date: Tue, 16 Jun 2026 05:26:07 -0700 Subject: [PATCH] [J2Objc] Switch to ObjC-compatible number hash codes for better interoperability. Native NSNumbers will be equal to Java numbers with the same value (and vice versa) Java Numbers keep being different for type mismatches even if the value (and hash) match (Double(1)!=Integer(1)), but will all be equal to native NSNumber with the same value For more details, please refer to b/524117699 PiperOrigin-RevId: 933033899 --- .../google/j2objc/util/ReflectionUtil.java | 8 ++++ .../harmony/tests/java/lang/ByteTest.java | 10 ++-- .../harmony/tests/java/lang/FloatTest.java | 9 ++-- .../harmony/tests/java/lang/IntegerTest.java | 10 ++-- .../harmony/tests/java/lang/LongTest.java | 8 ++-- .../harmony/tests/java/lang/ShortTest.java | 6 +-- .../tests/java/util/FormatterTest.java | 2 +- .../ojluni/src/main/java/java/lang/Byte.java | 27 ++++++----- .../src/main/java/java/lang/Double.java | 12 ++--- .../ojluni/src/main/java/java/lang/Float.java | 11 +++-- .../src/main/java/java/lang/Integer.java | 12 +++-- .../ojluni/src/main/java/java/lang/Long.java | 12 +++-- .../ojluni/src/main/java/java/lang/Short.java | 27 ++++++----- .../src/main/java/java/util/Arrays.java | 14 +++--- .../com/google/j2objc/ReflectionTest.java | 47 +++++++++++++++++++ 15 files changed, 136 insertions(+), 79 deletions(-) diff --git a/jre_emul/Classes/com/google/j2objc/util/ReflectionUtil.java b/jre_emul/Classes/com/google/j2objc/util/ReflectionUtil.java index 7f6b4832ec..fe2652a431 100644 --- a/jre_emul/Classes/com/google/j2objc/util/ReflectionUtil.java +++ b/jre_emul/Classes/com/google/j2objc/util/ReflectionUtil.java @@ -66,4 +66,12 @@ public static boolean isJreReflectionStripped() { // For more details about this behavior see {@link #matchClassNamePrefix}. return Boolean.TRUE.getClass().getName().equals("JavaLangBoolean"); } + + /** + * Checks if the given object is a NSNumber originating from ObjC by checking that it doesn't have + * class metadata. We use this for cross-language equality support, primarily for hashtable keys. + */ + public static native boolean isObjCNumber(Object obj) /*-[ + return [obj isKindOfClass:[NSNumber class]] && [[obj java_getClass] getMetadata] == NULL; + ]-*/; } diff --git a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/ByteTest.java b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/ByteTest.java index bbff653d67..c922cf5504 100644 --- a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/ByteTest.java +++ b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/ByteTest.java @@ -40,10 +40,10 @@ public void test_valueOfB() { * java.lang.Byte#hashCode() */ public void test_hashCode() { - assertEquals(1, new Byte((byte) 1).hashCode()); - assertEquals(2, new Byte((byte) 2).hashCode()); + assertEquals(-1640531535, new Byte((byte) 1).hashCode()); + assertEquals(1013904226, new Byte((byte) 2).hashCode()); assertEquals(0, new Byte((byte) 0).hashCode()); - assertEquals(-1, new Byte((byte) -1).hashCode()); + assertEquals(-1640531535, new Byte((byte) -1).hashCode()); } /** @@ -510,8 +510,8 @@ public void test_floatValue2() { * java.lang.Byte#hashCode() */ public void test_hashCode2() { - // Test for method int java.lang.Byte.hashCode() - assertEquals("Incorrect hash returned", 127, new Byte((byte) 127).hashCode()); + // Test for method int java.lang.Byte.hashCode() + assertEquals("Incorrect hash returned", 2105892559, new Byte((byte) 127).hashCode()); } /** diff --git a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/FloatTest.java b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/FloatTest.java index 0353068d09..28bcf1c944 100644 --- a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/FloatTest.java +++ b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/FloatTest.java @@ -207,16 +207,13 @@ public void test_floatValue() { public void test_hashCode() { // Test for method int java.lang.Float.hashCode() Float f = new Float(1908.8786f); - assertTrue("Returned invalid hash code for 1908.8786f", f.hashCode() == Float - .floatToIntBits(1908.8786f)); + assertEquals("Returned invalid hash code for 1908.8786f", -743541531, f.hashCode()); f = new Float(-1.112f); - assertTrue("Returned invalid hash code for -1.112", f.hashCode() == Float - .floatToIntBits(-1.112f)); + assertEquals("Returned invalid hash code for -1.112", -1640531535, f.hashCode()); f = new Float(0f); - assertTrue("Returned invalid hash code for 0", f.hashCode() == Float.floatToIntBits(0f)); - + assertEquals("Returned invalid hash code for 0", 0, f.hashCode()); } /** diff --git a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/IntegerTest.java b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/IntegerTest.java index 3c7faa304e..4a552da257 100644 --- a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/IntegerTest.java +++ b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/IntegerTest.java @@ -283,8 +283,8 @@ public void test_hashCode2() { Integer i1 = new Integer(1000); Integer i2 = new Integer(-1000); - assertTrue("Returned incorrect hashcode", i1.hashCode() == 1000 - && (i2.hashCode() == -1000)); + assertTrue("Returned incorrect hashcode", i1.hashCode() == 145972072 + && (i2.hashCode() == 145972072)); } /** @@ -688,10 +688,10 @@ public void test_valueOfI() { * java.lang.Integer#hashCode() */ public void test_hashCode() { - assertEquals(1, new Integer(1).hashCode()); - assertEquals(2, new Integer(2).hashCode()); + assertEquals(-1640531535, new Integer(1).hashCode()); + assertEquals(1013904226, new Integer(2).hashCode()); assertEquals(0, new Integer(0).hashCode()); - assertEquals(-1, new Integer(-1).hashCode()); + assertEquals(-1640531535, new Integer(-1).hashCode()); } /** diff --git a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/LongTest.java b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/LongTest.java index 9da75cfc42..330e1cc723 100644 --- a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/LongTest.java +++ b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/LongTest.java @@ -526,10 +526,10 @@ public void test_valueOfJ() { * java.lang.Long#hashCode() */ public void test_hashCode() { - assertEquals((int) (1L ^ (1L >>> 32)), new Long(1).hashCode()); - assertEquals((int) (2L ^ (2L >>> 32)), new Long(2).hashCode()); - assertEquals((int) (0L ^ (0L >>> 32)), new Long(0).hashCode()); - assertEquals((int) (-1L ^ (-1L >>> 32)), new Long(-1).hashCode()); + assertEquals(-1640531535, new Long(1).hashCode()); + assertEquals(1013904226, new Long(2).hashCode()); + assertEquals(0, new Long(0).hashCode()); + assertEquals(-1640531535, new Long(-1).hashCode()); } /** diff --git a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/ShortTest.java b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/ShortTest.java index bf747e7005..03a767fabe 100644 --- a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/ShortTest.java +++ b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/lang/ShortTest.java @@ -361,10 +361,10 @@ public void test_valueOfS() { * java.lang.Short#hashCode() */ public void test_hashCode() { - assertEquals(1, new Short((short) 1).hashCode()); - assertEquals(2, new Short((short) 2).hashCode()); + assertEquals(-1640531535, new Short((short) 1).hashCode()); + assertEquals(1013904226, new Short((short) 2).hashCode()); assertEquals(0, new Short((short) 0).hashCode()); - assertEquals(-1, new Short((short) -1).hashCode()); + assertEquals(-1640531535, new Short((short) -1).hashCode()); } /** diff --git a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/util/FormatterTest.java b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/util/FormatterTest.java index b3f9452f55..028daac630 100644 --- a/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/util/FormatterTest.java +++ b/jre_emul/android/platform/libcore/harmony-tests/src/test/java/org/apache/harmony/tests/java/util/FormatterTest.java @@ -773,7 +773,7 @@ public void test_ioException() throws IOException { formatter = new Formatter(Locale.FRANCE); formatter.format( - "xx%1$s22%2$s%s%>> 32)); - } + public static native int hashCode(double value) /*-[ + return (jint)[@(value) hash]; + ]-*/; /** * Compares this object against the specified object. The result @@ -927,8 +927,8 @@ public static int hashCode(double value) { * @jls 15.21.1 Numerical Equality Operators == and != */ public boolean equals(Object obj) { - return (obj instanceof Double) - && (doubleToLongBits(((Double)obj).value) == + return (obj instanceof Double || ReflectionUtil.isObjCNumber(obj)) + && (doubleToLongBits(((Number) obj).doubleValue()) == doubleToLongBits(value)); } diff --git a/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Float.java b/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Float.java index 65a828c791..59c9ff5279 100644 --- a/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Float.java +++ b/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Float.java @@ -34,6 +34,7 @@ */ // END Android-removed: dynamic constants not supported on Android. +import com.google.j2objc.util.ReflectionUtil; import sun.misc.FloatingDecimal; /** @@ -720,9 +721,9 @@ public int hashCode() { * @return a hash code value for a {@code float} value. * @since 1.8 */ - public static int hashCode(float value) { - return floatToIntBits(value); - } + public static native int hashCode(float value) /*-[ + return (jint)[@(value) hash]; + ]-*/; /** * Compares this object against the specified object. The result @@ -752,8 +753,8 @@ public static int hashCode(float value) { * @jls 15.21.1 Numerical Equality Operators == and != */ public boolean equals(Object obj) { - return (obj instanceof Float) - && (floatToIntBits(((Float)obj).value) == floatToIntBits(value)); + return (obj instanceof Float || ReflectionUtil.isObjCNumber(obj)) + && (floatToIntBits(((Number) obj).floatValue()) == floatToIntBits(value)); } /** diff --git a/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Integer.java b/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Integer.java index c80daca4e7..6ca133ba0f 100644 --- a/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Integer.java +++ b/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Integer.java @@ -25,6 +25,8 @@ package java.lang; +import com.google.j2objc.util.ReflectionUtil; + /* J2ObjC removed import jdk.internal.vm.annotation.IntrinsicCandidate; */ @@ -1039,9 +1041,9 @@ public int hashCode() { * * @return a hash code value for an {@code int} value. */ - public static int hashCode(int value) { - return value; - } + public static native int hashCode(int value) /*-[ + return (jint)[@(value) hash]; + ]-*/; /** * Compares this object to the specified object. The result is @@ -1054,8 +1056,8 @@ public static int hashCode(int value) { * {@code false} otherwise. */ public boolean equals(Object obj) { - if (obj instanceof Integer) { - return value == ((Integer)obj).intValue(); + if (obj instanceof Integer || ReflectionUtil.isObjCNumber(obj)) { + return value == ((Number) obj).intValue(); } return false; } diff --git a/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Long.java b/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Long.java index ea619a5576..20bce5b722 100644 --- a/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Long.java +++ b/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Long.java @@ -25,6 +25,8 @@ package java.lang; +import com.google.j2objc.util.ReflectionUtil; + /* J2ObjC removed import jdk.internal.vm.annotation.IntrinsicCandidate; */ @@ -1265,9 +1267,9 @@ public int hashCode() { * @return a hash code value for a {@code long} value. * @since 1.8 */ - public static int hashCode(long value) { - return (int)(value ^ (value >>> 32)); - } + public static native int hashCode(long value) /*-[ + return (jint)[@(value) hash]; + ]-*/; /** * Compares this object to the specified object. The result is @@ -1280,8 +1282,8 @@ public static int hashCode(long value) { * {@code false} otherwise. */ public boolean equals(Object obj) { - if (obj instanceof Long) { - return value == ((Long)obj).longValue(); + if (obj instanceof Long || ReflectionUtil.isObjCNumber(obj)) { + return value == ((Number) obj).longValue(); } return false; } diff --git a/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Short.java b/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Short.java index 4be3273042..cf9be3751e 100644 --- a/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Short.java +++ b/jre_emul/android/platform/libcore/ojluni/src/main/java/java/lang/Short.java @@ -25,6 +25,8 @@ package java.lang; +import com.google.j2objc.util.ReflectionUtil; + /* J2ObjC removed import jdk.internal.vm.annotation.IntrinsicCandidate; */ @@ -479,17 +481,16 @@ public int hashCode() { return Short.hashCode(value); } - /** - * Returns a hash code for a {@code short} value; compatible with - * {@code Short.hashCode()}. - * - * @param value the value to hash - * @return a hash code value for a {@code short} value. - * @since 1.8 - */ - public static int hashCode(short value) { - return (int)value; - } + /** + * Returns a hash code for a {@code short} value; compatible with {@code Short.hashCode()}. + * + * @param value the value to hash + * @return a hash code value for a {@code short} value. + * @since 1.8 + */ + public static native int hashCode(short value) /*-[ + return (jint)[@(value) hash]; + ]-*/; /** * Compares this object to the specified object. The result is @@ -502,8 +503,8 @@ public static int hashCode(short value) { * {@code false} otherwise. */ public boolean equals(Object obj) { - if (obj instanceof Short) { - return value == ((Short)obj).shortValue(); + if (obj instanceof Short || ReflectionUtil.isObjCNumber(obj)) { + return value == ((Number) obj).shortValue(); } return false; } diff --git a/jre_emul/android/platform/libcore/ojluni/src/main/java/java/util/Arrays.java b/jre_emul/android/platform/libcore/ojluni/src/main/java/java/util/Arrays.java index 19770da14b..9a6669f859 100644 --- a/jre_emul/android/platform/libcore/ojluni/src/main/java/java/util/Arrays.java +++ b/jre_emul/android/platform/libcore/ojluni/src/main/java/java/util/Arrays.java @@ -4084,8 +4084,7 @@ public static int hashCode(long a[]) { int result = 1; for (long element : a) { - int elementHash = (int)(element ^ (element >>> 32)); - result = 31 * result + elementHash; + result = 31 * result + Long.hashCode(element); } return result; @@ -4113,7 +4112,7 @@ public static int hashCode(int a[]) { int result = 1; for (int element : a) - result = 31 * result + element; + result = 31 * result + Integer.hashCode(element); return result; } @@ -4140,7 +4139,7 @@ public static int hashCode(short a[]) { int result = 1; for (short element : a) - result = 31 * result + element; + result = 31 * result + Short.hashCode(element); return result; } @@ -4194,7 +4193,7 @@ public static int hashCode(byte a[]) { int result = 1; for (byte element : a) - result = 31 * result + element; + result = 31 * result + Byte.hashCode(element); return result; } @@ -4248,7 +4247,7 @@ public static int hashCode(float a[]) { int result = 1; for (float element : a) - result = 31 * result + Float.floatToIntBits(element); + result = 31 * result + Float.hashCode(element); return result; } @@ -4275,8 +4274,7 @@ public static int hashCode(double a[]) { int result = 1; for (double element : a) { - long bits = Double.doubleToLongBits(element); - result = 31 * result + (int)(bits ^ (bits >>> 32)); + result = 31 * result + Double.hashCode(element); } return result; } diff --git a/jre_emul/misc_tests/com/google/j2objc/ReflectionTest.java b/jre_emul/misc_tests/com/google/j2objc/ReflectionTest.java index cd9fb13eb5..041d131c14 100644 --- a/jre_emul/misc_tests/com/google/j2objc/ReflectionTest.java +++ b/jre_emul/misc_tests/com/google/j2objc/ReflectionTest.java @@ -201,4 +201,51 @@ public void testAnnotationInitializedWithDefaults() { assertEquals("hello", a.annotationValue().value()); assertEquals(Iterable.class, a.classValue()); } + + private static native Object getNativeNumber(long val) /*-[ + return [NSNumber numberWithLongLong:(long long)val]; + ]-*/; + + public void testIsObjCNumber() { + Object nativeNum = getNativeNumber(42); + Object javaNum = Integer.valueOf(42); + Object decimalNum = new java.math.BigDecimal("1.23"); + + assertTrue(com.google.j2objc.util.ReflectionUtil.isObjCNumber(nativeNum)); + assertFalse(com.google.j2objc.util.ReflectionUtil.isObjCNumber(javaNum)); + assertFalse(com.google.j2objc.util.ReflectionUtil.isObjCNumber(decimalNum)); + assertFalse(com.google.j2objc.util.ReflectionUtil.isObjCNumber("not a number")); + assertFalse(com.google.j2objc.util.ReflectionUtil.isObjCNumber(null)); + } + + public void testNumberHashMapCompatibility() { + java.util.HashMap map = new java.util.HashMap<>(); + + // Test with positive number (sign bit not set) + Object javaInt = Integer.valueOf(42); + Object objcInt = getNativeNumber(42); + + map.put((Number) javaInt, "success"); + assertEquals(1, map.size()); + assertEquals("success", map.get(objcInt)); + + // Overwrite existing key (proves equals() and hashCode() consistency in map bucket resolution) + map.put((Number) objcInt, "success_reverse"); + assertEquals(1, map.size()); + assertEquals("success_reverse", map.get(javaInt)); + + // Test with negative number (sign bit set in the hash code) + map.clear(); + Object javaIntMinus1 = Integer.valueOf(-1); + Object objcIntMinus1 = getNativeNumber(-1); + + map.put((Number) javaIntMinus1, "success_minus1"); + assertEquals(1, map.size()); + assertEquals("success_minus1", map.get(objcIntMinus1)); + + // Overwrite existing key + map.put((Number) objcIntMinus1, "success_minus1_reverse"); + assertEquals(1, map.size()); + assertEquals("success_minus1_reverse", map.get(javaIntMinus1)); + } }