1 /*
  2  * Copyright (c) 2018, 2025, 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  * @summary test Object methods on value classes
 28  * @enablePreview
 29  * @run junit/othervm -XX:-UseFieldFlattening ValueObjectMethodsTest
 30  * @run junit/othervm -XX:-UseAtomicValueFlattening ValueObjectMethodsTest
 31  * @run junit/othervm -XX:+UseFieldFlattening ValueObjectMethodsTest
 32  * @run junit/othervm -XX:+UseAtomicValueFlattening ValueObjectMethodsTest
 33  */
 34 import java.util.Optional;
 35 import java.util.List;
 36 import java.util.Objects;
 37 import java.util.function.Function;
 38 import java.util.stream.Stream;
 39 import java.lang.reflect.AccessFlag;
 40 import java.lang.reflect.Modifier;
 41 
 42 import jdk.internal.vm.annotation.NullRestricted;
 43 import jdk.internal.vm.annotation.Strict;
 44 import org.junit.jupiter.api.Test;
 45 import org.junit.jupiter.params.ParameterizedTest;
 46 import org.junit.jupiter.params.provider.Arguments;
 47 import org.junit.jupiter.params.provider.MethodSource;
 48 
 49 import static org.junit.jupiter.api.Assertions.*;
 50 
 51 public class ValueObjectMethodsTest {
 52     static value class Point {
 53         public int x;
 54         public int y;
 55         Point(int x, int y) {
 56             this.x = x;
 57             this.y = y;
 58         }
 59     }
 60 
 61     static value class Line {
 62         @NullRestricted  @Strict
 63         Point p1;
 64         @NullRestricted  @Strict
 65         Point p2;
 66 
 67         Line(int x1, int y1, int x2, int y2) {
 68             this.p1 = new Point(x1, y1);
 69             this.p2 = new Point(x2, y2);
 70         }
 71     }
 72 
 73     static class Ref {
 74         @NullRestricted  @Strict
 75         Point p;
 76         Line l;
 77         Ref(Point p, Line l) {
 78             this.p = p;
 79             this.l = l;
 80         }
 81     }
 82 
 83     static value class Value {
 84         @NullRestricted  @Strict
 85         Point p;
 86         @NullRestricted  @Strict
 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, true),       // Fabricated primitive classes
136                 Arguments.of(long.class, false, true),
137                 Arguments.of(short.class, false, true),
138                 Arguments.of(byte.class, false, true),
139                 Arguments.of(float.class, false, true),
140                 Arguments.of(double.class, false, true),
141                 Arguments.of(char.class, false, true),
142                 Arguments.of(void.class, false, true),
143                 Arguments.of(String.class, true, false),
144                 Arguments.of(Object.class, true, false),
145                 Arguments.of(Function.class, false, true),  // 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(identityClass, clazz.isIdentity(), "Class.isIdentity(): " + clazz);
165 
166         assertEquals(valueClass, clazz.isValue(), "Class.isValue(): " + clazz);
167 
168         assertEquals(clazz.accessFlags().contains(AccessFlag.IDENTITY),
169                 identityClass, "AccessFlag.IDENTITY: " + clazz);
170 
171         int modifiers = clazz.getModifiers();
172         assertEquals(clazz.isIdentity(), (modifiers & Modifier.IDENTITY) != 0, "Class.getModifiers() & IDENTITY != 0");
173         assertEquals(clazz.isValue(), (modifiers & Modifier.IDENTITY) == 0, "Class.getModifiers() & IDENTITY == 0");
174     }
175 
176     @Test
177     public void identityTestNull() {
178         assertFalse(Objects.hasIdentity(null), "Objects.hasIdentity(null)");
179         assertFalse(Objects.isValueObject(null), "Objects.isValueObject(null)");
180     }
181 
182     static Stream<Arguments> equalsTests() {
183         return Stream.of(
184                 Arguments.of(P1, P1, true),
185                 Arguments.of(P1, new Point(1, 2), true),
186                 Arguments.of(P1, P2, false),
187                 Arguments.of(P1, L1, false),
188                 Arguments.of(L1, new Line(1, 2, 3, 4), true),
189                 Arguments.of(L1, L2, false),
190                 Arguments.of(L1, L1, true),
191                 Arguments.of(V, new Value(P1, L1, R1, "value"), true),
192                 Arguments.of(V, new Value(new Point(1, 2), new Line(1, 2, 3, 4), R1, "value"), true),
193                 Arguments.of(V, new Value(P1, L1, new Ref(P1, L1), "value"), false),
194                 Arguments.of(new Value(P1, L1, R2, "value2"), new Value(P1, L1, new Ref(P2, null), "value2"), false),
195                 Arguments.of(new ValueRecord(50, "fifty"), new ValueRecord(50, "fifty"), true),
196 
197                 // reference classes containing fields of value class
198                 Arguments.of(R1, new Ref(P1, L1), false),   // identity object
199 
200                 // uninitialized default value
201                 Arguments.of(new ValueOptional(L1), new ValueOptional(L1), true),
202                 Arguments.of(new ValueOptional(List.of(P1)), new ValueOptional(List.of(P1)), false)
203         );
204     }
205 
206     @ParameterizedTest
207     @MethodSource("equalsTests")
208     public void testEquals(Object o1, Object o2, boolean expected) {
209         assertEquals(expected, o1.equals(o2), "equality");
210         if (expected) {
211             // If the values are equals, then the hashcode should equal
212             assertEquals(o1.hashCode(), o2.hashCode(), "obj.hashCode");
213             assertEquals(System.identityHashCode(o1), System.identityHashCode(o2), "System.identityHashCode");
214         }
215     }
216 
217     static Stream<Arguments> toStringTests() {
218         return Stream.of(
219                 Arguments.of(new Point(100, 200)),
220                 Arguments.of(new Line(1, 2, 3, 4)),
221                 Arguments.of(V),
222                 Arguments.of(R1),
223                 // enclosing instance field `this$0` should be filtered
224                 Arguments.of(new Value(P1, L1, null, null)),
225                 Arguments.of(new Value(P2, L2, new Ref(P1, null), "value")),
226                 Arguments.of(new ValueOptional(P1))
227         );
228     }
229 
230     @ParameterizedTest
231     @MethodSource("toStringTests")
232     public void testToString(Object o) {
233         String expected = String.format("%s@%s", o.getClass().getName(), Integer.toHexString(o.hashCode()));
234         assertEquals(o.toString(), expected);
235     }
236 
237     @Test
238     public void testValueRecordToString() {
239         ValueRecord o = new ValueRecord(30, "thirty");
240         assertEquals("ValueRecord[i=30, name=thirty]", o.toString());
241     }
242 
243     static Stream<List<Object>> hashcodeTests() {
244         Point p1 = new Point(0, 1);
245         Point p2 = new Point(0, 2);
246         Point p3 = new Point(1, 1);
247         Point p4 = new Point(2, 2);
248 
249         Line l1 = new Line(0, 1, 2, 3);
250         Line l2 = new Line(9, 1, 2, 3);
251         Line l3 = new Line(0, 9, 2, 3);
252         Line l4 = new Line(0, 1, 9, 3);
253         Line l5 = new Line(0, 1, 2, 9);
254 
255         Ref r1 = new Ref(p1, l1);
256         Ref r2 = new Ref(p1, l2);
257         Ref r3 = new Ref(p2, l1);
258         Ref r4 = new Ref(p2, l2);
259         Value v1 = new Value(p1, l1, r1, "s1");
260         Value v2 = new Value(p2, l1, r1, "s1");
261         Value v3 = new Value(p1, l2, r1, "s1");
262         Value v4 = new Value(p1, l1, r2, "s1");
263         Value v5 = new Value(p1, l1, r1, "s2");
264         ValueOptional vo1 = new ValueOptional(p1);
265         ValueOptional vo2 = new ValueOptional(p2);
266 
267         // Each list has objects that differ from each other so the hashCodes must differ too
268         return Stream.of(
269                 List.of(p1, p2, p3, p4),
270                 List.of(l1, l2, l3, l4, l5),
271                 List.of(r1, r2, r3, r4),
272                 List.of(v1, v2, v3, v4, v5),
273                 List.of(vo1, vo2)
274         );
275     }
276 
277     @ParameterizedTest
278     @MethodSource("hashcodeTests")
279     public void testHashCode(List<Object> objects) {
280         assertTrue(objects.size() > 1, "More than one object is required: " + objects);
281 
282         long count = objects.stream().map(System::identityHashCode).distinct().count();
283         assertEquals(objects.size(), count, "System.identityHashCode must not repeat: " + objects);
284         count = objects.stream().map(Object::hashCode).distinct().count();
285         assertEquals(objects.size(), count, "Object.hashCode must not repeat: "  + objects);
286     }
287 
288     interface Number {
289         int value();
290     }
291 
292     static class ReferenceType implements Number {
293         int i;
294         public ReferenceType(int i) {
295             this.i = i;
296         }
297         public int value() {
298             return i;
299         }
300         @Override
301         public boolean equals(Object o) {
302             if (o instanceof Number) {
303                 return this.value() == ((Number)o).value();
304             }
305             return false;
306         }
307     }
308 
309     static value class ValueType1 implements Number {
310         int i;
311         public ValueType1(int i) {
312             this.i = i;
313         }
314         public int value() {
315             return i;
316         }
317     }
318 
319     static value class ValueType2 implements Number {
320         int i;
321         public ValueType2(int i) {
322             this.i = i;
323         }
324         public int value() {
325             return i;
326         }
327         @Override
328         public boolean equals(Object o) {
329             if (o instanceof Number) {
330                 return this.value() == ((Number)o).value();
331             }
332             return false;
333         }
334     }
335 
336     static Stream<Arguments> interfaceEqualsTests() {
337         return Stream.of(
338                 Arguments.of(new ReferenceType(10), new ReferenceType(10), false, true),
339                 Arguments.of(new ValueType1(10),    new ValueType1(10),    true,  true),
340                 Arguments.of(new ValueType2(10),    new ValueType2(10),    true,  true),
341                 Arguments.of(new ValueType1(20),    new ValueType2(20),    false, false),
342                 Arguments.of(new ValueType2(20),    new ValueType1(20),    false, true),
343                 Arguments.of(new ReferenceType(30), new ValueType1(30),    false, true),
344                 Arguments.of(new ReferenceType(30), new ValueType2(30),    false, true)
345         );
346     }
347 
348     @ParameterizedTest
349     @MethodSource("interfaceEqualsTests")
350     public void testNumber(Number n1, Number n2, boolean isSubstitutable, boolean isEquals) {
351         assertEquals(isSubstitutable, (n1 == n2));
352         assertEquals(isEquals, n1.equals(n2));
353     }
354 }