001/* 002 * Copyright 2013 Christopher Pheby 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.jadira.reflection.hashcode; 017 018import java.lang.reflect.Modifier; 019import java.util.IdentityHashMap; 020 021import org.jadira.reflection.access.api.ClassAccess; 022import org.jadira.reflection.access.api.ClassAccessFactory; 023import org.jadira.reflection.access.api.FieldAccess; 024import org.jadira.reflection.access.api.MethodAccess; 025import org.jadira.reflection.access.portable.PortableClassAccessFactory; 026import org.jadira.reflection.access.unsafe.UnsafeClassAccessFactory; 027import org.jadira.reflection.core.platform.FeatureDetection; 028 029/** 030 * This class is based on the capabilities provided by Commons-Lang's 031 * HashCodeBuilder. It is not a complete "drop-in" replacement, but is near enough 032 * in functionality and in terms of its API that it should be quite 033 * straightforward to switch from one class to the other. 034 * 035 * HashCodeBuilder makes use of Jadira's reflection capabilities. This means, like 036 * Cloning there is a brief warm-up time entailed in preparing introspection 037 * when a class is first processed. If you are already performing cloning on a 038 * type you will not be impacted by this as any necessary warm-up will have 039 * already taken place. 040 * 041 * Once the initial warmup has been performed, you will, usually, find enhanced 042 * performance compared to standard reflection based approaches. The strategy to 043 * be used can be configured. 044 * 045 * Where objects that are reachable from the object being compared do not 046 * override hashCode(), the default behaviour is to rely on 047 * Object#hashCode(Object) which uses the object identity for those 048 * objects. This is also the behaviour of Commons Lang. You can modify this to 049 * reflect into any reachable object that does not override equals by enabling 050 * the defaultDeepReflect property. 051 * 052 * Transient fields are not compared by the current implementation of this 053 * class. 054 */ 055public class HashCodeBuilder { 056 057 private static final ThreadLocal<HashCodeBuilder> reflectionBuilder = new ThreadLocal<HashCodeBuilder>() { 058 protected HashCodeBuilder initialValue() { 059 return new HashCodeBuilder(37, 17); 060 }; 061 }; 062 063 private IdentityHashMap<Object, Object> seenReferences; 064 065 /** 066 * Constant to use in building the hashCode. 067 */ 068 private final int constant; 069 070 /** 071 * Running total of the hashCode. 072 */ 073 private int computedTotal; 074 075 private final int seed; 076 077 private ClassAccessFactory classAccessFactory; 078 079 private boolean defaultDeepReflect = false; 080 081 private <C> void reflectionAppend(C object) { 082 083 if (seenReferences.containsKey(object)) { 084 return; 085 } 086 seenReferences.put(object, object); 087 088 @SuppressWarnings("unchecked") 089 ClassAccess<? super C> classAccess = (ClassAccess<C>) classAccessFactory 090 .getClassAccess(object.getClass()); 091 092 while ((classAccess.getType() != Object.class) 093 && (!classAccess.providesHashCode())) { 094 095 ClassAccess<? super C> classAccessInHierarchy = classAccess; 096 097 for (FieldAccess<? super C> fieldAccess : classAccessInHierarchy.getDeclaredFieldAccessors()) { 098 099 if ((fieldAccess.field().getName().indexOf('$') == -1) 100 && (!Modifier.isTransient(fieldAccess.field().getModifiers())) 101 && (!Modifier.isStatic(fieldAccess.field().getModifiers()))) { 102 103 Class<?> type = fieldAccess.fieldClass(); 104 105 if (type.isPrimitive()) { 106 if (java.lang.Boolean.TYPE == type) { 107 append(fieldAccess.getBooleanValue(object)); 108 } else if (java.lang.Byte.TYPE == type) { 109 append(fieldAccess.getByteValue(object)); 110 } else if (java.lang.Character.TYPE == type) { 111 append(fieldAccess.getCharValue(object)); 112 } else if (java.lang.Short.TYPE == type) { 113 append(fieldAccess.getShortValue(object)); 114 } else if (java.lang.Integer.TYPE == type) { 115 append(fieldAccess.getIntValue(object)); 116 } else if (java.lang.Long.TYPE == type) { 117 append(fieldAccess.getLongValue(object)); 118 } else if (java.lang.Float.TYPE == type) { 119 append(fieldAccess.getFloatValue(object)); 120 } else if (java.lang.Double.TYPE == type) { 121 append(fieldAccess.getDoubleValue(object)); 122 } 123 } else { 124 final Object value = fieldAccess.getValue(object); 125 append(value); 126 } 127 } 128 } 129 130 classAccessInHierarchy = classAccessInHierarchy.getSuperClassAccess(); 131 } 132 if (classAccess.getType() != Object.class) { 133 134 final MethodAccess<Object> methodAccess; 135 try { 136 @SuppressWarnings("unchecked") 137 final MethodAccess<Object> myMethodAccess = (MethodAccess<Object>) classAccess 138 .getDeclaredMethodAccess(classAccess.getType().getMethod( 139 "hashCode")); 140 methodAccess = myMethodAccess; 141 } catch (NoSuchMethodException e) { 142 throw new IllegalStateException("Cannot find hashCode() method"); 143 } catch (SecurityException e) { 144 throw new IllegalStateException("Cannot find hashCode() method"); 145 } 146 append(((Integer) (methodAccess.invoke(object))).intValue()); 147 } 148 } 149 150 public static <T> int reflectionHashCode(T object) { 151 return reflectionHashCode(37, 17, object); 152 } 153 154 public static <T> int reflectionHashCode(int initialNonZeroOddNumber, 155 int multiplierNonZeroOddNumber, T object) { 156 157 if (object == null) { 158 throw new IllegalArgumentException( 159 "Cannot build hashcode for null object"); 160 } 161 162 HashCodeBuilder builder = reflectionBuilder.get(); 163 // In case we recurse into this method 164 reflectionBuilder.set(null); 165 166 if (builder.seed == initialNonZeroOddNumber 167 && builder.constant == multiplierNonZeroOddNumber) { 168 builder.reset(); 169 } else { 170 builder = new HashCodeBuilder(initialNonZeroOddNumber, 171 multiplierNonZeroOddNumber); 172 } 173 174 builder.reflectionAppend(object); 175 176 final int hashCode = builder.toHashCode(); 177 reflectionBuilder.set(builder); 178 179 return hashCode; 180 } 181 182 public HashCodeBuilder() { 183 184 seenReferences = new IdentityHashMap<Object, Object>(); 185 186 constant = 37; 187 computedTotal = seed = 17; 188 189 if (FeatureDetection.hasUnsafe()) { 190 this.classAccessFactory = UnsafeClassAccessFactory.get(); 191 } else { 192 this.classAccessFactory = PortableClassAccessFactory.get(); 193 } 194 } 195 196 public HashCodeBuilder(int initialNonZeroOddNumber, 197 int multiplierNonZeroOddNumber) { 198 199 seenReferences = new IdentityHashMap<Object, Object>(); 200 201 if ((initialNonZeroOddNumber % 2 == 0) 202 || (initialNonZeroOddNumber == 0)) { 203 throw new IllegalArgumentException( 204 "Initial Value must be odd and non-zero but was: " 205 + initialNonZeroOddNumber); 206 } else if ((multiplierNonZeroOddNumber % 2 == 0) 207 || (multiplierNonZeroOddNumber == 0)) { 208 throw new IllegalArgumentException( 209 "Multiplier must be odd and non-zero but was: " 210 + multiplierNonZeroOddNumber); 211 } 212 213 constant = multiplierNonZeroOddNumber; 214 computedTotal = seed = initialNonZeroOddNumber; 215 216 if (FeatureDetection.hasUnsafe()) { 217 this.classAccessFactory = UnsafeClassAccessFactory.get(); 218 } else { 219 this.classAccessFactory = PortableClassAccessFactory.get(); 220 } 221 } 222 223 public HashCodeBuilder withDefaultDeepReflect(boolean newDefaultDeepReflect) { 224 this.defaultDeepReflect = newDefaultDeepReflect; 225 return this; 226 } 227 228 public HashCodeBuilder withClassAccessFactory( 229 ClassAccessFactory classAccessFactory) { 230 this.classAccessFactory = classAccessFactory; 231 return this; 232 } 233 234 public HashCodeBuilder append(boolean value) { 235 computedTotal = computedTotal * constant + (value ? 0 : 1); 236 return this; 237 } 238 239 public HashCodeBuilder append(boolean[] array) { 240 if (seenReferences.containsKey(array)) { 241 return this; 242 } 243 seenReferences.put(array, array); 244 245 if (array == null) { 246 appendNull(); 247 } else { 248 for (boolean element : array) { 249 append(element); 250 } 251 } 252 return this; 253 } 254 255 public HashCodeBuilder append(byte value) { 256 computedTotal = (computedTotal * constant) + value; 257 return this; 258 } 259 260 public HashCodeBuilder append(byte[] array) { 261 if (seenReferences.containsKey(array)) { 262 return this; 263 } 264 seenReferences.put(array, array); 265 266 if (array == null) { 267 appendNull(); 268 } else { 269 for (byte element : array) { 270 append(element); 271 } 272 } 273 return this; 274 } 275 276 public HashCodeBuilder append(char value) { 277 computedTotal = (computedTotal * constant) + value; 278 return this; 279 } 280 281 public HashCodeBuilder append(char[] array) { 282 if (seenReferences.containsKey(array)) { 283 return this; 284 } 285 seenReferences.put(array, array); 286 287 if (array == null) { 288 appendNull(); 289 } else { 290 for (char element : array) { 291 append(element); 292 } 293 } 294 return this; 295 } 296 297 public HashCodeBuilder append(double value) { 298 return append(Double.doubleToLongBits(value)); 299 } 300 301 public HashCodeBuilder append(double[] array) { 302 if (seenReferences.containsKey(array)) { 303 return this; 304 } 305 seenReferences.put(array, array); 306 307 if (array == null) { 308 appendNull(); 309 } else { 310 for (double element : array) { 311 append(element); 312 } 313 } 314 return this; 315 } 316 317 public HashCodeBuilder append(float value) { 318 computedTotal = (computedTotal * constant) 319 + Float.floatToIntBits(value); 320 return this; 321 } 322 323 public HashCodeBuilder append(float[] array) { 324 if (seenReferences.containsKey(array)) { 325 return this; 326 } 327 seenReferences.put(array, array); 328 329 if (array == null) { 330 appendNull(); 331 } else { 332 for (float element : array) { 333 append(element); 334 } 335 } 336 return this; 337 } 338 339 public HashCodeBuilder append(int value) { 340 computedTotal = (computedTotal * constant) + value; 341 return this; 342 } 343 344 public HashCodeBuilder append(int[] array) { 345 if (seenReferences.containsKey(array)) { 346 return this; 347 } 348 seenReferences.put(array, array); 349 350 if (array == null) { 351 appendNull(); 352 } else { 353 for (int element : array) { 354 append(element); 355 } 356 } 357 return this; 358 } 359 360 public HashCodeBuilder append(long value) { 361 computedTotal = computedTotal * constant 362 + (int) (value ^ (value >>> 32)); 363 return this; 364 } 365 366 public HashCodeBuilder append(long[] array) { 367 if (seenReferences.containsKey(array)) { 368 return this; 369 } 370 seenReferences.put(array, array); 371 372 if (array == null) { 373 appendNull(); 374 } else { 375 for (long element : array) { 376 append(element); 377 } 378 } 379 return this; 380 } 381 382 public HashCodeBuilder append(Object object) { 383 384 if (object == null) { 385 appendNull(); 386 return this; 387 } else { 388 if (object.getClass() == null) { 389 appendNull(); 390 } else if (object.getClass().isArray()) { 391 392 if (object instanceof long[]) { 393 append((long[]) object); 394 } else if (object instanceof int[]) { 395 append((int[]) object); 396 } else if (object instanceof short[]) { 397 append((short[]) object); 398 } else if (object instanceof char[]) { 399 append((char[]) object); 400 } else if (object instanceof byte[]) { 401 append((byte[]) object); 402 } else if (object instanceof double[]) { 403 append((double[]) object); 404 } else if (object instanceof float[]) { 405 append((float[]) object); 406 } else if (object instanceof boolean[]) { 407 append((boolean[]) object); 408 } else { 409 append((Object[]) object); 410 } 411 } else { 412 computedTotal = (computedTotal * constant); 413 if (defaultDeepReflect) { 414 reflectionAppend(object); 415 } else { 416 computedTotal = computedTotal + object.hashCode(); 417 } 418 } 419 } 420 return this; 421 } 422 423 public HashCodeBuilder append(Object[] array) { 424 if (seenReferences.containsKey(array)) { 425 return this; 426 } 427 seenReferences.put(array, array); 428 429 if (array == null) { 430 appendNull(); 431 } else { 432 for (Object element : array) { 433 append(element); 434 } 435 } 436 return this; 437 } 438 439 public HashCodeBuilder append(short value) { 440 computedTotal = (computedTotal * constant) + value; 441 return this; 442 } 443 444 public HashCodeBuilder append(short[] array) { 445 if (seenReferences.containsKey(array)) { 446 return this; 447 } 448 seenReferences.put(array, array); 449 450 if (array == null) { 451 appendNull(); 452 } else { 453 for (short element : array) { 454 append(element); 455 } 456 } 457 return this; 458 } 459 460 public HashCodeBuilder appendSuper(int superHashCode) { 461 computedTotal = (computedTotal * constant) + superHashCode; 462 return this; 463 } 464 465 protected void appendNull() { 466 computedTotal = computedTotal * constant; 467 } 468 469 public int toHashCode() { 470 return computedTotal; 471 } 472 473 public int hashCode() { 474 return toHashCode(); 475 } 476 477 public void reset() { 478 seenReferences = new IdentityHashMap<Object, Object>(); 479 computedTotal = seed; 480 } 481}