1 /*
2 * Copyright (c) 2017, 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. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package java.lang.runtime;
27
28 import java.lang.classfile.ClassFile;
29 import java.lang.classfile.ClassHierarchyResolver;
30 import java.lang.classfile.Opcode;
31 import java.lang.constant.ClassDesc;
32 import java.lang.constant.MethodTypeDesc;
33 import java.lang.invoke.ConstantCallSite;
34 import java.lang.invoke.MethodHandle;
35 import java.lang.invoke.MethodHandles;
36 import java.lang.invoke.MethodType;
37 import java.lang.invoke.StringConcatFactory;
38 import java.lang.invoke.TypeDescriptor;
39 import java.lang.reflect.Modifier;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Objects;
45
46 import static java.lang.classfile.ClassFile.ACC_STATIC;
47 import static java.lang.constant.ConstantDescs.*;
48 import static java.util.Objects.requireNonNull;
49
50 /**
51 * Bootstrap methods for state-driven implementations of core methods,
52 * including {@link Object#equals(Object)}, {@link Object#hashCode()}, and
53 * {@link Object#toString()}. These methods may be used, for example, by
54 * Java compiler implementations to implement the bodies of {@link Object}
55 * methods for record classes.
56 *
57 * @since 16
58 */
59 public final class ObjectMethods {
60
61 private ObjectMethods() { }
62
63 private static final int MAX_STRING_CONCAT_SLOTS = 20;
64
65 private static final MethodHandle FALSE = MethodHandles.zero(boolean.class);
66 private static final MethodHandle TRUE = MethodHandles.constant(boolean.class, true);
67 private static final MethodHandle ZERO = MethodHandles.zero(int.class);
68 private static final MethodHandle CLASS_IS_INSTANCE;
69 private static final MethodHandle IS_NULL;
70 private static final MethodHandle IS_ARG0_NULL;
71 private static final MethodHandle IS_ARG1_NULL;
72 private static final MethodHandle OBJECT_EQ;
73 private static final MethodHandle HASH_COMBINER;
74 private static final MethodType MT_OBJECT_BOOLEAN = MethodType.methodType(boolean.class, Object.class);
75 private static final MethodType MT_INT = MethodType.methodType(int.class);
76 private static final MethodTypeDesc MTD_OBJECT_BOOLEAN = MethodTypeDesc.of(CD_boolean, CD_Object);
77 private static final MethodTypeDesc MTD_INT = MethodTypeDesc.of(CD_int);
78
79 /* package-private */
80 static final HashMap<Class<?>, MethodHandle> primitiveEquals = new HashMap<>();
81
82 private static final HashMap<Class<?>, MethodHandle> primitiveHashers = new HashMap<>();
83
84 static {
85 try {
86 Class<ObjectMethods> OBJECT_METHODS_CLASS = ObjectMethods.class;
87 MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
88 MethodHandles.Lookup lookup = MethodHandles.lookup();
89
90 CLASS_IS_INSTANCE = publicLookup.findVirtual(Class.class, "isInstance",
91 MethodType.methodType(boolean.class, Object.class));
92
93 var objectsIsNull = publicLookup.findStatic(Objects.class, "isNull",
94 MethodType.methodType(boolean.class, Object.class));
95 IS_NULL = objectsIsNull;
96 IS_ARG0_NULL = MethodHandles.dropArguments(objectsIsNull, 1, Object.class);
97 IS_ARG1_NULL = MethodHandles.dropArguments(objectsIsNull, 0, Object.class);
98
99 OBJECT_EQ = lookup.findStatic(OBJECT_METHODS_CLASS, "eq",
100 MethodType.methodType(boolean.class, Object.class, Object.class));
101 HASH_COMBINER = lookup.findStatic(OBJECT_METHODS_CLASS, "hashCombiner",
102 MethodType.methodType(int.class, int.class, int.class));
103
104 primitiveEquals.put(byte.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq",
105 MethodType.methodType(boolean.class, byte.class, byte.class)));
106 primitiveEquals.put(short.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq",
107 MethodType.methodType(boolean.class, short.class, short.class)));
108 primitiveEquals.put(char.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq",
109 MethodType.methodType(boolean.class, char.class, char.class)));
110 primitiveEquals.put(int.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq",
111 MethodType.methodType(boolean.class, int.class, int.class)));
112 primitiveEquals.put(long.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq",
113 MethodType.methodType(boolean.class, long.class, long.class)));
114 primitiveEquals.put(float.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq",
115 MethodType.methodType(boolean.class, float.class, float.class)));
116 primitiveEquals.put(double.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq",
117 MethodType.methodType(boolean.class, double.class, double.class)));
118 primitiveEquals.put(boolean.class, lookup.findStatic(OBJECT_METHODS_CLASS, "eq",
119 MethodType.methodType(boolean.class, boolean.class, boolean.class)));
120
121 primitiveHashers.put(byte.class, lookup.findStatic(Byte.class, "hashCode",
122 MethodType.methodType(int.class, byte.class)));
123 primitiveHashers.put(short.class, lookup.findStatic(Short.class, "hashCode",
124 MethodType.methodType(int.class, short.class)));
125 primitiveHashers.put(char.class, lookup.findStatic(Character.class, "hashCode",
126 MethodType.methodType(int.class, char.class)));
127 primitiveHashers.put(int.class, lookup.findStatic(Integer.class, "hashCode",
128 MethodType.methodType(int.class, int.class)));
129 primitiveHashers.put(long.class, lookup.findStatic(Long.class, "hashCode",
130 MethodType.methodType(int.class, long.class)));
131 primitiveHashers.put(float.class, lookup.findStatic(Float.class, "hashCode",
132 MethodType.methodType(int.class, float.class)));
133 primitiveHashers.put(double.class, lookup.findStatic(Double.class, "hashCode",
134 MethodType.methodType(int.class, double.class)));
135 primitiveHashers.put(boolean.class, lookup.findStatic(Boolean.class, "hashCode",
136 MethodType.methodType(int.class, boolean.class)));
137 }
138 catch (ReflectiveOperationException e) {
139 throw new RuntimeException(e);
140 }
141 }
142
143 private static int hashCombiner(int x, int y) {
144 return x*31 + y;
145 }
146
147 private static boolean eq(Object a, Object b) { return a == b; }
148 private static boolean eq(byte a, byte b) { return a == b; }
149 private static boolean eq(short a, short b) { return a == b; }
150 private static boolean eq(char a, char b) { return a == b; }
151 private static boolean eq(int a, int b) { return a == b; }
152 private static boolean eq(long a, long b) { return a == b; }
153 private static boolean eq(float a, float b) { return Float.compare(a, b) == 0; }
154 private static boolean eq(double a, double b) { return Double.compare(a, b) == 0; }
155 private static boolean eq(boolean a, boolean b) { return a == b; }
156
157 /** Get the method handle for combining two values of a given type */
158 private static MethodHandle equalator(MethodHandles.Lookup lookup, Class<?> clazz) throws Throwable {
159 if (clazz.isPrimitive())
160 return primitiveEquals.get(clazz);
161 MethodType mt = MethodType.methodType(boolean.class, clazz, clazz);
162 return MethodHandles.guardWithTest(IS_ARG0_NULL.asType(mt),
163 IS_ARG1_NULL.asType(mt),
164 lookup.findVirtual(clazz, "equals", MT_OBJECT_BOOLEAN).asType(mt));
165 }
166
167 /** Get the hasher for a value of a given type */
168 private static MethodHandle hasher(MethodHandles.Lookup lookup, Class<?> clazz) throws Throwable {
169 if (clazz.isPrimitive())
170 return primitiveHashers.get(clazz);
171 MethodType mt = MethodType.methodType(int.class, clazz);
172 return MethodHandles.guardWithTest(IS_NULL.asType(MethodType.methodType(boolean.class, clazz)),
173 MethodHandles.dropArguments(MethodHandles.zero(int.class), 0, clazz),
174 lookup.findVirtual(clazz, "hashCode", MT_INT).asType(mt));
175 }
176
177 // If this type must be a monomorphic receiver, that is, one that has no
178 // subtypes in the JVM. For example, Object-typed fields may have a more
179 // specific one type at runtime and thus need optimizations.
180 private static boolean isMonomorphic(Class<?> type) {
181 // Includes primitives and final classes, but not arrays.
182 // All array classes are reported to be final, but Object[] can have subtypes like String[]
183 return Modifier.isFinal(type.getModifiers()) && !type.isArray();
184 }
185
186 private static String specializerClassName(Class<?> targetClass, String kind) {
187 String name = targetClass.getName();
188 if (targetClass.isHidden()) {
189 // use the original class name
190 name = name.replace('/', '_');
191 }
192 return name + "$$" + kind + "Specializer";
193 }
194
195 /**
196 * Generates a method handle for the {@code equals} method for a given data class
197 * @param receiverClass the data class
198 * @param getters the list of getters
199 * @return the method handle
200 */
201 private static MethodHandle makeEquals(MethodHandles.Lookup lookup, Class<?> receiverClass,
202 List<MethodHandle> getters) throws Throwable {
203 MethodType rr = MethodType.methodType(boolean.class, receiverClass, receiverClass);
204 MethodType ro = MethodType.methodType(boolean.class, receiverClass, Object.class);
205 MethodHandle instanceFalse = MethodHandles.dropArguments(FALSE, 0, receiverClass, Object.class); // (RO)Z
206 MethodHandle instanceTrue = MethodHandles.dropArguments(TRUE, 0, receiverClass, Object.class); // (RO)Z
207 MethodHandle isSameObject = OBJECT_EQ.asType(ro); // (RO)Z
208 MethodHandle isInstance = MethodHandles.dropArguments(CLASS_IS_INSTANCE.bindTo(receiverClass), 0, receiverClass); // (RO)Z
209 MethodHandle accumulator = MethodHandles.dropArguments(TRUE, 0, receiverClass, receiverClass); // (RR)Z
210
211 int size = getters.size();
212 MethodHandle[] equalators = new MethodHandle[size];
213 boolean hasPolymorphism = false;
214 for (int i = 0; i < size; i++) {
215 var getter = getters.get(i);
216 var type = getter.type().returnType();
217 if (isMonomorphic(type)) {
218 equalators[i] = equalator(lookup, type);
219 } else {
220 hasPolymorphism = true;
221 }
222 }
223
224 // Currently, hotspot does not support polymorphic inlining.
225 // As a result, if we have a MethodHandle to Object.equals,
226 // it does not enjoy separate profiles like individual invokevirtuals,
227 // and we must spin bytecode to accomplish separate profiling.
228 if (hasPolymorphism) {
229 String[] names = new String[size];
230
231 var classFileContext = ClassFile.of(ClassFile.ClassHierarchyResolverOption.of(ClassHierarchyResolver.ofClassLoading(lookup)));
232 var bytes = classFileContext.build(ClassDesc.of(specializerClassName(lookup.lookupClass(), "Equalator")), clb -> {
233 for (int i = 0; i < size; i++) {
234 if (equalators[i] == null) {
235 var name = "equalator".concat(Integer.toString(i));
236 names[i] = name;
237 var type = getters.get(i).type().returnType();
238 boolean isInterface = type.isInterface();
239 var typeDesc = type.describeConstable().orElseThrow();
240 clb.withMethodBody(name, MethodTypeDesc.of(CD_boolean, typeDesc, typeDesc), ACC_STATIC, cob -> {
241 var nonNullPath = cob.newLabel();
242 var fail = cob.newLabel();
243 cob.aload(0)
244 .ifnonnull(nonNullPath)
245 .aload(1)
246 .ifnonnull(fail)
247 .iconst_1() // arg0 null, arg1 null
248 .ireturn()
249 .labelBinding(fail)
250 .iconst_0() // arg0 null, arg1 non-null
251 .ireturn()
252 .labelBinding(nonNullPath)
253 .aload(0) // arg0.equals(arg1) - bytecode subject to customized profiling
254 .aload(1)
255 .invoke(isInterface ? Opcode.INVOKEINTERFACE : Opcode.INVOKEVIRTUAL, typeDesc, "equals", MTD_OBJECT_BOOLEAN, isInterface)
256 .ireturn();
257 });
258 }
259 }
260 });
261
262 var specializerLookup = lookup.defineHiddenClass(bytes, true, MethodHandles.Lookup.ClassOption.STRONG);
263
264 for (int i = 0; i < size; i++) {
265 if (equalators[i] == null) {
266 var type = getters.get(i).type().returnType();
267 equalators[i] = specializerLookup.findStatic(specializerLookup.lookupClass(), names[i], MethodType.methodType(boolean.class, type, type));
268 }
269 }
270 }
271
272 for (int i = 0; i < size; i++) {
273 var getter = getters.get(i);
274 MethodHandle equalator = equalators[i]; // (TT)Z
275 MethodHandle thisFieldEqual = MethodHandles.filterArguments(equalator, 0, getter, getter); // (RR)Z
276 accumulator = MethodHandles.guardWithTest(thisFieldEqual, accumulator, instanceFalse.asType(rr));
277 }
278
279 return MethodHandles.guardWithTest(isSameObject,
280 instanceTrue,
281 MethodHandles.guardWithTest(isInstance, accumulator.asType(ro), instanceFalse));
282 }
283
284 /**
285 * Generates a method handle for the {@code hashCode} method for a given data class
286 * @param receiverClass the data class
287 * @param getters the list of getters
288 * @return the method handle
289 */
290 private static MethodHandle makeHashCode(MethodHandles.Lookup lookup, Class<?> receiverClass,
291 List<MethodHandle> getters) throws Throwable {
292 MethodHandle accumulator = MethodHandles.dropArguments(ZERO, 0, receiverClass); // (R)I
293
294 int size = getters.size();
295 MethodHandle[] hashers = new MethodHandle[size];
296 boolean hasPolymorphism = false;
297 for (int i = 0; i < size; i++) {
298 var getter = getters.get(i);
299 var type = getter.type().returnType();
300 if (isMonomorphic(type)) {
301 hashers[i] = hasher(lookup, type);
302 } else {
303 hasPolymorphism = true;
304 }
305 }
306
307 // Currently, hotspot does not support polymorphic inlining.
308 // As a result, if we have a MethodHandle to Object.hashCode,
309 // it does not enjoy separate profiles like individual invokevirtuals,
310 // and we must spin bytecode to accomplish separate profiling.
311 if (hasPolymorphism) {
312 String[] names = new String[size];
313
314 var classFileContext = ClassFile.of(ClassFile.ClassHierarchyResolverOption.of(ClassHierarchyResolver.ofClassLoading(lookup)));
315 var bytes = classFileContext.build(ClassDesc.of(specializerClassName(lookup.lookupClass(), "Hasher")), clb -> {
316 for (int i = 0; i < size; i++) {
317 if (hashers[i] == null) {
318 var name = "hasher".concat(Integer.toString(i));
319 names[i] = name;
320 var type = getters.get(i).type().returnType();
321 boolean isInterface = type.isInterface();
322 var typeDesc = type.describeConstable().orElseThrow();
323 clb.withMethodBody(name, MethodTypeDesc.of(CD_int, typeDesc), ACC_STATIC, cob -> {
324 var nonNullPath = cob.newLabel();
325 cob.aload(0)
326 .ifnonnull(nonNullPath)
327 .iconst_0() // null hash is 0
328 .ireturn()
329 .labelBinding(nonNullPath)
330 .aload(0) // arg0.hashCode() - bytecode subject to customized profiling
331 .invoke(isInterface ? Opcode.INVOKEINTERFACE : Opcode.INVOKEVIRTUAL, typeDesc, "hashCode", MTD_INT, isInterface)
332 .ireturn();
333 });
334 }
335 }
336 });
337
338 var specializerLookup = lookup.defineHiddenClass(bytes, true, MethodHandles.Lookup.ClassOption.STRONG);
339
340 for (int i = 0; i < size; i++) {
341 if (hashers[i] == null) {
342 var type = getters.get(i).type().returnType();
343 hashers[i] = specializerLookup.findStatic(specializerLookup.lookupClass(), names[i], MethodType.methodType(int.class, type));
344 }
345 }
346 }
347
348 // @@@ Use loop combinator instead?
349 for (int i = 0; i < size; i++) {
350 var getter = getters.get(i);
351 MethodHandle hasher = hashers[i]; // (T)I
352 MethodHandle hashThisField = MethodHandles.filterArguments(hasher, 0, getter); // (R)I
353 MethodHandle combineHashes = MethodHandles.filterArguments(HASH_COMBINER, 0, accumulator, hashThisField); // (RR)I
354 accumulator = MethodHandles.permuteArguments(combineHashes, accumulator.type(), 0, 0); // adapt (R)I to (RR)I
355 }
356
357 return accumulator;
358 }
359
360 /**
361 * Generates a method handle for the {@code toString} method for a given data class
362 * @param receiverClass the data class
363 * @param getters the list of getters
364 * @param names the names
365 * @return the method handle
366 */
367 private static MethodHandle makeToString(MethodHandles.Lookup lookup,
368 Class<?> receiverClass,
369 MethodHandle[] getters,
370 List<String> names) {
371 assert getters.length == names.size();
372 if (getters.length == 0) {
373 // special case
374 MethodHandle emptyRecordCase = MethodHandles.constant(String.class, receiverClass.getSimpleName() + "[]");
375 emptyRecordCase = MethodHandles.dropArguments(emptyRecordCase, 0, receiverClass); // (R)S
376 return emptyRecordCase;
377 }
378
379 boolean firstTime = true;
380 MethodHandle[] mhs;
381 List<List<MethodHandle>> splits;
382 MethodHandle[] toSplit = getters;
383 int namesIndex = 0;
384 do {
385 /* StringConcatFactory::makeConcatWithConstants can only deal with 200 slots, longs and double occupy two
386 * the rest 1 slot, we need to chop the current `getters` into chunks, it could be that for records with
387 * a lot of components that we need to do a couple of iterations. The main difference between the first
388 * iteration and the rest would be on the recipe
389 */
390 splits = split(toSplit);
391 mhs = new MethodHandle[splits.size()];
392 for (int splitIndex = 0; splitIndex < splits.size(); splitIndex++) {
393 String recipe = "";
394 if (firstTime && splitIndex == 0) {
395 recipe = receiverClass.getSimpleName() + "[";
396 }
397 for (int i = 0; i < splits.get(splitIndex).size(); i++) {
398 recipe += firstTime ? names.get(namesIndex) + "=" + "\1" : "\1";
399 if (firstTime && namesIndex != names.size() - 1) {
400 recipe += ", ";
401 }
402 namesIndex++;
403 }
404 if (firstTime && splitIndex == splits.size() - 1) {
405 recipe += "]";
406 }
407 Class<?>[] concatTypeArgs = new Class<?>[splits.get(splitIndex).size()];
408 // special case: no need to create another getters if there is only one split
409 MethodHandle[] currentSplitGetters = new MethodHandle[splits.get(splitIndex).size()];
410 for (int j = 0; j < splits.get(splitIndex).size(); j++) {
411 concatTypeArgs[j] = splits.get(splitIndex).get(j).type().returnType();
412 currentSplitGetters[j] = splits.get(splitIndex).get(j);
413 }
414 MethodType concatMT = MethodType.methodType(String.class, concatTypeArgs);
415 try {
416 mhs[splitIndex] = StringConcatFactory.makeConcatWithConstants(
417 lookup, "",
418 concatMT,
419 recipe,
420 new Object[0]
421 ).getTarget();
422 mhs[splitIndex] = MethodHandles.filterArguments(mhs[splitIndex], 0, currentSplitGetters);
423 // this will spread the receiver class across all the getters
424 mhs[splitIndex] = MethodHandles.permuteArguments(
425 mhs[splitIndex],
426 MethodType.methodType(String.class, receiverClass),
427 new int[splits.get(splitIndex).size()]
428 );
429 } catch (Throwable t) {
430 throw new RuntimeException(t);
431 }
432 }
433 toSplit = mhs;
434 firstTime = false;
435 } while (splits.size() > 1);
436 return mhs[0];
437 }
438
439 /**
440 * Chops the getters into smaller chunks according to the maximum number of slots
441 * StringConcatFactory::makeConcatWithConstants can chew
442 * @param getters the current getters
443 * @return chunks that won't surpass the maximum number of slots StringConcatFactory::makeConcatWithConstants can chew
444 */
445 private static List<List<MethodHandle>> split(MethodHandle[] getters) {
446 List<List<MethodHandle>> splits = new ArrayList<>();
447
448 int slots = 0;
449
450 // Need to peel, so that neither call has more than acceptable number
451 // of slots for the arguments.
452 List<MethodHandle> cArgs = new ArrayList<>();
453 for (MethodHandle methodHandle : getters) {
454 Class<?> returnType = methodHandle.type().returnType();
455 int needSlots = (returnType == long.class || returnType == double.class) ? 2 : 1;
456 if (slots + needSlots > MAX_STRING_CONCAT_SLOTS) {
457 splits.add(cArgs);
458 cArgs = new ArrayList<>();
459 slots = 0;
460 }
461 cArgs.add(methodHandle);
462 slots += needSlots;
463 }
464
465 // Flush the tail slice
466 if (!cArgs.isEmpty()) {
467 splits.add(cArgs);
468 }
469
470 return splits;
471 }
472
473 /**
474 * Bootstrap method to generate the {@link Object#equals(Object)},
475 * {@link Object#hashCode()}, and {@link Object#toString()} methods, based
476 * on a description of the component names and accessor methods, for either
477 * {@code invokedynamic} call sites or dynamic constant pool entries.
478 *
479 * For more detail on the semantics of the generated methods see the specification
480 * of {@link java.lang.Record#equals(Object)}, {@link java.lang.Record#hashCode()} and
481 * {@link java.lang.Record#toString()}.
482 *
483 *
484 * @param lookup Every bootstrap method is expected to have a {@code lookup}
485 * which usually represents a lookup context with the
486 * accessibility privileges of the caller. This is because
487 * {@code invokedynamic} call sites always provide a {@code lookup}
488 * to the corresponding bootstrap method, but this method just
489 * ignores the {@code lookup} parameter
490 * @param methodName the name of the method to generate, which must be one of
491 * {@code "equals"}, {@code "hashCode"}, or {@code "toString"}
492 * @param type a {@link MethodType} corresponding the descriptor type
493 * for the method, which must correspond to the descriptor
494 * for the corresponding {@link Object} method, if linking
495 * an {@code invokedynamic} call site, or the
496 * constant {@code MethodHandle.class}, if linking a
497 * dynamic constant
498 * @param recordClass the record class hosting the record components
499 * @param names the list of component names, joined into a string
500 * separated by ";", or the empty string if there are no
501 * components. This parameter is ignored if the {@code methodName}
502 * parameter is {@code "equals"} or {@code "hashCode"}
503 * @param getters method handles for the accessor methods for the components
504 * @return a call site if invoked by indy, or a method handle
505 * if invoked by a condy
506 * @throws IllegalArgumentException if the bootstrap arguments are invalid
507 * or inconsistent
508 * @throws NullPointerException if any argument is {@code null} or if any element
509 * in the {@code getters} array is {@code null}
510 * @throws Throwable if any exception is thrown during call site construction
511 */
512 public static Object bootstrap(MethodHandles.Lookup lookup, String methodName, TypeDescriptor type,
513 Class<?> recordClass,
514 String names,
515 MethodHandle... getters) throws Throwable {
516 requireNonNull(lookup);
517 requireNonNull(methodName);
518 requireNonNull(type);
519 requireNonNull(recordClass);
520 requireNonNull(names);
521 requireNonNull(getters);
522 Arrays.stream(getters).forEach(Objects::requireNonNull);
523 MethodType methodType;
524 if (type instanceof MethodType mt) {
525 methodType = mt;
526 if (mt.parameterType(0) != recordClass) {
527 throw new IllegalArgumentException("Bad method type: " + mt);
528 }
529 } else {
530 methodType = null;
531 if (!MethodHandle.class.equals(type))
532 throw new IllegalArgumentException(type.toString());
533 }
534 List<MethodHandle> getterList = List.of(getters);
535 for (MethodHandle getter : getterList) {
536 if (getter.type().parameterType(0) != recordClass) {
537 throw new IllegalArgumentException("Bad receiver type: " + getter);
538 }
539 }
540 MethodHandle handle = switch (methodName) {
541 case "equals" -> {
542 if (methodType != null && !methodType.equals(MethodType.methodType(boolean.class, recordClass, Object.class)))
543 throw new IllegalArgumentException("Bad method type: " + methodType);
544 yield makeEquals(lookup, recordClass, getterList);
545 }
546 case "hashCode" -> {
547 if (methodType != null && !methodType.equals(MethodType.methodType(int.class, recordClass)))
548 throw new IllegalArgumentException("Bad method type: " + methodType);
549 yield makeHashCode(lookup, recordClass, getterList);
550 }
551 case "toString" -> {
552 if (methodType != null && !methodType.equals(MethodType.methodType(String.class, recordClass)))
553 throw new IllegalArgumentException("Bad method type: " + methodType);
554 List<String> nameList = "".equals(names) ? List.of() : List.of(names.split(";"));
555 if (nameList.size() != getterList.size())
556 throw new IllegalArgumentException("Name list and accessor list do not match");
557 yield makeToString(lookup, recordClass, getters, nameList);
558 }
559 default -> throw new IllegalArgumentException(methodName);
560 };
561 return methodType != null ? new ConstantCallSite(handle) : handle;
562 }
563 }