diff --git a/byte-buddy-dep/src/main/java/net/bytebuddy/build/HashCodeAndEqualsPlugin.java b/byte-buddy-dep/src/main/java/net/bytebuddy/build/HashCodeAndEqualsPlugin.java index b2bdbdcd0e..6c0cc99644 100644 --- a/byte-buddy-dep/src/main/java/net/bytebuddy/build/HashCodeAndEqualsPlugin.java +++ b/byte-buddy-dep/src/main/java/net/bytebuddy/build/HashCodeAndEqualsPlugin.java @@ -150,7 +150,8 @@ public DynamicType.Builder apply(DynamicType.Builder builder, TypeDescript ? ElementMatchers.none() : ElementMatchers.isSynthetic()) .withIgnoredFields(new ValueMatcher(ValueHandling.Sort.IGNORE)) - .withNonNullableFields(nonNullable(new ValueMatcher(ValueHandling.Sort.REVERSE_NULLABILITY)))); + .withNonNullableFields(nonNullable(new ValueMatcher(ValueHandling.Sort.REVERSE_NULLABILITY))) + .withIdentityFields(isAnnotatedWith(Identity.class))); } if (typeDescription.getDeclaredMethods().filter(isEquals()).isEmpty()) { EqualsMethod equalsMethod = enhance.getValue(ENHANCE_INVOKE_SUPER).load(Enhance.class.getClassLoader()).resolve(Enhance.InvokeSuper.class) @@ -160,6 +161,7 @@ public DynamicType.Builder apply(DynamicType.Builder builder, TypeDescript : ElementMatchers.isSynthetic()) .withIgnoredFields(new ValueMatcher(ValueHandling.Sort.IGNORE)) .withNonNullableFields(nonNullable(new ValueMatcher(ValueHandling.Sort.REVERSE_NULLABILITY))) + .withIdentityFields(isAnnotatedWith(Identity.class)) .withFieldOrder(AnnotationOrderComparator.INSTANCE); if (enhance.getValue(ENHANCE_SIMPLE_COMPARISON_FIRST).resolve(Boolean.class)) { equalsMethod = equalsMethod @@ -465,6 +467,17 @@ enum Sort { int value(); } + /** + * Indicates that a field should be compared by identity. Hash codes are then determined by + * {@link System#identityHashCode(Object)}. Fields that are compared by identity are implicitly null-safe. + */ + @Documented + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface Identity { + /* empty */ + } + /** * A comparator that arranges fields in the order of {@link Sorted}. */ diff --git a/byte-buddy-dep/src/main/java/net/bytebuddy/implementation/EqualsMethod.java b/byte-buddy-dep/src/main/java/net/bytebuddy/implementation/EqualsMethod.java index 252086960a..0d2cd85924 100644 --- a/byte-buddy-dep/src/main/java/net/bytebuddy/implementation/EqualsMethod.java +++ b/byte-buddy-dep/src/main/java/net/bytebuddy/implementation/EqualsMethod.java @@ -78,6 +78,11 @@ public class EqualsMethod implements Implementation { */ private final ElementMatcher.Junction nonNullable; + /** + * A matcher to determine that a field should be considered by its identity. + */ + private final ElementMatcher.Junction identity; + /** * The comparator to apply for ordering fields. */ @@ -89,7 +94,7 @@ public class EqualsMethod implements Implementation { * @param superClassCheck The baseline equality to check. */ protected EqualsMethod(SuperClassCheck superClassCheck) { - this(superClassCheck, TypeCompatibilityCheck.EXACT, none(), none(), NaturalOrderComparator.INSTANCE); + this(superClassCheck, TypeCompatibilityCheck.EXACT, none(), none(), none(), NaturalOrderComparator.INSTANCE); } /** @@ -99,17 +104,20 @@ protected EqualsMethod(SuperClassCheck superClassCheck) { * @param typeCompatibilityCheck The instance type compatibility check. * @param ignored A matcher to filter fields that should not be used for a equality resolution. * @param nonNullable A matcher to determine fields of a reference type that cannot be {@code null}. + * @param identity A matcher to determine that a field should be considered by its identity. * @param comparator The comparator to apply for ordering fields. */ private EqualsMethod(SuperClassCheck superClassCheck, TypeCompatibilityCheck typeCompatibilityCheck, ElementMatcher.Junction ignored, ElementMatcher.Junction nonNullable, + ElementMatcher.Junction identity, Comparator comparator) { this.superClassCheck = superClassCheck; this.typeCompatibilityCheck = typeCompatibilityCheck; this.ignored = ignored; this.nonNullable = nonNullable; + this.identity = identity; this.comparator = comparator; } @@ -139,7 +147,7 @@ public static EqualsMethod isolated() { * @return A new version of this equals method implementation that also ignores any fields matched by the provided matcher. */ public EqualsMethod withIgnoredFields(ElementMatcher ignored) { - return new EqualsMethod(superClassCheck, typeCompatibilityCheck, this.ignored.or(ignored), nonNullable, comparator); + return new EqualsMethod(superClassCheck, typeCompatibilityCheck, this.ignored.or(ignored), nonNullable, identity, comparator); } /** @@ -151,7 +159,17 @@ public EqualsMethod withIgnoredFields(ElementMatcher nonNullable) { - return new EqualsMethod(superClassCheck, typeCompatibilityCheck, ignored, this.nonNullable.or(nonNullable), comparator); + return new EqualsMethod(superClassCheck, typeCompatibilityCheck, ignored, this.nonNullable.or(nonNullable), identity, comparator); + } + + /** + * Returns a new version of this equals method implementation that considers the matched fields by their identity. + * + * @param identity A matcher to determine that a field should be considered by its identity. + * @return A new version of this equals method implementation that also considers the matched fields by their identity. + */ + public EqualsMethod withIdentityFields(ElementMatcher identity) { + return new EqualsMethod(superClassCheck, typeCompatibilityCheck, ignored, nonNullable, this.identity.or(identity), comparator); } /** @@ -199,7 +217,7 @@ public EqualsMethod withStringTypedFieldsFirst() { */ @SuppressWarnings("unchecked") // In absence of @SafeVarargs public EqualsMethod withFieldOrder(Comparator comparator) { - return new EqualsMethod(superClassCheck, typeCompatibilityCheck, ignored, nonNullable, new CompoundComparator(this.comparator, comparator)); + return new EqualsMethod(superClassCheck, typeCompatibilityCheck, ignored, nonNullable, identity, new CompoundComparator(this.comparator, comparator)); } /** @@ -210,7 +228,7 @@ public EqualsMethod withFieldOrder(Comparator nonNullable; + /** + * A matcher to determine fields of a reference type that cannot be {@code null}. + */ + private final ElementMatcher identity; + /** * Creates a new appender. * @@ -739,15 +762,18 @@ protected static class Appender implements ByteCodeAppender { * @param baseline The baseline stack manipulation. * @param fieldDescriptions A list of fields to use for the comparison. * @param nonNullable A matcher to determine fields of a reference type that cannot be {@code null}. + * @param identity A matcher to determine that a field should be considered by its identity. */ protected Appender(TypeDescription instrumentedType, StackManipulation baseline, List fieldDescriptions, - ElementMatcher nonNullable) { + ElementMatcher nonNullable, + ElementMatcher identity) { this.instrumentedType = instrumentedType; this.baseline = baseline; this.fieldDescriptions = fieldDescriptions; this.nonNullable = nonNullable; + this.identity = identity; } /** @@ -770,13 +796,17 @@ public Size apply(MethodVisitor methodVisitor, Context implementationContext, Me stackManipulations.add(MethodVariableAccess.REFERENCE.loadFrom(1)); stackManipulations.add(TypeCasting.to(instrumentedType)); stackManipulations.add(FieldAccess.forField(fieldDescription).read()); - NullValueGuard nullValueGuard = fieldDescription.getType().isPrimitive() || fieldDescription.getType().isArray() || nonNullable.matches(fieldDescription) - ? NullValueGuard.NoOp.INSTANCE - : new NullValueGuard.UsingJump(instrumentedMethod); - stackManipulations.add(nullValueGuard.before()); - stackManipulations.add(ValueComparator.of(fieldDescription.getType())); - stackManipulations.add(nullValueGuard.after()); - padding = Math.max(padding, nullValueGuard.getRequiredVariablePadding()); + if (!fieldDescription.getType().isPrimitive() && identity.matches(fieldDescription)) { + stackManipulations.add(ConditionalReturn.onNonIdentity()); + } else { + NullValueGuard nullValueGuard = fieldDescription.getType().isPrimitive() || fieldDescription.getType().isArray() || nonNullable.matches(fieldDescription) + ? NullValueGuard.NoOp.INSTANCE + : new NullValueGuard.UsingJump(instrumentedMethod); + stackManipulations.add(nullValueGuard.before()); + stackManipulations.add(ValueComparator.of(fieldDescription.getType())); + stackManipulations.add(nullValueGuard.after()); + padding = Math.max(padding, nullValueGuard.getRequiredVariablePadding()); + } } stackManipulations.add(IntegerConstant.forValue(true)); stackManipulations.add(MethodReturn.INTEGER); @@ -790,11 +820,6 @@ public Size apply(MethodVisitor methodVisitor, Context implementationContext, Me @HashCodeAndEqualsPlugin.Enhance protected static class ConditionalReturn extends StackManipulation.AbstractBase { - /** - * An empty array. - */ - private static final Object[] EMPTY = new Object[0]; - /** * The conditional jump instruction upon which the return is not triggered. */ diff --git a/byte-buddy-dep/src/main/java/net/bytebuddy/implementation/HashCodeMethod.java b/byte-buddy-dep/src/main/java/net/bytebuddy/implementation/HashCodeMethod.java index ac0395350a..c82c6dab88 100644 --- a/byte-buddy-dep/src/main/java/net/bytebuddy/implementation/HashCodeMethod.java +++ b/byte-buddy-dep/src/main/java/net/bytebuddy/implementation/HashCodeMethod.java @@ -94,13 +94,18 @@ public class HashCodeMethod implements Implementation { */ private final ElementMatcher.Junction nonNullable; + /** + * A matcher to determine that a field should be considered by its identity. + */ + private final ElementMatcher.Junction identity; + /** * Creates a new hash code method implementation. * * @param offsetProvider The hash code's offset provider. */ protected HashCodeMethod(OffsetProvider offsetProvider) { - this(offsetProvider, DEFAULT_MULTIPLIER, none(), none()); + this(offsetProvider, DEFAULT_MULTIPLIER, none(), none(), none()); } /** @@ -110,15 +115,18 @@ protected HashCodeMethod(OffsetProvider offsetProvider) { * @param multiplier A multiplier for each value before adding a field's hash code value * @param ignored A matcher to filter fields that should not be used for a hash codes computation. * @param nonNullable A matcher to determine fields of a reference type that cannot be {@code null}. + * @param identity A matcher to determine that a field should be considered by its identity. */ private HashCodeMethod(OffsetProvider offsetProvider, int multiplier, ElementMatcher.Junction ignored, - ElementMatcher.Junction nonNullable) { + ElementMatcher.Junction nonNullable, + ElementMatcher.Junction identity) { this.offsetProvider = offsetProvider; this.multiplier = multiplier; this.ignored = ignored; this.nonNullable = nonNullable; + this.identity = identity; } /** @@ -167,7 +175,7 @@ public static HashCodeMethod usingOffset(int value) { * @return A new version of this hash code method implementation that also ignores any fields matched by the provided matcher. */ public HashCodeMethod withIgnoredFields(ElementMatcher ignored) { - return new HashCodeMethod(offsetProvider, multiplier, this.ignored.or(ignored), nonNullable); + return new HashCodeMethod(offsetProvider, multiplier, this.ignored.or(ignored), nonNullable, identity); } /** @@ -179,7 +187,17 @@ public HashCodeMethod withIgnoredFields(ElementMatcher nonNullable) { - return new HashCodeMethod(offsetProvider, multiplier, ignored, this.nonNullable.or(nonNullable)); + return new HashCodeMethod(offsetProvider, multiplier, ignored, this.nonNullable.or(nonNullable), identity); + } + + /** + * Returns a new version of this hash code method implementation that considers the matched fields by their identity. + * + * @param identity A matcher to specify any fields that should be considered by their identity. + * @return A new version of this hash code method implementation that also considers the matched fields by their identity. + */ + public HashCodeMethod withIdentityFields(ElementMatcher identity) { + return new HashCodeMethod(offsetProvider, multiplier, ignored, nonNullable, this.identity.or(identity)); } /** @@ -194,7 +212,7 @@ public Implementation withMultiplier(int multiplier) { if (multiplier == 0) { throw new IllegalArgumentException("Hash code multiplier must not be zero"); } - return new HashCodeMethod(offsetProvider, multiplier, ignored, nonNullable); + return new HashCodeMethod(offsetProvider, multiplier, ignored, nonNullable, identity); } /** @@ -214,7 +232,8 @@ public ByteCodeAppender appender(Target implementationTarget) { return new Appender(offsetProvider.resolve(implementationTarget.getInstrumentedType()), multiplier, implementationTarget.getInstrumentedType().getDeclaredFields().filter(not(isStatic().or(ignored))), - nonNullable); + nonNullable, + identity); } /** @@ -617,6 +636,17 @@ public Size apply(MethodVisitor methodVisitor, Context implementationContext) { methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "java/util/Arrays", "deepHashCode", "([Ljava/lang/Object;)I", false); return Size.ZERO; } + }, + + /** + * A transformer for computing the identity hash code for a reference. + */ + REFERENCE_IDENTITY { + /** {@inheritDoc} */ + public Size apply(MethodVisitor methodVisitor, Context implementationContext) { + methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "identityHashCode", "(Ljava/lang/Object;)I", false); + return Size.ZERO; + } }; /** @@ -698,6 +728,11 @@ protected static class Appender implements ByteCodeAppender { */ private final ElementMatcher nonNullable; + /** + * A matcher to determine that a field should be considered by its identity. + */ + private final ElementMatcher identity; + /** * Creates a new appender for implementing a hash code method. * @@ -705,15 +740,18 @@ protected static class Appender implements ByteCodeAppender { * @param multiplier A multiplier for each value before adding a field's hash code value. * @param fieldDescriptions A list of fields to include in the hash code computation. * @param nonNullable A matcher to determine fields of a reference type that cannot be {@code null}. + * @param identity A matcher to determine that a field should be considered by its identity. */ protected Appender(StackManipulation initialValue, int multiplier, List fieldDescriptions, - ElementMatcher nonNullable) { + ElementMatcher nonNullable, + ElementMatcher identity) { this.initialValue = initialValue; this.multiplier = multiplier; this.fieldDescriptions = fieldDescriptions; this.nonNullable = nonNullable; + this.identity = identity; } /** @@ -733,14 +771,19 @@ public Size apply(MethodVisitor methodVisitor, Context implementationContext, Me stackManipulations.add(Multiplication.INTEGER); stackManipulations.add(MethodVariableAccess.loadThis()); stackManipulations.add(FieldAccess.forField(fieldDescription).read()); - NullValueGuard nullValueGuard = fieldDescription.getType().isPrimitive() || fieldDescription.getType().isArray() || nonNullable.matches(fieldDescription) - ? NullValueGuard.NoOp.INSTANCE - : new NullValueGuard.UsingJump(instrumentedMethod); - stackManipulations.add(nullValueGuard.before()); - stackManipulations.add(ValueTransformer.of(fieldDescription.getType())); - stackManipulations.add(Addition.INTEGER); - stackManipulations.add(nullValueGuard.after()); - padding = Math.max(padding, nullValueGuard.getRequiredVariablePadding()); + if (!fieldDescription.getType().isPrimitive() && identity.matches(fieldDescription)) { + stackManipulations.add(ValueTransformer.REFERENCE_IDENTITY); + stackManipulations.add(Addition.INTEGER); + } else { + NullValueGuard nullValueGuard = fieldDescription.getType().isPrimitive() || fieldDescription.getType().isArray() || nonNullable.matches(fieldDescription) + ? NullValueGuard.NoOp.INSTANCE + : new NullValueGuard.UsingJump(instrumentedMethod); + stackManipulations.add(nullValueGuard.before()); + stackManipulations.add(ValueTransformer.of(fieldDescription.getType())); + stackManipulations.add(Addition.INTEGER); + stackManipulations.add(nullValueGuard.after()); + padding = Math.max(padding, nullValueGuard.getRequiredVariablePadding()); + } } stackManipulations.add(MethodReturn.INTEGER); return new Size(new StackManipulation.Compound(stackManipulations).apply(methodVisitor, implementationContext).getMaximalSize(), instrumentedMethod.getStackSize() + padding); diff --git a/byte-buddy-dep/src/test/java/net/bytebuddy/build/HashCodeAndEqualsPluginTest.java b/byte-buddy-dep/src/test/java/net/bytebuddy/build/HashCodeAndEqualsPluginTest.java index 3858fa77b5..03eddd59ad 100644 --- a/byte-buddy-dep/src/test/java/net/bytebuddy/build/HashCodeAndEqualsPluginTest.java +++ b/byte-buddy-dep/src/test/java/net/bytebuddy/build/HashCodeAndEqualsPluginTest.java @@ -16,9 +16,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import java.util.Comparator; +import java.util.HashSet; import static net.bytebuddy.test.utility.FieldByFieldComparison.hasPrototype; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -70,6 +72,24 @@ public void testPluginEnhanceIgnore() throws Exception { assertThat(left, is(right)); } + @Test + public void testPluginEnhanceIdentity() throws Exception { + Class type = new HashCodeAndEqualsPlugin() + .apply(new ByteBuddy().redefine(IdentityFieldSample.class), TypeDescription.ForLoadedType.of(IdentityFieldSample.class), ClassFileLocator.ForClassLoader.of(IdentityFieldSample.class.getClassLoader())) + .make() + .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER) + .getLoaded(); + Object left = type.getDeclaredConstructor().newInstance(), right = type.getDeclaredConstructor().newInstance(); + Object leftValue = new HashSet(), rightValue = new HashSet(); + type.getDeclaredField(FOO).set(left, leftValue); + type.getDeclaredField(FOO).set(right, rightValue); + assertThat(left.hashCode(), not(right.hashCode())); + assertThat(left, not(right)); + type.getDeclaredField(FOO).set(right, leftValue); + assertThat(left.hashCode(), is(right.hashCode())); + assertThat(left, is(right)); + } + @Test(expected = NullPointerException.class) public void testPluginEnhanceNonNullableHashCode() throws Exception { new HashCodeAndEqualsPlugin() @@ -280,6 +300,13 @@ public static class NonNullableField { public String foo; } + @HashCodeAndEqualsPlugin.Enhance + public static class IdentityFieldSample { + + @HashCodeAndEqualsPlugin.Identity + public Object foo; + } + @HashCodeAndEqualsPlugin.Enhance public static class FieldSortOrderSample { diff --git a/byte-buddy-dep/src/test/java/net/bytebuddy/implementation/EqualsMethodOtherTest.java b/byte-buddy-dep/src/test/java/net/bytebuddy/implementation/EqualsMethodOtherTest.java index f185e4f0e3..b2917cc99c 100644 --- a/byte-buddy-dep/src/test/java/net/bytebuddy/implementation/EqualsMethodOtherTest.java +++ b/byte-buddy-dep/src/test/java/net/bytebuddy/implementation/EqualsMethodOtherTest.java @@ -7,10 +7,12 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.DynamicType; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.test.utility.DebuggingWrapper; import org.junit.Test; import java.lang.annotation.RetentionPolicy; import java.util.Comparator; +import java.util.HashSet; import static net.bytebuddy.matcher.ElementMatchers.any; import static net.bytebuddy.matcher.ElementMatchers.*; @@ -83,7 +85,28 @@ public void testIgnoredField() throws Exception { assertThat(loaded.getLoaded().getDeclaredFields().length, is(1)); Object left = loaded.getLoaded().getDeclaredConstructor().newInstance(), right = loaded.getLoaded().getDeclaredConstructor().newInstance(); left.getClass().getDeclaredField(FOO).set(left, FOO); - left.getClass().getDeclaredField(FOO).set(left, BAR); + assertThat(left, is(right)); + } + + @Test + public void testIdentityField() throws Exception { + DynamicType.Loaded loaded = new ByteBuddy() + .subclass(Object.class) + .defineField(FOO, Object.class, Visibility.PUBLIC) + .method(isEquals()) + .intercept(EqualsMethod.isolated().withIdentityFields(named(FOO))) + .visit(DebuggingWrapper.makeDefault(true)) + .make() + .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER); + assertThat(loaded.getLoadedAuxiliaryTypes().size(), is(0)); + assertThat(loaded.getLoaded().getDeclaredMethods().length, is(1)); + assertThat(loaded.getLoaded().getDeclaredFields().length, is(1)); + Object left = loaded.getLoaded().getDeclaredConstructor().newInstance(), right = loaded.getLoaded().getDeclaredConstructor().newInstance(); + Object leftValue = new HashSet(), rightValue = new HashSet(); + left.getClass().getDeclaredField(FOO).set(left, leftValue); + right.getClass().getDeclaredField(FOO).set(right, rightValue); + assertThat(left, not(right)); + right.getClass().getDeclaredField(FOO).set(right, leftValue); assertThat(left, is(right)); } diff --git a/byte-buddy-dep/src/test/java/net/bytebuddy/implementation/HashCodeMethodOtherTest.java b/byte-buddy-dep/src/test/java/net/bytebuddy/implementation/HashCodeMethodOtherTest.java index 8117c3b59e..5b62f1c838 100644 --- a/byte-buddy-dep/src/test/java/net/bytebuddy/implementation/HashCodeMethodOtherTest.java +++ b/byte-buddy-dep/src/test/java/net/bytebuddy/implementation/HashCodeMethodOtherTest.java @@ -48,6 +48,23 @@ public void testIgnoredField() throws Exception { assertThat(instance.hashCode(), is(0)); } + @Test + public void testIdentityField() throws Exception { + DynamicType.Loaded loaded = new ByteBuddy() + .subclass(Object.class) + .defineField(FOO, Object.class, Visibility.PUBLIC) + .method(isHashCode()) + .intercept(HashCodeMethod.usingOffset(0).withIdentityFields(named(FOO))) + .make() + .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER); + assertThat(loaded.getLoadedAuxiliaryTypes().size(), is(0)); + assertThat(loaded.getLoaded().getDeclaredMethods().length, is(1)); + assertThat(loaded.getLoaded().getDeclaredFields().length, is(1)); + Object instance = loaded.getLoaded().getDeclaredConstructor().newInstance(); + instance.getClass().getDeclaredField(FOO).set(instance, FOO); + assertThat(instance.hashCode(), is(System.identityHashCode(FOO))); + } + @Test public void testSuperMethod() throws Exception { DynamicType.Loaded loaded = new ByteBuddy()