1 /* 2 * Copyright (c) 2016, 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 jdk.experimental.value; 27 28 import java.lang.invoke.MethodHandle; 29 import java.lang.invoke.MethodHandles; 30 import java.lang.invoke.MethodHandles.Lookup; 31 import java.lang.invoke.MethodType; 32 import java.lang.reflect.Field; 33 import java.lang.reflect.Modifier; 34 import java.util.Arrays; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Objects; 38 import java.util.concurrent.ConcurrentHashMap; 39 import java.util.function.Consumer; 40 import java.util.function.Supplier; 41 import java.util.stream.Collectors; 42 import java.util.stream.IntStream; 43 import java.util.stream.Stream; 44 45 import jdk.experimental.bytecode.AnnotationsBuilder.Kind; 46 import jdk.experimental.bytecode.Flag; 47 import jdk.experimental.bytecode.Opcode; 48 import jdk.experimental.value.MethodHandleBuilder.IsolatedMethodBuilder; 49 import jdk.experimental.value.MethodHandleBuilder.MethodHandleCodeBuilder; 50 import jdk.experimental.value.ValueType.ValueHandleKind.ValueHandleKey; 51 import jdk.experimental.bytecode.MacroCodeBuilder.CondKind; 52 import jdk.experimental.bytecode.TypeTag; 53 import jdk.internal.misc.Unsafe; 54 import sun.invoke.util.BytecodeDescriptor; 55 import sun.invoke.util.Wrapper; 56 import valhalla.shady.MinimalValueTypes_1_0; 57 58 // Rough place holder just now... 59 public class ValueType<T> { 60 61 enum ValueHandleKind { 62 BOX("box"), 63 UNBOX("unbox"), 64 DEFAULT("defaultValueConstant"), 65 EQ("substitutabilityTest"), 66 HASH("substitutabilityHashCode"), 67 ARRAYLENGTH("arrayLength"), 68 WITHER("findWither", Lookup.class, String.class, Class.class) { 69 @Override 70 ValueHandleKey key(Object fieldName) { 71 return new ValueHandleKey(this, fieldName); 72 } 73 }, 74 UNREFLECT_WITHERS("unreflectWithers", Lookup.class, boolean.class, Field[].class) { 75 @Override 76 ValueHandleKey key(Object fields) { 77 return new ValueHandleKey(this, fields); 78 } 79 }, 80 NEWARRAY("newArray"), 81 VALOAD("arrayGetter"), 82 VASTORE("arraySetter"), 83 MULTINEWARRAY("newMultiArray", int.class) { 84 @Override 85 ValueHandleKey key(Object dims) { 86 return new ValueHandleKey(this, dims); 87 } 88 }, 89 IDENTITY("identity"), 90 GETTER("findGetter", Lookup.class, String.class, Class.class) { 91 @Override 92 ValueHandleKey key(Object fieldName) { 93 return new ValueHandleKey(this, fieldName); 94 } 95 }; 96 97 final MethodHandle handle; 98 99 ValueHandleKind(String handleName, Class<?>... argtypes) { 100 try { 101 this.handle = MethodHandles.lookup().findVirtual(ValueType.class, handleName, MethodType.methodType(MethodHandle.class, argtypes)); 102 } catch (ReflectiveOperationException ex) { 103 throw new IllegalArgumentException("Cannot initialize value handle key for: " + handleName); 104 } 105 } 106 107 String handleName() { 108 return MethodHandles.lookup().revealDirect(handle).getName(); 109 } 110 111 MethodType handleType() { 112 return MethodHandles.lookup().revealDirect(handle).getMethodType(); 113 } 114 115 ValueHandleKey key() { 116 return new ValueHandleKey(this, null); 117 } 118 119 ValueHandleKey key(Object optArg) { 120 throw new IllegalStateException(); 121 } 122 123 static class ValueHandleKey { 124 ValueHandleKind kind; 125 Object optArg; 126 127 ValueHandleKey(ValueHandleKind kind, Object optArg) { 128 this.kind = kind; 129 this.optArg = optArg; 130 } 131 132 @Override 133 public boolean equals(Object obj) { 134 if (obj instanceof ValueHandleKey) { 135 ValueHandleKey that = (ValueHandleKey)obj; 136 return Objects.equals(kind, that.kind) && 137 Objects.equals(optArg, that.optArg); 138 } else { 139 return false; 140 } 141 } 142 143 @Override 144 public int hashCode() { 145 return Objects.hashCode(kind) * 31 + Objects.hashCode(optArg); 146 } 147 } 148 } 149 150 private static final Lookup IMPL_LOOKUP; 151 152 static { 153 try { 154 Field f = Lookup.class.getDeclaredField("IMPL_LOOKUP"); 155 f.setAccessible(true); 156 IMPL_LOOKUP = (Lookup)f.get(null); 157 } catch (ReflectiveOperationException ex) { 158 throw new AssertionError(ex); 159 } 160 } 161 162 private static final ConcurrentHashMap<Class<?>, ValueType<?>> BOX_TO_VT = new ConcurrentHashMap<>(); 163 164 public static boolean classHasValueType(Class<?> x) { 165 if (!MinimalValueTypes_1_0.isValueCapable(x)) { 166 return false; 167 } 168 return MinimalValueTypes_1_0.getValueTypeClass(x) != null; 169 } 170 171 @SuppressWarnings("unchecked") 172 public static <T> ValueType<T> forClass(Class<T> x) { 173 if (!MinimalValueTypes_1_0.isValueCapable(x)) { 174 throw new IllegalArgumentException("Class " + x + " not a value capable class"); 175 } 176 177 ValueType<T> vt = (ValueType<T>) BOX_TO_VT.get(x); 178 if (vt != null) { 179 return vt; 180 } 181 182 Class<T> valueClass = (Class<T>) MinimalValueTypes_1_0.getValueTypeClass(x); 183 vt = new ValueType<T>(x, valueClass); 184 ValueType<T> old = (ValueType<T>) BOX_TO_VT.putIfAbsent(x, vt); 185 if (old != null) { 186 vt = old; 187 } 188 return vt; 189 } 190 191 public static <T> ValueType<T> make(Lookup lookup, String name, String[] fieldNames, Class<?>... fieldTypes) throws ReflectiveOperationException { 192 if (fieldNames.length != fieldTypes.length) { 193 throw new IllegalArgumentException("Field names length and field types length must match"); 194 } 195 if (!(fieldNames.length > 0)) { 196 throw new IllegalArgumentException("Field length must be greater than zero"); 197 } 198 IsolatedMethodBuilder builder = new IsolatedMethodBuilder(name, lookup); 199 builder.withMajorVersion(53) 200 .withMinorVersion(0) 201 .withSuperclass(Object.class) 202 .withFlags(Flag.ACC_FINAL) 203 .withAnnotation(Kind.RUNTIME_VISIBLE, "Ljvm/internal/value/ValueCapableClass;"); 204 //add fields 205 for (int i = 0 ; i < fieldNames.length ; i++) { 206 builder.withField(fieldNames[i], BytecodeDescriptor.unparse(fieldTypes[i]), F -> F.withFlags(Flag.ACC_FINAL)); 207 } 208 //add constructor 209 String ctype = BytecodeDescriptor.unparseMethod(void.class, fieldTypes); 210 builder.withMethod("<init>", ctype, M -> M.withCode(MethodHandleCodeBuilder::new, C -> { 211 C.aload_0().invokespecial(Object.class, "<init>", "()V", false); 212 int l = 1; 213 for (int i = 0 ; i < fieldNames.length ; i++) { 214 String fType = BytecodeDescriptor.unparse(fieldTypes[i]); 215 C.aload_0().load(l).putfield(builder.thisClass(), fieldNames[i], fType); 216 l += Wrapper.forBasicType(fieldTypes[i]).stackSlots(); 217 } 218 C.return_(); 219 })); 220 //add equals and hashCode 221 builder.withMethod("equals", "(Ljava/lang/Object;)Z", M -> 222 M.withFlags(Flag.ACC_PUBLIC).withCode(MethodHandleCodeBuilder::new, 223 C -> substitutabilityTestBuilder(true, builder.thisClass(), FieldInfo.stream(fieldNames, fieldTypes), C))); 224 builder.withMethod("hashCode", "()I", M -> 225 M.withFlags(Flag.ACC_PUBLIC).withCode(MethodHandleCodeBuilder::new, 226 C -> substitutabilityHashCodeBuilder(builder.thisClass(), FieldInfo.stream(fieldNames, fieldTypes), C))); 227 byte[] barr = builder.build(); 228 MinimalValueTypes_1_0.maybeDump(name, barr); 229 @SuppressWarnings("unchecked") 230 Class<T> vtClass = (Class<T>)lookup.defineClass(barr); 231 return forClass(vtClass); 232 } 233 234 private Lookup boxLookup; 235 private Lookup valueLookup; 236 private Map<ValueHandleKind.ValueHandleKey, MethodHandle> handleMap = new ConcurrentHashMap<>(); 237 238 private ValueType(Class<T> boxClass, Class<T> valueClass) { 239 this.boxLookup = IMPL_LOOKUP.in(boxClass); 240 this.valueLookup = IMPL_LOOKUP.in(valueClass); 241 } 242 243 @SuppressWarnings("unchecked") 244 public Class<T> boxClass() { 245 return (Class<T>)boxLookup.lookupClass(); 246 } 247 248 public Class<?> sourceClass() { 249 return boxClass(); 250 } 251 252 public Class<?> valueClass() { 253 return valueLookup.lookupClass(); 254 } 255 256 public Class<?> arrayValueClass() { 257 return arrayValueClass(1); 258 } 259 260 public Class<?> arrayValueClass(int dims) { 261 String dimsStr = "[[[[[[[[[[[[[[[["; 262 if (dims < 1 || dims > 16) { 263 throw new IllegalArgumentException("cannot create array class for dimension > 16"); 264 } 265 String cn = dimsStr.substring(0, dims) + "Q" + valueClass().getName() + ";"; 266 return MinimalValueTypes_1_0.loadValueTypeClass(boxLookup.lookupClass(), cn); 267 } 268 269 public String toString() { 270 return "ValueType boxClass=" + boxClass() + " valueClass=" + valueClass(); 271 } 272 273 public MethodHandle defaultValueConstant() { 274 ValueHandleKey key = ValueHandleKind.DEFAULT.key(); 275 return getOrLoad(boxLookup, key, 276 () -> MethodType.methodType(valueClass()), 277 C -> C.vdefault(valueClass()).vreturn()); 278 } 279 280 public MethodHandle substitutabilityTest() { 281 ValueHandleKey key = ValueHandleKind.EQ.key(); 282 return getOrLoad(valueLookup, key, 283 () -> MethodType.methodType(boolean.class, valueClass(), valueClass()), 284 C -> substitutabilityTestBuilder(false, valueClass(), FieldInfo.stream(valueFields()), C)); 285 } 286 287 private static <T extends MethodHandleCodeBuilder<T>> void substitutabilityTestBuilder(boolean needsInstanceCheck, Class<?> clazz, Stream<FieldInfo> fInfos, MethodHandleCodeBuilder<T> C) { 288 if (needsInstanceCheck) { 289 C.aload_1() 290 .instanceof_(clazz) 291 .emitCondJump(Opcode.IFEQ, CondKind.EQ, "fail") 292 .aload_1() 293 .checkcast(clazz) 294 .store(1); 295 } 296 fInfos.forEach(fInfo -> { 297 String fDesc = BytecodeDescriptor.unparse(fInfo.getType()); 298 if (fInfo.getType().isPrimitive()) { 299 //field is a primitive type - perform bytecode comparison 300 C.load(0).getfield(clazz, fInfo.getName(), fDesc); 301 C.load(1).getfield(clazz, fInfo.getName(), fDesc); 302 C.ifcmp(fDesc, CondKind.NE, "fail"); 303 } else if (MinimalValueTypes_1_0.isValueType(fInfo.getType())) { 304 //field is a value type - call subst handle recursively 305 C.load(0).getfield(clazz, fInfo.getName(), fDesc).dup().store(2); 306 valueHandleBuilder(fInfo.getType(), ValueHandleKind.EQ.key(), C) 307 .load(2) 308 .load(1).getfield(clazz, fInfo.getName(), fDesc) 309 .invokevirtual(MethodHandle.class, "invoke", 310 MethodType.methodType(boolean.class, fInfo.getType(), fInfo.getType()).toMethodDescriptorString(), false) 311 .const_(0).ifcmp(TypeTag.I, CondKind.EQ, "fail"); 312 } else { 313 //otherwise, field is a reference type, fallback to Objects.equals 314 C.load(0).getfield(clazz, fInfo.getName(), fDesc); 315 C.load(1).getfield(clazz, fInfo.getName(), fDesc); 316 C.invokestatic(Objects.class, "equals", "(Ljava/lang/Object;Ljava/lang/Object;)Z", false); 317 C.const_(0).ifcmp(TypeTag.I, CondKind.EQ, "fail"); 318 } 319 }); 320 C.const_(1); 321 C.ireturn(); 322 C.label("fail"); 323 C.const_(0); 324 C.ireturn(); 325 } 326 327 public MethodHandle substitutabilityHashCode() { 328 ValueHandleKey key = ValueHandleKind.HASH.key(); 329 return getOrLoad(valueLookup, key, 330 () -> MethodType.methodType(int.class, valueClass()), 331 C -> substitutabilityHashCodeBuilder(valueClass(), FieldInfo.stream(valueFields()), C)); 332 } 333 334 private static <T extends MethodHandleCodeBuilder<T>> void substitutabilityHashCodeBuilder(Class<?> clazz, Stream<FieldInfo> fInfos, MethodHandleCodeBuilder<T> C) { 335 C.withLocal("res", "I"); 336 C.const_(1).store("res"); 337 fInfos.forEach(fInfo -> { 338 String desc = BytecodeDescriptor.unparse(fInfo.getType()); 339 if (fInfo.getType().isPrimitive()) { 340 C.load(0).getfield(clazz, fInfo.getName(), desc); 341 C.invokestatic(Wrapper.asWrapperType(fInfo.getType()), "hashCode", "(" + desc + ")I", false); 342 } else if (MinimalValueTypes_1_0.isValueType(fInfo.getType())) { 343 //field is a value type - call subst handle recursively 344 C.load(0).getfield(clazz, fInfo.getName(), desc).dup().store(2); 345 valueHandleBuilder(fInfo.getType(), ValueHandleKind.HASH.key(), C) 346 .load(2) 347 .invokevirtual(MethodHandle.class, "invoke", 348 MethodType.methodType(int.class, fInfo.getType()).toMethodDescriptorString(), false); 349 } else { 350 C.load(0).getfield(clazz, fInfo.getName(), desc); 351 C.invokestatic(Objects.class, "hashCode", "(Ljava/lang/Object;)I", false); 352 } 353 C.load("res").const_(31).imul(); 354 C.iadd().store("res"); 355 }); 356 C.load("res").ireturn(); 357 } 358 359 // ()Q 360 public MethodHandle findConstructor(Lookup lookup, MethodType type) throws NoSuchMethodException, IllegalAccessException { 361 return MethodHandles.filterReturnValue(lookup.findConstructor(boxClass(), type), unbox()); 362 } 363 364 // (F1, ..., Fn)Q, fromDefault == true 365 // (Q, F1, ..., Fn)Q, fromDefault == false 366 public MethodHandle unreflectWithers(Lookup lookup, 367 boolean fromDefault, 368 Field... fields) throws NoSuchFieldException, IllegalAccessException { 369 // Allow access if the lookup class is the VCC or DVT and the lookup 370 // has private access 371 Class<?> lc = lookup.lookupClass(); 372 if (!lookup.hasPrivateAccess() || (valueClass() != lc && boxClass() != lc)) { 373 throw new IllegalAccessException(String.format("Class %s does not have vwithfield access to fields of %s", 374 lc.getName(), boxClass().getName())); 375 } 376 377 // Ensure fields are value component fields declared by the VCC 378 for (Field f : fields) { 379 if (!isValueField(f) || f.getDeclaringClass() != sourceClass()) { 380 throw new IllegalArgumentException( 381 String.format("Field %s is not a value component field declared in value capable class %s", f.getName(), sourceClass().getName())); 382 } 383 } 384 385 ValueHandleKey key = ValueHandleKind.UNREFLECT_WITHERS.key(List.of(fromDefault, 386 FieldInfo.stream(fields).collect(Collectors.toList()))); 387 return getOrLoad(valueLookup, key, 388 () -> { 389 MethodType mt = MethodType.methodType( 390 valueClass(), 391 Stream.of(fields).map(Field::getType).toArray(Class[]::new)); 392 393 if (!fromDefault) { 394 mt = mt.insertParameterTypes(0, valueClass()); 395 } 396 return mt; 397 }, 398 C -> { 399 int l = 0; 400 if (fromDefault) { 401 C.vdefault(valueClass()); 402 } else { 403 C.load(0); 404 l = 1; 405 } 406 407 for (Field f : fields) { 408 String fType = BytecodeDescriptor.unparse(f.getType()); 409 C.load(l).vwithfield(valueClass(), f.getName(), fType); 410 l += Wrapper.forBasicType(f.getType()).stackSlots(); 411 } 412 C.vreturn(); 413 }); 414 } 415 416 // (Q, T)Q 417 public MethodHandle findWither(Lookup lookup, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { 418 // Allow access if the lookup class is the VCC or DVT and the lookup 419 // has private access 420 Class<?> lc = lookup.lookupClass(); 421 if (!lookup.hasPrivateAccess() || (valueClass() != lc && boxClass() != lc)) { 422 throw new IllegalAccessException(String.format("Class %s does not have vwithfield access to field %s.%s", 423 lc.getName(), boxClass().getName(), name)); 424 } 425 426 // Check field exists on VCC 427 lookup.findGetter(boxClass(), name, type); 428 429 ValueHandleKey key = ValueHandleKind.WITHER.key(new FieldInfo(name, type)); 430 return getOrLoad(valueLookup, key, 431 () -> MethodType.methodType(valueClass(), valueClass(), type), 432 C -> C.vload(0).load(1).vwithfield(valueClass(), name, BytecodeDescriptor.unparse(type)).vreturn()); 433 } 434 435 public MethodHandle unbox() { 436 ValueHandleKey key = ValueHandleKind.UNBOX.key(); 437 return getOrLoad(boxLookup, key, 438 () -> MethodType.methodType(valueClass(), boxClass()), 439 C -> C.load(0).vunbox(valueClass()).vreturn()); 440 } 441 442 public MethodHandle box() { 443 ValueHandleKey key = ValueHandleKind.BOX.key(); 444 return getOrLoad(boxLookup, key, 445 () -> MethodType.methodType(boxClass(), valueClass()), 446 C -> C.vload(0).vbox(boxClass()).areturn()); 447 } 448 449 public MethodHandle newArray() { 450 ValueHandleKey key = ValueHandleKind.NEWARRAY.key(); 451 return getOrLoad(boxLookup, key, 452 () -> MethodType.methodType(arrayValueClass(), int.class), 453 C -> C.load(0).anewarray(valueClass()).areturn()); 454 } 455 456 public MethodHandle arrayGetter() { 457 ValueHandleKey key = ValueHandleKind.VALOAD.key(); 458 return getOrLoad(boxLookup, key, 459 () -> MethodType.methodType(valueClass(), arrayValueClass(), int.class), 460 C -> C.load(0).load(1).vaload().vreturn()); 461 } 462 463 public MethodHandle arraySetter() { 464 ValueHandleKey key = ValueHandleKind.VASTORE.key(); 465 return getOrLoad(boxLookup, key, 466 () -> MethodType.methodType(void.class, arrayValueClass(), int.class, valueClass()), 467 C -> C.load(0).load(1).load(2).vastore().return_()); 468 } 469 470 public MethodHandle newMultiArray(int dims) { 471 Class<?> arrayValueClass = arrayValueClass(dims); 472 ValueHandleKey key = ValueHandleKind.MULTINEWARRAY.key(dims); 473 return getOrLoad(boxLookup, key, 474 () -> { 475 Class<?>[] params = new Class<?>[dims]; 476 Arrays.fill(params, int.class); 477 return MethodType.methodType(arrayValueClass, params); 478 }, 479 C -> { 480 for (int i = 0 ; i < dims ; i++) { 481 C.load(i); 482 } 483 C.multianewarray(arrayValueClass, (byte)dims).areturn(); 484 }); 485 } 486 487 public MethodHandle arrayLength() { 488 ValueHandleKey key = ValueHandleKind.ARRAYLENGTH.key(); 489 return getOrLoad(boxLookup, key, 490 () -> MethodType.methodType(int.class, arrayValueClass()), 491 C -> C.load(0).arraylength().ireturn()); 492 } 493 494 public MethodHandle identity() { 495 ValueHandleKey key = ValueHandleKind.IDENTITY.key(); 496 return getOrLoad(boxLookup, key, 497 () -> MethodType.methodType(valueClass(), valueClass()), 498 C -> C.vload(0).vreturn()); 499 } 500 501 public MethodHandle findGetter(Lookup lookup, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { 502 //force access-check 503 lookup.findGetter(boxClass(), name, type); 504 505 ValueHandleKey key = ValueHandleKind.GETTER.key(new FieldInfo(name, type)); 506 String fieldType = BytecodeDescriptor.unparse(type); 507 return getOrLoad(boxLookup, key, 508 () -> MethodType.methodType(type, valueClass()), 509 C -> C.vload(0).getfield(valueClass(), name, fieldType).return_(fieldType)); 510 } 511 512 private static <T extends MethodHandleCodeBuilder<T>> T valueHandleBuilder(Class<?> dvt, ValueHandleKey key, MethodHandleCodeBuilder<T> C) { 513 MethodType mt = key.kind.handleType(); 514 if (mt.parameterCount() > 0) { 515 throw new AssertionError("Non-nilary handle builders not supported yet"); 516 } 517 return C.vbox(MinimalValueTypes_1_0.getValueCapableClass(dvt)) 518 .invokevirtual(Object.class, "getClass", "()Ljava/lang/Class;", false) 519 .invokestatic(ValueType.class, "forClass", 520 MethodType.methodType(ValueType.class, Class.class).toMethodDescriptorString(), false) 521 .invokevirtual(ValueType.class, key.kind.handleName(), key.kind.handleType().toMethodDescriptorString(), false); 522 } 523 524 private MethodHandle getOrLoad(Lookup lookup, ValueHandleKey key, Supplier<MethodType> typeSupplier, Consumer<? super MethodHandleCodeBuilder<?>> codeBuilder) { 525 MethodHandle result = handleMap.get(key); 526 if (result == null) { 527 String handleDebugName = sourceClass().getName() + "_" + key.kind.handleName(); 528 result = MethodHandleBuilder.loadCode(lookup, handleDebugName, typeSupplier.get(), codeBuilder); 529 handleMap.put(key, result); 530 } 531 return result; 532 } 533 534 boolean isValueField(Field f) { 535 return (f.getModifiers() & (Modifier.FINAL | Modifier.STATIC)) == Modifier.FINAL; 536 } 537 538 public Field[] valueFields() { 539 return Stream.of(sourceClass().getDeclaredFields()) 540 .filter(this::isValueField) 541 .toArray(Field[]::new); 542 } 543 544 final static class FieldInfo { 545 546 private final String name; 547 private final Class<?> type; 548 549 FieldInfo(String name, Class<?> type) { 550 this.name = name; 551 this.type = type; 552 } 553 554 String getName() { return name; } 555 Class<?> getType() { return type; } 556 557 @Override 558 public boolean equals(Object o) { 559 if (o instanceof FieldInfo) { 560 FieldInfo that = (FieldInfo)o; 561 return Objects.equals(name, that.name) && 562 Objects.equals(type, that.type); 563 } else { 564 return false; 565 } 566 } 567 568 @Override 569 public int hashCode() { 570 return Objects.hash(name, type); 571 } 572 573 private static Stream<FieldInfo> stream(Field[] fields) { 574 return Stream.of(fields).map(f -> new FieldInfo(f.getName(), f.getType())); 575 } 576 577 private static Stream<FieldInfo> stream(String[] fieldNames, Class<?>[] fieldTypes) { 578 return IntStream.range(0, fieldNames.length) 579 .mapToObj(i -> new FieldInfo(fieldNames[i], fieldTypes[i])); 580 } 581 } 582 }