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