package org.yajul.util; import java.io.Serializable; import java.util.Arrays; /** * Provides an easy way to make a key for a Map from. It uses an array of objects, computing hash codes * and testing for equality. Immutable, and the components should be immutable as well. * Objects used in the compound key should implement Comparable and Serializable if the CompoundKey * is to be used in either of those ways. * * @author Joshua Davis */ public class CompoundKey implements Comparable<CompoundKey>, Serializable, Cloneable { private static final int HASH_NUMBER = 31; /** * The component objects in the key. */ private final Object[] components; /** * The pre-computed hash code. */ private final int hash; /** * Computes the hash code for an array of objects. * * @param components An array of objects. * @return int - The hash value. */ public static int computeHash(Object[] components) { // Sum all of the hash codes of the components, using an algorithm similar to that used by // java.lang.String. int rv = 0; int limit = components.length; for (int i = 0; i < limit; i++) { Object c = components[i]; if (c == null) throw new IllegalArgumentException("A value for component[" + i + "] is required!"); rv += c.hashCode() * (HASH_NUMBER ^ (limit - i)); } return rv; } /** * Compare arrays of objects, as parallel arrays. * <p/> * NOTE: May throw a class cast exception if * the a_array objects do not implement Comparable<Object>! * <p/> * <br><br> * <b>Throws:</b> * <blockquote>ClassCastException if the type of any object in a_array prevents it * from being compared to the corresponding object in b_array.</blockquote> * * @param a_array an array of objects that implement Comparable * @param b_array a parallel array of objects. * @return a negative integer, zero, or a positive integer as a_array * is less than, equal to, or greater than b_array */ public static int compareObjectArrays(Object[] a_array, Object[] b_array) { // First, check if the length of the arrays is different. int rc = a_array.length - b_array.length; if (rc == 0) { Comparable<Object> a; Object b; for (int i = 0; i < a_array.length; i++) { //noinspection unchecked a = (Comparable<Object>) a_array[i]; b = b_array[i]; //noinspection unchecked rc = a.compareTo(b); if (rc != 0) // If the a_array are not equal, stop now. break; } // for } return rc; } /** * Creates a new compound key. The array of objects <b><i>will not be copied</i></b>, so please make sure that * it does not change for the life of the compound key object. Same goes for the objects referenced by the * array. * * @param components The components of the key. */ public CompoundKey(Object... components) { if (components == null) throw new IllegalArgumentException("components cannot be null!"); if (components.length == 0) throw new IllegalArgumentException("Must have at least one component!"); this.components = components; hash = computeHash(components); } /** * Indicates whether some other object is "equal to" this one. This implementation * compares all of the elements of the compound key. * * @param obj the reference object with which to compare. * @return <code>true</code> if this object is the same as the obj * argument; <code>false</code> otherwise. * @see java.lang.Boolean#hashCode() * @see java.util.Hashtable * @see java.util.Arrays#equals(java.lang.Object[], java.lang.Object[]) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (obj instanceof CompoundKey) { CompoundKey other = (CompoundKey) obj; // Check the hash codes first, if they are not equal... return false. if (hashCode() != other.hashCode()) return false; // Hash codes are the same, so the objects must be compared. // Use the Arrays class as a short cut. return Arrays.equals(components, other.components); } else return false; } @Override public int hashCode() { return hash; } /** * Returns a string representation of the object. In general, the * <code>toString</code> method returns a string that * "textually represents" this object. The result should * be a concise but informative representation that is easy for a * person to read. * It is recommended that all subclasses override this method. * <p/> * The <code>toString</code> method for class <code>Object</code> * returns a string consisting of the name of the class of which the * object is an instance, the at-sign character `<code>@</code>', and * the unsigned hexadecimal representation of the hash code of the * object. In other words, this method returns a string equal to the * value of: * <blockquote> * <pre> * getClass().getName() + '@' + Integer.toHexString(hashCode()) * </pre></blockquote> * * @return a string representation of the object. */ public String toString() { StringBuffer buf = new StringBuffer(); buf.append(this.getClass().getName()); buf.append("@"); buf.append(Integer.toHexString(System.identityHashCode(this))); buf.append("[ "); for (int i = 0; i < components.length; i++) { buf.append("#"); buf.append(Integer.toString(i)); buf.append(" [ "); buf.append(components[i].toString()); buf.append(" ] "); } buf.append(" ]"); return buf.toString(); } /** * NOTE: May throw a class cast exception if the 'components' of this * CompoundKey do not implement Comparable. * <p/> * Compares this object with the specified object for order. Returns a * negative integer, zero, or a positive integer as this object is less * than, equal to, or greater than the specified object.<p> * <p/> * The implementor must ensure <tt>sgn(x.compareTo(y)) == * -sgn(y.compareTo(x))</tt> for all <tt>x</tt> and <tt>y</tt>. (This * implies that <tt>x.compareTo(y)</tt> must throw an exception iff * <tt>y.compareTo(x)</tt> throws an exception.)<p> * <p/> * The implementor must also ensure that the relation is transitive: * <tt>(x.compareTo(y)>0 && y.compareTo(z)>0)</tt> implies * <tt>x.compareTo(z)>0</tt>.<p> * <p/> * Finally, the implementer must ensure that <tt>x.compareTo(y)==0</tt> * implies that <tt>sgn(x.compareTo(z)) == sgn(y.compareTo(z))</tt>, for * all <tt>z</tt>.<p> * <p/> * It is strongly recommended, but <i>not</i> strictly required that * <tt>(x.compareTo(y)==0) == (x.equals(y))</tt>. Generally speaking, any * class that implements the <tt>Comparable</tt> interface and violates * this condition should clearly indicate this fact. The recommended * language is "Note: this class has a natural ordering that is * inconsistent with equals." * <br><br> * <b>Throws:</b> * <blockquote>ClassCastException if the specified object's type prevents it * from being compared to this Object.</blockquote> * * @param other the Object to be compared. * @return a negative integer, zero, or a positive integer as this object * is less than, equal to, or greater than the specified object. */ public int compareTo(CompoundKey other) { return compareObjectArrays(components, other.components); } /** * Generic cast get method. * * @param clazz the type * @param index the index * @param <T> the type * @return the element of the key, autocasted */ public <T> T get(Class<T> clazz, int index) { return clazz.cast(getComponent(index)); } /** * Returns the number of components in the compound key. * * @return int - The number of components. */ public int size() { return components.length; } /** * Returns the 'nth' component of the compound key. * * @param n The index. * @return Object - The 'nth' component. */ public Object getComponent(int n) { return components[n]; } /** * Returns a new copy of the component array. * * @return Object[] - A copy of the array of components. */ public Object[] cloneComponents() { Object[] array = new Object[components.length]; System.arraycopy(components, 0, array, 0, components.length); return array; } /** * Creates and returns a copy of this object. The precise meaning * of "copy" may depend on the class of the object. * <br>exception OutOfMemoryError if there is not enough memory. * * @return a clone of this instance. * @throws CloneNotSupportedException if the object's class does not * support the <code>Cloneable</code> interface. Subclasses * that override the <code>clone</code> method can also * throw this exception to indicate that an instance cannot * be cloned. * @see java.lang.Cloneable */ public Object clone() throws CloneNotSupportedException { return new CompoundKey(cloneComponents()); } /** * A two-component compound key using generics. * * @param <X> the type of the first component * @param <Y> the type of the second component. */ public static class Two<X, Y> extends CompoundKey { protected Two(Object one, Object two, Object three) { super(one, two, three); } public Two(X one, Y two) { super(one, two); } /** * @return the first component of the key */ public X getOne() { //noinspection unchecked return (X) getComponent(0); } /** * @return the second component of the key */ public Y getTwo() { //noinspection unchecked return (Y) getComponent(1); } } /** * A three-component compound key using generics. * * @param <X> the type of the first component * @param <Y> the type of the second component. * @param <Z> the type of the third component. */ public static class Three<X, Y, Z> extends Two<X, Y> { public Three(X one, Y two, Z three) { super(one, two, three); } /** * @return the third component of the key */ public Z getThree() { //noinspection unchecked return (Z) getComponent(2); } } }