001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.collections4.keyvalue; 018 019import java.io.Serializable; 020import java.lang.reflect.Array; 021import java.util.Arrays; 022import java.util.Objects; 023 024/** 025 * A {@code MultiKey} allows multiple map keys to be merged together. 026 * <p> 027 * The purpose of this class is to avoid the need to write code to handle 028 * maps of maps. An example might be the need to look up a file name by 029 * key and locale. The typical solution might be nested maps. This class 030 * can be used instead by creating an instance passing in the key and locale. 031 * </p> 032 * <p> 033 * Example usage: 034 * </p> 035 * <pre> 036 * // populate map with data mapping key+locale to localizedText 037 * Map map = new HashMap(); 038 * MultiKey multiKey = new MultiKey(key, locale); 039 * map.put(multiKey, localizedText); 040 * 041 * // later retrieve the localized text 042 * MultiKey multiKey = new MultiKey(key, locale); 043 * String localizedText = (String) map.get(multiKey); 044 * </pre> 045 * 046 * @param <K> the type of keys 047 * @since 3.0 048 */ 049public class MultiKey<K> implements Serializable { 050 // This class could implement List, but that would confuse its purpose 051 052 /** Serialisation version */ 053 private static final long serialVersionUID = 4465448607415788805L; 054 055 @SuppressWarnings("unchecked") 056 private static <T> Class<? extends T> getClass(final T value) { 057 return (Class<? extends T>) (value == null ? Object.class : value.getClass()); 058 } 059 060 @SafeVarargs 061 private static <T> Class<? extends T> getComponentType(final T... values) { 062 @SuppressWarnings("unchecked") 063 final Class<? extends T> rootClass = (Class<? extends T>) Object.class; 064 if (values == null) { 065 return rootClass; 066 } 067 Class<? extends T> prevClass = values.length > 0 ? getClass(values[0]) : rootClass; 068 for (int i = 1; i < values.length; i++) { 069 final Class<? extends T> classI = getClass(values[i]); 070 if (prevClass != classI) { 071 return rootClass; 072 } 073 prevClass = classI; 074 } 075 return prevClass; 076 } 077 078 private static <T> T[] newArray(final T key1, final T key2) { 079 @SuppressWarnings("unchecked") 080 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2), 2); 081 array[0] = key1; 082 array[1] = key2; 083 return array; 084 } 085 086 private static <T> T[] newArray(final T key1, final T key2, final T key3) { 087 @SuppressWarnings("unchecked") 088 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3), 3); 089 array[0] = key1; 090 array[1] = key2; 091 array[2] = key3; 092 return array; 093 } 094 095 private static <T> T[] newArray(final T key1, final T key2, final T key3, final T key4) { 096 @SuppressWarnings("unchecked") 097 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3, key4), 4); 098 array[0] = key1; 099 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}