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 }