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 }