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:+UseFieldFlattening ValueObjectMethodsTest
32 * @run junit/othervm -XX:+UseAtomicValueFlattening 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, 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 & ClassFile.ACC_IDENTITY) != 0, "Class.getModifiers() & ACC_IDENTITY != 0");
173 assertEquals(clazz.isValue(), (modifiers & ClassFile.ACC_IDENTITY) == 0, "Class.getModifiers() & ACC_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(10, 20, 30, 40),
270 List.of(0x0001000100020002L, 0x0002000200040004L, 0x0008000800100010L, 0x0020002000400040L),
271 List.of(p1, p2, p3, p4),
272 List.of(l1, l2, l3, l4, l5),
273 List.of(r1, r2, r3, r4),
274 List.of(v1, v2, v3, v4, v5),
275 List.of(vo1, vo2)
276 );
277 }
278
279 @ParameterizedTest
280 @MethodSource("hashcodeTests")
281 public void testHashCode(List<Object> objects) {
282 assertTrue(objects.size() > 1, "More than one object is required: " + objects);
283
284 long count = objects.stream().map(System::identityHashCode).distinct().count();
285 assertEquals(objects.size(), count, "System.identityHashCode must not repeat: " + objects);
286 count = objects.stream().map(Object::hashCode).distinct().count();
287 assertEquals(objects.size(), count, "Object.hashCode must not repeat: " + objects);
288 }
289
290 interface Number {
291 int value();
292 }
293
294 static class ReferenceType implements Number {
295 int i;
296 public ReferenceType(int i) {
297 this.i = i;
298 }
299 public int value() {
300 return i;
301 }
302 @Override
303 public boolean equals(Object o) {
304 if (o instanceof Number) {
305 return this.value() == ((Number)o).value();
306 }
307 return false;
308 }
309 }
310
311 static value class ValueType1 implements Number {
312 int i;
313 public ValueType1(int i) {
314 this.i = i;
315 }
316 public int value() {
317 return i;
318 }
319 }
320
321 static value class ValueType2 implements Number {
322 int i;
323 public ValueType2(int i) {
324 this.i = i;
325 }
326 public int value() {
327 return i;
328 }
329 @Override
330 public boolean equals(Object o) {
331 if (o instanceof Number) {
332 return this.value() == ((Number)o).value();
333 }
334 return false;
335 }
336 }
337
338 static Stream<Arguments> interfaceEqualsTests() {
339 return Stream.of(
340 Arguments.of(new ReferenceType(10), new ReferenceType(10), false, true),
341 Arguments.of(new ValueType1(10), new ValueType1(10), true, true),
342 Arguments.of(new ValueType2(10), new ValueType2(10), true, true),
343 Arguments.of(new ValueType1(20), new ValueType2(20), false, false),
344 Arguments.of(new ValueType2(20), new ValueType1(20), false, true),
345 Arguments.of(new ReferenceType(30), new ValueType1(30), false, true),
346 Arguments.of(new ReferenceType(30), new ValueType2(30), false, true)
347 );
348 }
349
350 @ParameterizedTest
351 @MethodSource("interfaceEqualsTests")
352 public void testNumber(Number n1, Number n2, boolean isSubstitutable, boolean isEquals) {
353 assertEquals(isSubstitutable, (n1 == n2));
354 assertEquals(isEquals, n1.equals(n2));
355 }
356 }