1 /*
  2  * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 
 25 /*
 26  * @test
 27  * @bug 8357373 8370714
 28  * @summary test Object methods on value classes
 29  * @enablePreview
 30  * @run junit/othervm ValueObjectMethodsTest
 31  * @run junit/othervm -XX:+UnlockDiagnosticVMOptions -XX:+UseFieldFlattening ValueObjectMethodsTest
 32  * @run junit/othervm -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -XX:+UseNullFreeAtomicValueFlattening ValueObjectMethodsTest
 33  */
 34 import java.lang.classfile.ClassFile;
 35 import java.util.Optional;
 36 import java.util.List;
 37 import java.util.Objects;
 38 import java.util.function.Function;
 39 import java.util.stream.Stream;
 40 import java.lang.reflect.AccessFlag;
 41 
 42 import jdk.internal.vm.annotation.NullRestricted;
 43 import org.junit.jupiter.api.Test;
 44 import org.junit.jupiter.params.ParameterizedTest;
 45 import org.junit.jupiter.params.provider.Arguments;
 46 import org.junit.jupiter.params.provider.MethodSource;
 47 
 48 import static org.junit.jupiter.api.Assertions.*;
 49 
 50 public class ValueObjectMethodsTest {
 51     static value class Point {
 52         public int x;
 53         public int y;
 54         Point(int x, int y) {
 55             this.x = x;
 56             this.y = y;
 57         }
 58     }
 59 
 60     static value class Line {
 61         @NullRestricted
 62         Point p1;
 63         @NullRestricted
 64         Point p2;
 65 
 66         Line(int x1, int y1, int x2, int y2) {
 67             this.p1 = new Point(x1, y1);
 68             this.p2 = new Point(x2, y2);
 69         }
 70     }
 71 
 72     static class Ref {
 73         @NullRestricted
 74         Point p;
 75         Line l;
 76         Ref(Point p, Line l) {
 77             this.p = p;
 78             this.l = l;
 79             super();
 80         }
 81     }
 82 
 83     static value class Value {
 84         @NullRestricted
 85         Point p;
 86         @NullRestricted
 87         Line l;
 88         Ref r;
 89         String s;
 90         Value(Point p, Line l, Ref r, String s) {
 91             this.p = p;
 92             this.l = l;
 93             this.r = r;
 94             this.s = s;
 95         }
 96     }
 97 
 98     static value class ValueOptional {
 99         private Object o;
100         public ValueOptional(Object o) {
101             this.o = o;
102         }
103     }
104 
105     value record ValueRecord(int i, String name) {}
106 
107     static final Point P1 = new Point(1, 2);
108     static final Point P2 = new Point(30, 40);
109     static final Line L1 = new Line(1, 2, 3, 4);
110     static final Line L2 = new Line(10, 20, 3, 4);
111     static final Ref R1 = new Ref(P1, L1);
112     static final Ref R2 = new Ref(P2, null);
113     static final Value V = new Value(P1, L1, R1, "value");
114 
115     // Instances to test, classes of each instance are tested too
116     static Stream<Arguments> identitiesData() {
117         Function<String, String> lambda1 = (a) -> "xyz";
118         return Stream.of(
119                 Arguments.of(lambda1, true, false),         // a lambda (Identity for now)
120                 Arguments.of(new Object(), true, false),    // java.lang.Object
121                 Arguments.of("String", true, false),
122                 Arguments.of(L1, false, true),
123                 Arguments.of(V, false, true),
124                 Arguments.of(new ValueRecord(1, "B"), false, true),
125                 Arguments.of(new int[0], true, false),     // arrays of primitive type are identity objects
126                 Arguments.of(new Object[0], true, false),  // arrays of identity classes are identity objects
127                 Arguments.of(new String[0], true, false),  // arrays of identity classes are identity objects
128                 Arguments.of(new Value[0], true, false)    // arrays of value classes are identity objects
129         );
130     }
131 
132     // Classes to test
133     static Stream<Arguments> classesData() {
134         return Stream.of(
135                 Arguments.of(int.class, false, false),       // Fabricated primitive classes
136                 Arguments.of(long.class, false, false),
137                 Arguments.of(short.class, false, false),
138                 Arguments.of(byte.class, false, false),
139                 Arguments.of(float.class, false, false),
140                 Arguments.of(double.class, false, false),
141                 Arguments.of(char.class, false, false),
142                 Arguments.of(void.class, false, false),
143                 Arguments.of(String.class, true, false),
144                 Arguments.of(Object.class, true, false),
145                 Arguments.of(Function.class, false, false),  // Interface
146                 Arguments.of(Optional.class, false, true),  // Concrete value classes...
147                 Arguments.of(Character.class, false, true)
148         );
149     }
150 
151     @ParameterizedTest
152     @MethodSource("identitiesData")
153     public void identityTests(Object obj, boolean identityClass, boolean valueClass) {
154         Class<?> clazz = obj.getClass();
155         assertEquals(identityClass, Objects.hasIdentity(obj), "Objects.hasIdentity(" + obj + ")");
156 
157         // Run tests on the class
158         classTests(clazz, identityClass, valueClass);
159     }
160 
161     @ParameterizedTest
162     @MethodSource("classesData")
163     public void classTests(Class<?> clazz, boolean identityClass, boolean valueClass) {
164         assertEquals(valueClass, clazz.isValue(), "Class.isValue(): " + clazz);
165 
166         assertEquals(clazz.accessFlags().contains(AccessFlag.IDENTITY),
167                 identityClass, "AccessFlag.IDENTITY: " + clazz);
168 
169         int modifiers = clazz.getModifiers();
170         assertEquals(identityClass, (modifiers & ClassFile.ACC_IDENTITY) != 0, "Class.getModifiers() & ACC_IDENTITY != 0");
171     }
172 
173     @Test
174     public void identityTestNull() {
175         assertFalse(Objects.hasIdentity(null), "Objects.hasIdentity(null)");
176     }
177 
178     static Stream<Arguments> equalsTests() {
179         return Stream.of(
180                 Arguments.of(P1, P1, true),
181                 Arguments.of(P1, new Point(1, 2), true),
182                 Arguments.of(P1, P2, false),
183                 Arguments.of(P1, L1, false),
184                 Arguments.of(L1, new Line(1, 2, 3, 4), true),
185                 Arguments.of(L1, L2, false),
186                 Arguments.of(L1, L1, true),
187                 Arguments.of(V, new Value(P1, L1, R1, "value"), true),
188                 Arguments.of(V, new Value(new Point(1, 2), new Line(1, 2, 3, 4), R1, "value"), true),
189                 Arguments.of(V, new Value(P1, L1, new Ref(P1, L1), "value"), false),
190                 Arguments.of(new Value(P1, L1, R2, "value2"), new Value(P1, L1, new Ref(P2, null), "value2"), false),
191                 Arguments.of(new ValueRecord(50, "fifty"), new ValueRecord(50, "fifty"), true),
192 
193                 // reference classes containing fields of value class
194                 Arguments.of(R1, new Ref(P1, L1), false),   // identity object
195 
196                 // uninitialized default value
197                 Arguments.of(new ValueOptional(L1), new ValueOptional(L1), true),
198                 Arguments.of(new ValueOptional(List.of(P1)), new ValueOptional(List.of(P1)), false)
199         );
200     }
201 
202     @ParameterizedTest
203     @MethodSource("equalsTests")
204     public void testEquals(Object o1, Object o2, boolean expected) {
205         assertEquals(expected, o1.equals(o2), "equality");
206         if (expected) {
207             // If the values are equals, then the hashcode should equal
208             assertEquals(o1.hashCode(), o2.hashCode(), "obj.hashCode");
209             assertEquals(System.identityHashCode(o1), System.identityHashCode(o2), "System.identityHashCode");
210         }
211     }
212 
213     static Stream<Arguments> toStringTests() {
214         return Stream.of(
215                 Arguments.of(new Point(100, 200)),
216                 Arguments.of(new Line(1, 2, 3, 4)),
217                 Arguments.of(V),
218                 Arguments.of(R1),
219                 // enclosing instance field `this$0` should be filtered
220                 Arguments.of(new Value(P1, L1, null, null)),
221                 Arguments.of(new Value(P2, L2, new Ref(P1, null), "value")),
222                 Arguments.of(new ValueOptional(P1))
223         );
224     }
225 
226     @ParameterizedTest
227     @MethodSource("toStringTests")
228     public void testToString(Object o) {
229         String expected = String.format("%s@%s", o.getClass().getName(), Integer.toHexString(o.hashCode()));
230         assertEquals(o.toString(), expected);
231     }
232 
233     @Test
234     public void testValueRecordToString() {
235         ValueRecord o = new ValueRecord(30, "thirty");
236         assertEquals("ValueRecord[i=30, name=thirty]", o.toString());
237     }
238 
239     static Stream<List<Object>> hashcodeTests() {
240         Point p1 = new Point(0, 1);
241         Point p2 = new Point(0, 2);
242         Point p3 = new Point(1, 1);
243         Point p4 = new Point(2, 2);
244 
245         Line l1 = new Line(0, 1, 2, 3);
246         Line l2 = new Line(9, 1, 2, 3);
247         Line l3 = new Line(0, 9, 2, 3);
248         Line l4 = new Line(0, 1, 9, 3);
249         Line l5 = new Line(0, 1, 2, 9);
250 
251         Ref r1 = new Ref(p1, l1);
252         Ref r2 = new Ref(p1, l2);
253         Ref r3 = new Ref(p2, l1);
254         Ref r4 = new Ref(p2, l2);
255         Value v1 = new Value(p1, l1, r1, "s1");
256         Value v2 = new Value(p2, l1, r1, "s1");
257         Value v3 = new Value(p1, l2, r1, "s1");
258         Value v4 = new Value(p1, l1, r2, "s1");
259         Value v5 = new Value(p1, l1, r1, "s2");
260         ValueOptional vo1 = new ValueOptional(p1);
261         ValueOptional vo2 = new ValueOptional(p2);
262 
263         // Each list has objects that differ from each other so the hashCodes must differ too
264         return Stream.of(
265                 List.of(10, 20, 30, 40),
266                 List.of(0x0001000100020002L, 0x0002000200040004L, 0x0008000800100010L, 0x0020002000400040L),
267                 List.of(p1, p2, p3, p4),
268                 List.of(l1, l2, l3, l4, l5),
269                 List.of(r1, r2, r3, r4),
270                 List.of(v1, v2, v3, v4, v5),
271                 List.of(vo1, vo2)
272         );
273     }
274 
275     @ParameterizedTest
276     @MethodSource("hashcodeTests")
277     public void testHashCode(List<Object> objects) {
278         assertTrue(objects.size() > 1, "More than one object is required: " + objects);
279 
280         long count = objects.stream().map(System::identityHashCode).distinct().count();
281         assertEquals(objects.size(), count, "System.identityHashCode must not repeat: " + objects);
282         count = objects.stream().map(Object::hashCode).distinct().count();
283         assertEquals(objects.size(), count, "Object.hashCode must not repeat: "  + objects);
284     }
285 
286     interface Number {
287         int value();
288     }
289 
290     static class ReferenceType implements Number {
291         int i;
292         public ReferenceType(int i) {
293             this.i = i;
294         }
295         public int value() {
296             return i;
297         }
298         @Override
299         public boolean equals(Object o) {
300             if (o instanceof Number) {
301                 return this.value() == ((Number)o).value();
302             }
303             return false;
304         }
305     }
306 
307     static value class ValueType1 implements Number {
308         int i;
309         public ValueType1(int i) {
310             this.i = i;
311         }
312         public int value() {
313             return i;
314         }
315     }
316 
317     static value class ValueType2 implements Number {
318         int i;
319         public ValueType2(int i) {
320             this.i = i;
321         }
322         public int value() {
323             return i;
324         }
325         @Override
326         public boolean equals(Object o) {
327             if (o instanceof Number) {
328                 return this.value() == ((Number)o).value();
329             }
330             return false;
331         }
332     }
333 
334     static Stream<Arguments> interfaceEqualsTests() {
335         return Stream.of(
336                 Arguments.of(new ReferenceType(10), new ReferenceType(10), false, true),
337                 Arguments.of(new ValueType1(10),    new ValueType1(10),    true,  true),
338                 Arguments.of(new ValueType2(10),    new ValueType2(10),    true,  true),
339                 Arguments.of(new ValueType1(20),    new ValueType2(20),    false, false),
340                 Arguments.of(new ValueType2(20),    new ValueType1(20),    false, true),
341                 Arguments.of(new ReferenceType(30), new ValueType1(30),    false, true),
342                 Arguments.of(new ReferenceType(30), new ValueType2(30),    false, true)
343         );
344     }
345 
346     @ParameterizedTest
347     @MethodSource("interfaceEqualsTests")
348     public void testNumber(Number n1, Number n2, boolean isSubstitutable, boolean isEquals) {
349         assertEquals(isSubstitutable, (n1 == n2));
350         assertEquals(isEquals, n1.equals(n2));
351     }
352 }