diff --git a/src/java.base/share/classes/java/lang/Class.java b/src/java.base/share/classes/java/lang/Class.java index 02ddc71f04b..ae60cbe4047 100644 --- a/src/java.base/share/classes/java/lang/Class.java +++ b/src/java.base/share/classes/java/lang/Class.java @@ -607,36 +607,34 @@ public static Class forName(Module module, String name) { } /** - * {@return {@code true} if this {@code Class} object represents an identity - * class or interface; otherwise {@code false}} + * {@return {@code true} if this {@code Class} object represents an identity class; + * otherwise {@code false}} * - * If this {@code Class} object represents an array type, then this method - * returns {@code true}. - * If this {@code Class} object represents a primitive type, or {@code void}, + * If this {@code Class} object represents an array type then this method returns {@code true}. + * If this {@code Class} object represents an interface, a primitive type, or {@code void} * then this method returns {@code false}. * + * @see AccessFlag#IDENTITY * @since Valhalla */ @PreviewFeature(feature = PreviewFeature.Feature.VALUE_OBJECTS, reflective=true) public native boolean isIdentity(); /** - * {@return {@code true} if this {@code Class} object represents a value - * class; otherwise {@code false}} + * {@return {@code true} if this {@code Class} object represents a value class; + * otherwise {@code false}} + * All classes that are not {@linkplain #isIdentity identity classes} are value classes. * - * If this {@code Class} object represents an array type, an interface, - * a primitive type, or {@code void}, then this method returns {@code false}. + * If this {@code Class} object represents an array type then this method returns {@code false}. + * If this {@code Class} object represents an interface, a primitive type, or {@code void}, + * then this method returns {@code true}. * + * @see AccessFlag#IDENTITY * @since Valhalla */ @PreviewFeature(feature = PreviewFeature.Feature.VALUE_OBJECTS, reflective=true) public boolean isValue() { - if (!PreviewFeatures.isEnabled()) { - return false; - } - if (isPrimitive() || isArray() || isInterface()) - return false; - return ((getModifiers() & Modifier.IDENTITY) == 0); + return PreviewFeatures.isEnabled() ? !isIdentity() : false; } /** diff --git a/src/java.base/share/classes/java/lang/IdentityException.java b/src/java.base/share/classes/java/lang/IdentityException.java index c6a5ca0563d..848b964ed2a 100644 --- a/src/java.base/share/classes/java/lang/IdentityException.java +++ b/src/java.base/share/classes/java/lang/IdentityException.java @@ -30,7 +30,8 @@ *

* Identity objects are required for synchronization and locking. * Value-based - * objects do not have identity and cannot be used for synchronization or locking. + * objects do not have identity and cannot be used for synchronization, locking, + * or for any type of {@link java.lang.ref.Reference}. * * @since Valhalla */ diff --git a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java index 41ff4623609..b8b746ee028 100644 --- a/src/java.base/share/classes/java/lang/System.java +++ b/src/java.base/share/classes/java/lang/System.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1994, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1994, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -471,6 +471,21 @@ public static native void arraycopy(Object src, int srcPos, * hashCode(). * The hash code for the null reference is zero. * + *

+ *
+ * The "identity hash code" of a {@linkplain Class#isValue() value object} + * is computed by combining the hash codes of the value object's fields recursively. + *
+ *
+ * @apiNote + *
+ *
+ * Note that, like ==, this hash code exposes information about a value object's + * private fields that might otherwise be hidden by an identity object. + * Developers should be cautious about storing sensitive secrets in value object fields. + *
+ *
+ * * @param x object for which the hashCode is to be calculated * @return the hashCode * @since 1.1 diff --git a/src/java.base/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java b/src/java.base/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java index a7d6217988e..282aa257aaf 100644 --- a/src/java.base/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java +++ b/src/java.base/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -596,7 +596,7 @@ LoadableDescriptorsAttributeBuilder add(MethodType... mtypes) { } boolean requiresLoadableDescriptors(Class cls) { - return cls.isValue() && cls.accessFlags().contains(AccessFlag.FINAL); + return cls.isValue() && cls.accessFlags().contains(AccessFlag.FINAL) && !cls.isPrimitive(); } boolean isEmpty() { diff --git a/src/java.base/share/classes/java/util/IdentityHashMap.java b/src/java.base/share/classes/java/util/IdentityHashMap.java index 48a6d7b28df..5a11bada32a 100644 --- a/src/java.base/share/classes/java/util/IdentityHashMap.java +++ b/src/java.base/share/classes/java/util/IdentityHashMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,8 +35,8 @@ /** * This class implements the {@code Map} interface with a hash table, using - * reference-equality in place of object-equality when comparing keys (and - * values). In other words, in an {@code IdentityHashMap}, two keys + * `==` in place of object-equality when comparing keys (and values). + * In other words, in an {@code IdentityHashMap}, two keys * {@code k1} and {@code k2} are considered equal if and only if * {@code (k1==k2)}. (In normal {@code Map} implementations (like * {@code HashMap}) two keys {@code k1} and {@code k2} are considered equal diff --git a/src/java.base/share/classes/java/util/Objects.java b/src/java.base/share/classes/java/util/Objects.java index 7085323ce1e..f732401819a 100644 --- a/src/java.base/share/classes/java/util/Objects.java +++ b/src/java.base/share/classes/java/util/Objects.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,6 @@ import jdk.internal.javac.PreviewFeature; import jdk.internal.util.Preconditions; import jdk.internal.vm.annotation.ForceInline; -import jdk.internal.misc.Unsafe; import java.util.function.Supplier; @@ -180,19 +179,38 @@ public static String toIdentityString(Object o) { } /** - * {@return {@code true} if the specified object reference is an identity object, - * otherwise {@code false}} + * {@return {@code true} if the object is a non-null reference + * to an {@linkplain Class#isIdentity() identity object}, otherwise {@code false}} * - * @param obj an object - * @throws NullPointerException if {@code obj} is {@code null} + * @apiNote + * If the parameter is {@code null}, there is no object + * and hence no class to check for identity; the return is {@code false}. + * To test for a {@linkplain Class#isValue() value object} use: + * {@snippet type="java" : + * if (obj != null && !Objects.hasIdentity(obj)) { + * // obj is a non-null value object + * } + * } + * @param obj an object or {@code null} * @since Valhalla */ @PreviewFeature(feature = PreviewFeature.Feature.VALUE_OBJECTS) // @IntrinsicCandidate public static boolean hasIdentity(Object obj) { - requireNonNull(obj); - return obj.getClass().isIdentity() || // Before Valhalla all classes are identity classes - obj.getClass() == Object.class; + return (obj == null) ? false : obj.getClass().isIdentity(); + } + + /** + * {@return {@code true} if the object is a non-null reference + * to an {@linkplain Class#isValue() value object}, otherwise {@code false}} + * + * @param obj an object or {@code null} + * @since Valhalla + */ + @PreviewFeature(feature = PreviewFeature.Feature.VALUE_OBJECTS) +// @IntrinsicCandidate + public static boolean isValueObject(Object obj) { + return (obj == null) ? false : obj.getClass().isValue(); } /** diff --git a/src/java.base/share/classes/java/util/WeakHashMap.java b/src/java.base/share/classes/java/util/WeakHashMap.java index 276e8731d84..e379a3a8f2e 100644 --- a/src/java.base/share/classes/java/util/WeakHashMap.java +++ b/src/java.base/share/classes/java/util/WeakHashMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,8 +25,8 @@ package java.util; -import java.lang.ref.WeakReference; import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -122,6 +122,22 @@ * * Java Collections Framework. * + * @apiNote + *
+ *
+ * Objects that are {@linkplain Class#isValue() value objects} do not have identity + * and can not be used as keys in a {@code WeakHashMap}. {@linkplain java.lang.ref.Reference References} + * such as {@linkplain WeakReference WeakReference} used by {@code WeakhashMap} + * to hold the key; cannot refer to a value object. + * Methods such as {@linkplain #get get} or {@linkplain #containsKey containsKey} + * will always return {@code null} or {@code false} respectively. + * The methods such as {@linkplain #put put}, {@linkplain #putAll putAll}, + * {@linkplain #compute(Object, BiFunction) compute}, and + * {@linkplain #computeIfAbsent(Object, Function) computeIfAbsent} or any method putting + * a value object, as a key, throw {@link IdentityException}. + *
+ *
+ * * @param the type of keys maintained by this map * @param the type of mapped values * @@ -288,6 +304,8 @@ static Object unmaskNull(Object key) { /** * Checks for equality of non-null reference x and possibly-null y. By * default uses Object.equals. + * The key may be a value object, but it will never be equal to the referent + * so does not need a separate Objects.hasIdentity check. */ private boolean matchesKey(Entry e, Object key) { // check if the given entry refers to the given key without @@ -456,9 +474,11 @@ Entry getEntry(Object key) { * {@code null} if there was no mapping for {@code key}. * (A {@code null} return can also indicate that the map * previously associated {@code null} with {@code key}.) + * @throws IdentityException if {@code key} is a value object */ public V put(K key, V value) { Object k = maskNull(key); + Objects.requireIdentity(k); int h = hash(k); Entry[] tab = getTable(); int i = indexFor(h, tab.length); @@ -548,6 +568,7 @@ private void transfer(Entry[] src, Entry[] dest) { * * @param m mappings to be stored in this map. * @throws NullPointerException if the specified map is null. + * @throws IdentityException if any of the {@code keys} is a value object */ public void putAll(Map m) { int numKeysToBeAdded = m.size(); diff --git a/test/jdk/java/lang/Class/GenericStringTest.java b/test/jdk/java/lang/Class/GenericStringTest.java index 73a3ffed5ad..9b0f744944d 100644 --- a/test/jdk/java/lang/Class/GenericStringTest.java +++ b/test/jdk/java/lang/Class/GenericStringTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -57,7 +57,7 @@ public static void main(String... args) throws ReflectiveOperationException { new PlatformTestCase(java.lang.Enum.class, "public abstract class java.lang.Enum>"), new PlatformTestCase(java.util.Map.class, - "public abstract interface java.util.Map"), + "public abstract value interface java.util.Map"), new PlatformTestCase(java.util.EnumMap.class, "public class java.util.EnumMap,V>"), new PlatformTestCase(java.util.EventListenerProxy.class, @@ -141,10 +141,10 @@ private static int checkToGenericString(Class clazz, String expected) { String value(); } -@ExpectedGenericString("abstract interface AnInterface") +@ExpectedGenericString("abstract value interface AnInterface") strictfp interface AnInterface {} -@ExpectedGenericString("abstract interface LocalMap") +@ExpectedGenericString("abstract value interface LocalMap") interface LocalMap {} @ExpectedGenericString("final enum AnEnum") @@ -202,7 +202,7 @@ non-sealed class GreatGrandChildACCB extends GrandChildACC {} } // Test cases for sealed/non-sealed _interface_ hierarchy. -@ExpectedGenericString("abstract sealed interface SealedRootIntf") +@ExpectedGenericString("abstract sealed value interface SealedRootIntf") sealed interface SealedRootIntf permits SealedRootIntf.ChildA, @@ -241,13 +241,13 @@ non-sealed class GreatGrandChildACCB extends GrandChildACC {} } } - @ExpectedGenericString("public abstract static sealed interface SealedRootIntf$IntfA") + @ExpectedGenericString("public abstract static sealed value interface SealedRootIntf$IntfA") sealed interface IntfA extends SealedRootIntf { @ExpectedGenericString("public static non-sealed class SealedRootIntf$IntfA$IntfAImpl") non-sealed class IntfAImpl implements IntfA {} } - @ExpectedGenericString("public abstract static non-sealed interface SealedRootIntf$IntfB") + @ExpectedGenericString("public abstract static non-sealed value interface SealedRootIntf$IntfB") non-sealed interface IntfB extends SealedRootIntf { // Check that non-sealing can be allowed with a second superinterface being sealed. @ExpectedGenericString("public static non-sealed class SealedRootIntf$IntfB$IntfAImpl") diff --git a/test/jdk/java/util/Collection/MOAT.java b/test/jdk/java/util/Collection/MOAT.java index d281e5db125..cee1d373aa6 100644 --- a/test/jdk/java/util/Collection/MOAT.java +++ b/test/jdk/java/util/Collection/MOAT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,11 +26,12 @@ * @bug 6207984 6272521 6192552 6269713 6197726 6260652 5073546 4137464 * 4155650 4216399 4294891 6282555 6318622 6355327 6383475 6420753 * 6431845 4802633 6570566 6570575 6570631 6570924 6691185 6691215 - * 4802647 7123424 8024709 8193128 + * 4802647 7123424 8024709 8193128 8346307 * @summary Run many tests on many Collection and Map implementations * @author Martin Buchholz * @modules java.base/java.util:open * @run main MOAT + * @run main/othervm --enable-preview MOAT * @key randomness */ diff --git a/test/jdk/java/util/WeakHashMapValues.java b/test/jdk/java/util/WeakHashMapValues.java new file mode 100644 index 00000000000..774f3ade96b --- /dev/null +++ b/test/jdk/java/util/WeakHashMapValues.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.util.HashMap; +import java.util.WeakHashMap; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/* + * @test + * @summary Check WeakHashMap throws IdentityException when Value Objects are put + * @enablePreview + * @run junit WeakHashMapValues + */ +public class WeakHashMapValues { + + /* + * Check that any kind of put with a value class as a key throws IdentityException + */ + @Test + void checkThrowsIdentityException() { + WeakHashMap whm = new WeakHashMap<>(); + Object key = new Foo(1); + assertThrows(IdentityException.class, () -> whm.put(key, "1")); + assertThrows(IdentityException.class, () -> whm.putIfAbsent(key, "2")); + assertThrows(IdentityException.class, () -> whm.compute(key, (_, _) -> "3")); + assertThrows(IdentityException.class, () -> whm.computeIfAbsent(key, (_) -> "4")); + + HashMap hmap = new HashMap<>(); + hmap.put(key, "6"); + assertThrows(IdentityException.class, () -> whm.putAll(hmap)); + } + + /* + * Check that any kind of put with Integer as a value class as a key throws IdentityException + */ + @Test + void checkIntegerThrowsIdentityException() { + WeakHashMap whm = new WeakHashMap<>(); + Object key = 1; + assertThrows(IdentityException.class, () -> whm.put(key, "1")); + assertThrows(IdentityException.class, () -> whm.putIfAbsent(key, "2")); + assertThrows(IdentityException.class, () -> whm.compute(key, (_, _) -> "3")); + assertThrows(IdentityException.class, () -> whm.computeIfAbsent(key, (_) -> "4")); + + HashMap hmap = new HashMap<>(); + hmap.put(key, "6"); + assertThrows(IdentityException.class, () -> whm.putAll(hmap)); + + } + + /** + * Check that queries with a value object return false or null. + */ + @Test + void checkValueObjectGet() { + WeakHashMap whm = new WeakHashMap<>(); + Object key = "X"; + Object v = new Foo(1); + assertEquals(whm.get(v), null, "Get of value object should return null"); + assertEquals(whm.containsKey(v), false, "containsKey should return false"); + } +} + +value class Foo { + int x; + Foo(int x) { + this.x = x; + } +} diff --git a/test/jdk/valhalla/valuetypes/ObjectMethods.java b/test/jdk/valhalla/valuetypes/ObjectMethods.java index 9e6cb23d2e3..83369efd85f 100644 --- a/test/jdk/valhalla/valuetypes/ObjectMethods.java +++ b/test/jdk/valhalla/valuetypes/ObjectMethods.java @@ -29,9 +29,13 @@ * @run junit/othervm -Dvalue.bsm.salt=1 ObjectMethods * @run junit/othervm -Dvalue.bsm.salt=1 -XX:-UseFieldFlattening ObjectMethods */ +import java.util.Optional; import java.util.List; import java.util.Objects; +import java.util.function.Function; import java.util.stream.Stream; +import java.lang.reflect.AccessFlag; +import java.lang.reflect.Modifier; import jdk.internal.value.ValueClass; import jdk.internal.vm.annotation.ImplicitlyConstructible; @@ -112,12 +116,13 @@ static value record ValueRecord(int i, String name) {} static final Ref R2 = new Ref(P2, null); static final Value V = new Value(P1, L1, R1, "value"); + // Instances to test, classes of each instance are tested too static Stream identitiesData() { + Function lambda1 = (a) -> "xyz"; return Stream.of( - Arguments.of(new Object(), true, false), + Arguments.of(lambda1, true, false), // a lambda (Identity for now) + Arguments.of(new Object(), true, false), // java.lang.Object Arguments.of("String", true, false), - Arguments.of(String.class, true, false), - Arguments.of(Object.class, true, false), Arguments.of(L1, false, true), Arguments.of(V, false, true), Arguments.of(new ValueRecord(1, "B"), false, true), @@ -128,27 +133,54 @@ static Stream identitiesData() { ); } + // Classes to test + static Stream classesData() { + return Stream.of( + Arguments.of(int.class, false, true), // Fabricated primitive classes + Arguments.of(long.class, false, true), + Arguments.of(short.class, false, true), + Arguments.of(byte.class, false, true), + Arguments.of(float.class, false, true), + Arguments.of(double.class, false, true), + Arguments.of(char.class, false, true), + Arguments.of(void.class, false, true), + Arguments.of(String.class, true, false), + Arguments.of(Object.class, true, false), + Arguments.of(Function.class, false, true), // Interface + Arguments.of(Optional.class, false, true), // Concrete value classes... + Arguments.of(Character.class, false, true) + ); + } + @ParameterizedTest @MethodSource("identitiesData") public void identityTests(Object obj, boolean identityClass, boolean valueClass) { Class clazz = obj.getClass(); + assertEquals(identityClass, Objects.hasIdentity(obj), "Objects.hasIdentity(" + obj + ")"); - if (clazz == Object.class) { - assertTrue(Objects.hasIdentity(obj), "Objects.hasIdentity()"); - } else { - assertEquals(identityClass, Objects.hasIdentity(obj), "Objects.hasIdentity()"); - } + // Run tests on the class + classTests(clazz, identityClass, valueClass); + } + + @ParameterizedTest + @MethodSource("classesData") + public void classTests(Class clazz, boolean identityClass, boolean valueClass) { + assertEquals(identityClass, clazz.isIdentity(), "Class.isIdentity(): " + clazz); - assertEquals(identityClass, clazz.isIdentity(), "Class.isIdentity()"); + assertEquals(valueClass, clazz.isValue(), "Class.isValue(): " + clazz); - assertEquals(valueClass, clazz.isValue(), "Class.isValue()"); + assertEquals(clazz.accessFlags().contains(AccessFlag.IDENTITY), + identityClass, "AccessFlag.IDENTITY: " + clazz); - // JDK-8294866: Not yet implemented checks of AccessFlags for the array class -// assertEquals(clazz.accessFlags().contains(AccessFlag.IDENTITY), -// identityClass, "AccessFlag.IDENTITY"); -// -// assertEquals(clazz.accessFlags().contains(AccessFlag.VALUE), -// valueClass, "AccessFlag.VALUE"); + int modifiers = clazz.getModifiers(); + assertEquals(clazz.isIdentity(), (modifiers & Modifier.IDENTITY) != 0, "Class.getModifiers() & IDENTITY != 0"); + assertEquals(clazz.isValue(), (modifiers & Modifier.IDENTITY) == 0, "Class.getModifiers() & IDENTITY == 0"); + } + + @Test + public void identityTestNull() { + assertFalse(Objects.hasIdentity(null), "Objects.hasIdentity(null)"); + assertFalse(Objects.isValueObject(null), "Objects.isValueObject(null)"); } static Stream equalsTests() {