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 -Dvalue.bsm.salt=1 ObjectMethods
 30  * @run junit/othervm -Dvalue.bsm.salt=1 -XX:-UseFieldFlattening ObjectMethods
 31  */
 32 import java.util.List;
 33 import java.util.Objects;
 34 import java.util.stream.Stream;
 35 
 36 import jdk.internal.value.ValueClass;
 37 import jdk.internal.vm.annotation.NullRestricted;
 38 import jdk.internal.vm.annotation.Strict;
 39 import org.junit.jupiter.api.Test;
 40 import org.junit.jupiter.params.ParameterizedTest;
 41 import org.junit.jupiter.params.provider.Arguments;
 42 import org.junit.jupiter.params.provider.MethodSource;
 43 
 44 import static org.junit.jupiter.api.Assertions.*;
 45 
 46 public class ObjectMethods {
 47     static value class Point {
 48         public int x;
 49         public int y;
 50         Point(int x, int y) {
 51             this.x = x;
 52             this.y = y;
 53         }
 54     }
 55 
 56     static value class Line {
 57         @NullRestricted  @Strict
 58         Point p1;
 59         @NullRestricted  @Strict
 60         Point p2;
 61 
 62         Line(int x1, int y1, int x2, int y2) {
 63             this.p1 = new Point(x1, y1);
 64             this.p2 = new Point(x2, y2);
 65         }
 66     }
 67 
 68     static class Ref {
 69         @NullRestricted  @Strict
 70         Point p;
 71         Line l;
 72         Ref(Point p, Line l) {
 73             this.p = p;
 74             this.l = l;
 75         }
 76     }
 77 
 78     static value class Value {
 79         @NullRestricted  @Strict
 80         Point p;
 81         @NullRestricted  @Strict
 82         Line l;
 83         Ref r;
 84         String s;
 85         Value(Point p, Line l, Ref r, String s) {
 86             this.p = p;
 87             this.l = l;
 88             this.r = r;
 89             this.s = s;
 90         }
 91     }
 92 
 93     static value class ValueOptional {
 94         private Object o;
 95         public ValueOptional(Object o) {
 96             this.o = o;
 97         }
 98     }
 99 
100     static value record ValueRecord(int i, String name) {}
101 
102     static final int SALT = 1;
103     static final Point P1 = new Point(1, 2);
104     static final Point P2 = new Point(30, 40);
105     static final Line L1 = new Line(1, 2, 3, 4);
106     static final Line L2 = new Line(10, 20, 3, 4);
107     static final Ref R1 = new Ref(P1, L1);
108     static final Ref R2 = new Ref(P2, null);
109     static final Value V = new Value(P1, L1, R1, "value");
110 
111     static Stream<Arguments> identitiesData() {
112         return Stream.of(
113                 Arguments.of(new Object(), true, false),
114                 Arguments.of("String", true, false),
115                 Arguments.of(String.class, true, false),
116                 Arguments.of(Object.class, true, false),
117                 Arguments.of(L1, false, true),
118                 Arguments.of(V, false, true),
119                 Arguments.of(new ValueRecord(1, "B"), false, true),
120                 Arguments.of(new int[0], true, false),     // arrays of primitive type are identity objects
121                 Arguments.of(new Object[0], true, false),  // arrays of identity classes are identity objects
122                 Arguments.of(new String[0], true, false),  // arrays of identity classes are identity objects
123                 Arguments.of(new Value[0], true, false)    // arrays of value classes are identity objects
124         );
125     }
126 
127     @ParameterizedTest
128     @MethodSource("identitiesData")
129     public void identityTests(Object obj, boolean identityClass, boolean valueClass) {
130         Class<?> clazz = obj.getClass();
131 
132         if (clazz == Object.class) {
133             assertTrue(Objects.hasIdentity(obj), "Objects.hasIdentity()");
134         } else {
135             assertEquals(identityClass, Objects.hasIdentity(obj), "Objects.hasIdentity()");
136         }
137 
138         assertEquals(identityClass, clazz.isIdentity(), "Class.isIdentity()");
139 
140         assertEquals(valueClass, clazz.isValue(), "Class.isValue()");
141 
142         // JDK-8294866: Not yet implemented checks of AccessFlags for the array class
143 //        assertEquals(clazz.accessFlags().contains(AccessFlag.IDENTITY),
144 //                identityClass, "AccessFlag.IDENTITY");
145 //
146 //        assertEquals(clazz.accessFlags().contains(AccessFlag.VALUE),
147 //                valueClass, "AccessFlag.VALUE");
148     }
149 
150     static Stream<Arguments> equalsTests() {
151         return Stream.of(
152                 Arguments.of(P1, P1, true),
153                 Arguments.of(P1, new Point(1, 2), true),
154                 Arguments.of(P1, P2, false),
155                 Arguments.of(P1, L1, false),
156                 Arguments.of(L1, new Line(1, 2, 3, 4), true),
157                 Arguments.of(L1, L2, false),
158                 Arguments.of(L1, L1, true),
159                 Arguments.of(V, new Value(P1, L1, R1, "value"), true),
160                 Arguments.of(V, new Value(new Point(1, 2), new Line(1, 2, 3, 4), R1, "value"), true),
161                 Arguments.of(V, new Value(P1, L1, new Ref(P1, L1), "value"), false),
162                 Arguments.of(new Value(P1, L1, R2, "value2"), new Value(P1, L1, new Ref(P2, null), "value2"), false),
163                 Arguments.of(new ValueRecord(50, "fifty"), new ValueRecord(50, "fifty"), true),
164 
165                 // reference classes containing fields of value class
166                 Arguments.of(R1, new Ref(P1, L1), false),   // identity object
167 
168                 // uninitialized default value
169                 Arguments.of(new ValueOptional(L1), new ValueOptional(L1), true),
170                 Arguments.of(new ValueOptional(List.of(P1)), new ValueOptional(List.of(P1)), false)
171         );
172     }
173 
174     @ParameterizedTest
175     @MethodSource("equalsTests")
176     public void testEquals(Object o1, Object o2, boolean expected) {
177         assertTrue(o1.equals(o2) == expected);
178     }
179 
180     static Stream<Arguments> toStringTests() {
181         return Stream.of(
182                 Arguments.of(new Point(100, 200)),
183                 Arguments.of(new Line(1, 2, 3, 4)),
184                 Arguments.of(V),
185                 Arguments.of(R1),
186                 // enclosing instance field `this$0` should be filtered
187                 Arguments.of(new Value(P1, L1, null, null)),
188                 Arguments.of(new Value(P2, L2, new Ref(P1, null), "value")),
189                 Arguments.of(new ValueOptional(P1))
190         );
191     }
192 
193     @ParameterizedTest
194     @MethodSource("toStringTests")
195     public void testToString(Object o) {
196         String expected = String.format("%s@%s", o.getClass().getName(), Integer.toHexString(o.hashCode()));
197         assertEquals(o.toString(), expected);
198     }
199 
200     @Test
201     public void testValueRecordToString() {
202         ValueRecord o = new ValueRecord(30, "thirty");
203         assertEquals(o.toString(), "ValueRecord[i=30, name=thirty]");
204     }
205 
206     static Stream<Arguments> hashcodeTests() {
207         Point p = new Point(0, 0);
208         Line l = new Line(0, 0, 0, 0);
209         // this is sensitive to the order of the returned fields from Class::getDeclaredFields
210         return Stream.of(
211                 Arguments.of(P1, hash(Point.class, 1, 2)),
212                 Arguments.of(L1, hash(Line.class, new Point(1, 2), new Point(3, 4))),
213                 Arguments.of(V, hash(Value.class, P1, L1, V.r, V.s)),
214                 Arguments.of(new Point(0, 0), hash(Point.class, 0, 0)),
215                 Arguments.of(p, hash(Point.class, 0, 0)),
216                 Arguments.of(new ValueOptional(P1), hash(ValueOptional.class, P1))
217         );
218     }
219 
220     @ParameterizedTest
221     @MethodSource("hashcodeTests")
222     public void testHashCode(Object o, int hash) {
223         assertEquals(o.hashCode(), hash);
224         assertEquals(System.identityHashCode(o), hash);
225     }
226 
227     private static int hash(Object... values) {
228         int hc = SALT;
229         for (Object o : values) {
230             hc = 31 * hc + (o != null ? o.hashCode() : 0);
231         }
232         return hc;
233     }
234 
235     interface Number {
236         int value();
237     }
238 
239     static class ReferenceType implements Number {
240         int i;
241         public ReferenceType(int i) {
242             this.i = i;
243         }
244         public int value() {
245             return i;
246         }
247         @Override
248         public boolean equals(Object o) {
249             if (o != null && o instanceof Number) {
250                 return this.value() == ((Number)o).value();
251             }
252             return false;
253         }
254     }
255 
256     static value class ValueType1 implements Number {
257         int i;
258         public ValueType1(int i) {
259             this.i = i;
260         }
261         public int value() {
262             return i;
263         }
264     }
265 
266     static value class ValueType2 implements Number {
267         int i;
268         public ValueType2(int i) {
269             this.i = i;
270         }
271         public int value() {
272             return i;
273         }
274         @Override
275         public boolean equals(Object o) {
276             if (o != null && o instanceof Number) {
277                 return this.value() == ((Number)o).value();
278             }
279             return false;
280         }
281     }
282 
283     static Stream<Arguments> interfaceEqualsTests() {
284         return Stream.of(
285                 Arguments.of(new ReferenceType(10), new ReferenceType(10), false, true),
286                 Arguments.of(new ValueType1(10),    new ValueType1(10),    true,  true),
287                 Arguments.of(new ValueType2(10),    new ValueType2(10),    true,  true),
288                 Arguments.of(new ValueType1(20),    new ValueType2(20),    false, false),
289                 Arguments.of(new ValueType2(20),    new ValueType1(20),    false, true),
290                 Arguments.of(new ReferenceType(30), new ValueType1(30),    false, true),
291                 Arguments.of(new ReferenceType(30), new ValueType2(30),    false, true)
292         );
293     }
294 
295     @ParameterizedTest
296     @MethodSource("interfaceEqualsTests")
297     public void testNumber(Number n1, Number n2, boolean isSubstitutable, boolean isEquals) {
298         assertTrue((n1 == n2) == isSubstitutable);
299         assertTrue(n1.equals(n2) == isEquals);
300     }
301 }