/* * Copyright 2013 Christopher Pheby * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jadira.reflection.hashcode; import java.lang.reflect.Modifier; import java.util.IdentityHashMap; import org.jadira.reflection.access.api.ClassAccess; import org.jadira.reflection.access.api.ClassAccessFactory; import org.jadira.reflection.access.api.FieldAccess; import org.jadira.reflection.access.api.MethodAccess; import org.jadira.reflection.access.portable.PortableClassAccessFactory; import org.jadira.reflection.access.unsafe.UnsafeClassAccessFactory; import org.jadira.reflection.core.platform.FeatureDetection; /** * This class is based on the capabilities provided by Commons-Lang's * HashCodeBuilder. It is not a complete "drop-in" replacement, but is near enough * in functionality and in terms of its API that it should be quite * straightforward to switch from one class to the other. * * HashCodeBuilder makes use of Jadira's reflection capabilities. This means, like * Cloning there is a brief warm-up time entailed in preparing introspection * when a class is first processed. If you are already performing cloning on a * type you will not be impacted by this as any necessary warm-up will have * already taken place. * * Once the initial warmup has been performed, you will, usually, find enhanced * performance compared to standard reflection based approaches. The strategy to * be used can be configured. * * Where objects that are reachable from the object being compared do not * override hashCode(), the default behaviour is to rely on * Object#hashCode(Object) which uses the object identity for those * objects. This is also the behaviour of Commons Lang. You can modify this to * reflect into any reachable object that does not override equals by enabling * the defaultDeepReflect property. * * Transient fields are not compared by the current implementation of this * class. */ public class HashCodeBuilder { private static final ThreadLocal<HashCodeBuilder> reflectionBuilder = new ThreadLocal<HashCodeBuilder>() { protected HashCodeBuilder initialValue() { return new HashCodeBuilder(37, 17); }; }; private IdentityHashMap<Object, Object> seenReferences; /** * Constant to use in building the hashCode. */ private final int constant; /** * Running total of the hashCode. */ private int computedTotal; private final int seed; private ClassAccessFactory classAccessFactory; private boolean defaultDeepReflect = false; private <C> void reflectionAppend(C object) { if (seenReferences.containsKey(object)) { return; } seenReferences.put(object, object); @SuppressWarnings("unchecked") ClassAccess<? super C> classAccess = (ClassAccess<C>) classAccessFactory .getClassAccess(object.getClass()); while ((classAccess.getType() != Object.class) && (!classAccess.providesHashCode())) { ClassAccess<? super C> classAccessInHierarchy = classAccess; for (FieldAccess<? super C> fieldAccess : classAccessInHierarchy.getDeclaredFieldAccessors()) { if ((fieldAccess.field().getName().indexOf('$') == -1) && (!Modifier.isTransient(fieldAccess.field().getModifiers())) && (!Modifier.isStatic(fieldAccess.field().getModifiers()))) { Class<?> type = fieldAccess.fieldClass(); if (type.isPrimitive()) { if (java.lang.Boolean.TYPE == type) { append(fieldAccess.getBooleanValue(object)); } else if (java.lang.Byte.TYPE == type) { append(fieldAccess.getByteValue(object)); } else if (java.lang.Character.TYPE == type) { append(fieldAccess.getCharValue(object)); } else if (java.lang.Short.TYPE == type) { append(fieldAccess.getShortValue(object)); } else if (java.lang.Integer.TYPE == type) { append(fieldAccess.getIntValue(object)); } else if (java.lang.Long.TYPE == type) { append(fieldAccess.getLongValue(object)); } else if (java.lang.Float.TYPE == type) { append(fieldAccess.getFloatValue(object)); } else if (java.lang.Double.TYPE == type) { append(fieldAccess.getDoubleValue(object)); } } else { final Object value = fieldAccess.getValue(object); append(value); } } } classAccessInHierarchy = classAccessInHierarchy.getSuperClassAccess(); } if (classAccess.getType() != Object.class) { final MethodAccess<Object> methodAccess; try { @SuppressWarnings("unchecked") final MethodAccess<Object> myMethodAccess = (MethodAccess<Object>) classAccess .getDeclaredMethodAccess(classAccess.getType().getMethod( "hashCode")); methodAccess = myMethodAccess; } catch (NoSuchMethodException e) { throw new IllegalStateException("Cannot find hashCode() method"); } catch (SecurityException e) { throw new IllegalStateException("Cannot find hashCode() method"); } append(((Integer) (methodAccess.invoke(object))).intValue()); } } public static <T> int reflectionHashCode(T object) { return reflectionHashCode(37, 17, object); } public static <T> int reflectionHashCode(int initialNonZeroOddNumber, int multiplierNonZeroOddNumber, T object) { if (object == null) { throw new IllegalArgumentException( "Cannot build hashcode for null object"); } HashCodeBuilder builder = reflectionBuilder.get(); // In case we recurse into this method reflectionBuilder.set(null); if (builder.seed == initialNonZeroOddNumber && builder.constant == multiplierNonZeroOddNumber) { builder.reset(); } else { builder = new HashCodeBuilder(initialNonZeroOddNumber, multiplierNonZeroOddNumber); } builder.reflectionAppend(object); final int hashCode = builder.toHashCode(); reflectionBuilder.set(builder); return hashCode; } public HashCodeBuilder() { seenReferences = new IdentityHashMap<Object, Object>(); constant = 37; computedTotal = seed = 17; if (FeatureDetection.hasUnsafe()) { this.classAccessFactory = UnsafeClassAccessFactory.get(); } else { this.classAccessFactory = PortableClassAccessFactory.get(); } } public HashCodeBuilder(int initialNonZeroOddNumber, int multiplierNonZeroOddNumber) { seenReferences = new IdentityHashMap<Object, Object>(); if ((initialNonZeroOddNumber % 2 == 0) || (initialNonZeroOddNumber == 0)) { throw new IllegalArgumentException( "Initial Value must be odd and non-zero but was: " + initialNonZeroOddNumber); } else if ((multiplierNonZeroOddNumber % 2 == 0) || (multiplierNonZeroOddNumber == 0)) { throw new IllegalArgumentException( "Multiplier must be odd and non-zero but was: " + multiplierNonZeroOddNumber); } constant = multiplierNonZeroOddNumber; computedTotal = seed = initialNonZeroOddNumber; if (FeatureDetection.hasUnsafe()) { this.classAccessFactory = UnsafeClassAccessFactory.get(); } else { this.classAccessFactory = PortableClassAccessFactory.get(); } } public HashCodeBuilder withDefaultDeepReflect(boolean newDefaultDeepReflect) { this.defaultDeepReflect = newDefaultDeepReflect; return this; } public HashCodeBuilder withClassAccessFactory( ClassAccessFactory classAccessFactory) { this.classAccessFactory = classAccessFactory; return this; } public HashCodeBuilder append(boolean value) { computedTotal = computedTotal * constant + (value ? 0 : 1); return this; } public HashCodeBuilder append(boolean[] array) { if (seenReferences.containsKey(array)) { return this; } seenReferences.put(array, array); if (array == null) { appendNull(); } else { for (boolean element : array) { append(element); } } return this; } public HashCodeBuilder append(byte value) { computedTotal = (computedTotal * constant) + value; return this; } public HashCodeBuilder append(byte[] array) { if (seenReferences.containsKey(array)) { return this; } seenReferences.put(array, array); if (array == null) { appendNull(); } else { for (byte element : array) { append(element); } } return this; } public HashCodeBuilder append(char value) { computedTotal = (computedTotal * constant) + value; return this; } public HashCodeBuilder append(char[] array) { if (seenReferences.containsKey(array)) { return this; } seenReferences.put(array, array); if (array == null) { appendNull(); } else { for (char element : array) { append(element); } } return this; } public HashCodeBuilder append(double value) { return append(Double.doubleToLongBits(value)); } public HashCodeBuilder append(double[] array) { if (seenReferences.containsKey(array)) { return this; } seenReferences.put(array, array); if (array == null) { appendNull(); } else { for (double element : array) { append(element); } } return this; } public HashCodeBuilder append(float value) { computedTotal = (computedTotal * constant) + Float.floatToIntBits(value); return this; } public HashCodeBuilder append(float[] array) { if (seenReferences.containsKey(array)) { return this; } seenReferences.put(array, array); if (array == null) { appendNull(); } else { for (float element : array) { append(element); } } return this; } public HashCodeBuilder append(int value) { computedTotal = (computedTotal * constant) + value; return this; } public HashCodeBuilder append(int[] array) { if (seenReferences.containsKey(array)) { return this; } seenReferences.put(array, array); if (array == null) { appendNull(); } else { for (int element : array) { append(element); } } return this; } public HashCodeBuilder append(long value) { computedTotal = computedTotal * constant + (int) (value ^ (value >>> 32)); return this; } public HashCodeBuilder append(long[] array) { if (seenReferences.containsKey(array)) { return this; } seenReferences.put(array, array); if (array == null) { appendNull(); } else { for (long element : array) { append(element); } } return this; } public HashCodeBuilder append(Object object) { if (object == null) { appendNull(); return this; } else { if (object.getClass() == null) { appendNull(); } else if (object.getClass().isArray()) { if (object instanceof long[]) { append((long[]) object); } else if (object instanceof int[]) { append((int[]) object); } else if (object instanceof short[]) { append((short[]) object); } else if (object instanceof char[]) { append((char[]) object); } else if (object instanceof byte[]) { append((byte[]) object); } else if (object instanceof double[]) { append((double[]) object); } else if (object instanceof float[]) { append((float[]) object); } else if (object instanceof boolean[]) { append((boolean[]) object); } else { append((Object[]) object); } } else { computedTotal = (computedTotal * constant); if (defaultDeepReflect) { reflectionAppend(object); } else { computedTotal = computedTotal + object.hashCode(); } } } return this; } public HashCodeBuilder append(Object[] array) { if (seenReferences.containsKey(array)) { return this; } seenReferences.put(array, array); if (array == null) { appendNull(); } else { for (Object element : array) { append(element); } } return this; } public HashCodeBuilder append(short value) { computedTotal = (computedTotal * constant) + value; return this; } public HashCodeBuilder append(short[] array) { if (seenReferences.containsKey(array)) { return this; } seenReferences.put(array, array); if (array == null) { appendNull(); } else { for (short element : array) { append(element); } } return this; } public HashCodeBuilder appendSuper(int superHashCode) { computedTotal = (computedTotal * constant) + superHashCode; return this; } protected void appendNull() { computedTotal = computedTotal * constant; } public int toHashCode() { return computedTotal; } public int hashCode() { return toHashCode(); } public void reset() { seenReferences = new IdentityHashMap<Object, Object>(); computedTotal = seed; } }