1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.collections4.keyvalue; 18 19 import java.io.Serializable; 20 import java.lang.reflect.Array; 21 import java.util.Arrays; 22 import java.util.Objects; 23 24 /** 25 * A {@code MultiKey} allows multiple map keys to be merged together. 26 * <p> 27 * The purpose of this class is to avoid the need to write code to handle 28 * maps of maps. An example might be the need to look up a file name by 29 * key and locale. The typical solution might be nested maps. This class 30 * can be used instead by creating an instance passing in the key and locale. 31 * </p> 32 * <p> 33 * Example usage: 34 * </p> 35 * <pre> 36 * // populate map with data mapping key+locale to localizedText 37 * Map map = new HashMap(); 38 * MultiKey multiKey = new MultiKey(key, locale); 39 * map.put(multiKey, localizedText); 40 * 41 * // later retrieve the localized text 42 * MultiKey multiKey = new MultiKey(key, locale); 43 * String localizedText = (String) map.get(multiKey); 44 * </pre> 45 * 46 * @param <K> the type of keys 47 * @since 3.0 48 */ 49 public class MultiKey<K> implements Serializable { 50 // This class could implement List, but that would confuse its purpose 51 52 /** Serialisation version */ 53 private static final long serialVersionUID = 4465448607415788805L; 54 55 @SuppressWarnings("unchecked") 56 private static <T> Class<? extends T> getClass(final T value) { 57 return (Class<? extends T>) (value == null ? Object.class : value.getClass()); 58 } 59 60 @SafeVarargs 61 private static <T> Class<? extends T> getComponentType(final T... values) { 62 @SuppressWarnings("unchecked") 63 final Class<? extends T> rootClass = (Class<? extends T>) Object.class; 64 if (values == null) { 65 return rootClass; 66 } 67 Class<? extends T> prevClass = values.length > 0 ? getClass(values[0]) : rootClass; 68 for (int i = 1; i < values.length; i++) { 69 final Class<? extends T> classI = getClass(values[i]); 70 if (prevClass != classI) { 71 return rootClass; 72 } 73 prevClass = classI; 74 } 75 return prevClass; 76 } 77 78 private static <T> T[] newArray(final T key1, final T key2) { 79 @SuppressWarnings("unchecked") 80 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2), 2); 81 array[0] = key1; 82 array[1] = key2; 83 return array; 84 } 85 86 private static <T> T[] newArray(final T key1, final T key2, final T key3) { 87 @SuppressWarnings("unchecked") 88 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3), 3); 89 array[0] = key1; 90 array[1] = key2; 91 array[2] = key3; 92 return array; 93 } 94 95 private static <T> T[] newArray(final T key1, final T key2, final T key3, final T key4) { 96 @SuppressWarnings("unchecked") 97 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3, key4), 4); 98 array[0] = key1; 99 array[1] = key2; 100 array[2] = key3; 101 array[3] = key4; 102 return array; 103 } 104 105 private static <T> T[] newArray(final T key1, final T key2, final T key3, final T key4, final T key5) { 106 @SuppressWarnings("unchecked") 107 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3, key4, key5), 5); 108 array[0] = key1; 109 array[1] = key2; 110 array[2] = key3; 111 array[3] = key4; 112 array[4] = key5; 113 return array; 114 } 115 116 /** The individual keys */ 117 private final K[] keys; 118 119 /** The cached hashCode */ 120 private transient int hashCode; 121 122 /** 123 * Constructor taking two keys. 124 * <p> 125 * The keys should be immutable. 126 * If they are not then they must not be changed after adding to the MultiKey. 127 * </p> 128 * 129 * @param key1 the first key 130 * @param key2 the second key 131 */ 132 public MultiKey(final K key1, final K key2) { 133 this(newArray(key1, key2), false); 134 } 135 136 /** 137 * Constructor taking three keys. 138 * <p> 139 * The keys should be immutable 140 * If they are not then they must not be changed after adding to the MultiKey. 141 * </p> 142 * 143 * @param key1 the first key 144 * @param key2 the second key 145 * @param key3 the third key 146 */ 147 public MultiKey(final K key1, final K key2, final K key3) { 148 this(newArray(key1, key2, key3), false); 149 } 150 151 /** 152 * Constructor taking four keys. 153 * <p> 154 * The keys should be immutable. 155 * If they are not then they must not be changed after adding to the MultiKey. 156 * </p> 157 * 158 * @param key1 the first key 159 * @param key2 the second key 160 * @param key3 the third key 161 * @param key4 the fourth key 162 */ 163 public MultiKey(final K key1, final K key2, final K key3, final K key4) { 164 this(newArray(key1, key2, key3, key4), false); 165 } 166 167 /** 168 * Constructor taking five keys. 169 * <p> 170 * The keys should be immutable. 171 * If they are not then they must not be changed after adding to the MultiKey. 172 * </p> 173 * 174 * @param key1 the first key 175 * @param key2 the second key 176 * @param key3 the third key 177 * @param key4 the fourth key 178 * @param key5 the fifth key 179 */ 180 public MultiKey(final K key1, final K key2, final K key3, final K key4, final K key5) { 181 this(newArray(key1, key2, key3, key4, key5), false); 182 } 183 184 /** 185 * Constructor taking an array of keys which is cloned. 186 * <p> 187 * The keys should be immutable. 188 * If they are not then they must not be changed after adding to the MultiKey. 189 * </p> 190 * <p> 191 * This is equivalent to {@code new MultiKey(keys, true)}. 192 * </p> 193 * 194 * @param keys the array of keys, not null 195 * @throws NullPointerException if the key array is null 196 */ 197 public MultiKey(final K[] keys) { 198 this(keys, true); 199 } 200 201 /** 202 * Constructor taking an array of keys, optionally choosing whether to clone. 203 * <p> 204 * <strong>If the array is not cloned, then it must not be modified.</strong> 205 * </p> 206 * <p> 207 * This method is public for performance reasons only, to avoid a clone. 208 * The hash code is calculated once here in this method. 209 * Therefore, changing the array passed in would not change the hash code but 210 * would change the equals method, which is a bug. 211 * </p> 212 * <p> 213 * This is the only fully safe usage of this constructor, as the object array 214 * is never made available in a variable: 215 * <pre> 216 * new MultiKey(new Object[] {...}, false); 217 * </pre> 218 * <p> 219 * The keys should be immutable. 220 * If they are not then they must not be changed after adding to the MultiKey. 221 * </p> 222 * 223 * @param keys the array of keys, not null 224 * @param makeClone true to clone the array, false to assign it 225 * @throws NullPointerException if the key array is null 226 * @since 3.1 227 */ 228 public MultiKey(final K[] keys, final boolean makeClone) { 229 Objects.requireNonNull(keys, "keys"); 230 this.keys = makeClone ? keys.clone() : keys; 231 calculateHashCode(keys); 232 } 233 234 /** 235 * Calculate the hash code of the instance using the provided keys. 236 * @param keys the keys to calculate the hash code for 237 */ 238 private void calculateHashCode(final Object[] keys) { 239 int total = 0; 240 for (final Object key : keys) { 241 if (key != null) { 242 total ^= key.hashCode(); 243 } 244 } 245 hashCode = total; 246 } 247 248 /** 249 * Compares this object to another. 250 * <p> 251 * To be equal, the other object must be a {@code MultiKey} with the 252 * same number of keys which are also equal. 253 * </p> 254 * 255 * @param other the other object to compare to 256 * @return true if equal 257 */ 258 @Override 259 public boolean equals(final Object other) { 260 if (other == this) { 261 return true; 262 } 263 if (other instanceof MultiKey) { 264 final MultiKey<?> otherMulti = (MultiKey<?>) other; 265 return Arrays.equals(keys, otherMulti.keys); 266 } 267 return false; 268 } 269 270 /** 271 * Gets the key at the specified index. 272 * <p> 273 * The key should be immutable. 274 * If it is not then it must not be changed. 275 * </p> 276 * 277 * @param index the index to retrieve 278 * @return the key at the index 279 * @throws IndexOutOfBoundsException if the index is invalid 280 * @since 3.1 281 */ 282 public K getKey(final int index) { 283 return keys[index]; 284 } 285 286 /** 287 * Gets a clone of the array of keys. 288 * <p> 289 * The keys should be immutable 290 * If they are not then they must not be changed. 291 * </p> 292 * 293 * @return the individual keys 294 */ 295 public K[] getKeys() { 296 return keys.clone(); 297 } 298 299 /** 300 * Gets the combined hash code that is computed from all the keys. 301 * <p> 302 * This value is computed once and then cached, so elements should not 303 * change their hash codes once created (note that this is the same 304 * constraint that would be used if the individual keys elements were 305 * themselves {@link java.util.Map Map} keys). 306 * </p> 307 * 308 * @return the hash code 309 */ 310 @Override 311 public int hashCode() { 312 return hashCode; 313 } 314 315 /** 316 * Recalculate the hash code after deserialization. The hash code of some 317 * keys might have change (hash codes based on the system hash code are 318 * only stable for the same process). 319 * @return the instance with recalculated hash code 320 */ 321 protected Object readResolve() { 322 calculateHashCode(keys); 323 return this; 324 } 325 326 /** 327 * Gets the size of the list of keys. 328 * 329 * @return the size of the list of keys 330 * @since 3.1 331 */ 332 public int size() { 333 return keys.length; 334 } 335 336 /** 337 * Gets a debugging string version of the key. 338 * 339 * @return a debugging string 340 */ 341 @Override 342 public String toString() { 343 return "MultiKey" + Arrays.toString(keys); 344 } 345 }