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 * https://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.Map; 023import java.util.Objects; 024 025/** 026 * A {@code MultiKey} allows multiple map keys to be merged together. 027 * <p> 028 * The purpose of this class is to avoid the need to write code to handle 029 * maps of maps. An example might be the need to look up a file name by 030 * key and locale. The typical solution might be nested maps. This class 031 * can be used instead by creating an instance passing in the key and locale. 032 * </p> 033 * <p> 034 * Example usage: 035 * </p> 036 * <pre> 037 * // populate map with data mapping key+locale to localizedText 038 * Map map = new HashMap(); 039 * MultiKey multiKey = new MultiKey(key, locale); 040 * map.put(multiKey, localizedText); 041 * 042 * // later retrieve the localized text 043 * MultiKey multiKey = new MultiKey(key, locale); 044 * String localizedText = (String) map.get(multiKey); 045 * </pre> 046 * 047 * @param <K> The type of keys 048 * @since 3.0 049 */ 050public class MultiKey<K> implements Serializable { 051 // This class could implement List, but that would confuse its purpose 052 053 /** Serialization version */ 054 private static final long serialVersionUID = 4465448607415788805L; 055 056 @SuppressWarnings("unchecked") 057 private static <T> Class<? extends T> getClass(final T value) { 058 return (Class<? extends T>) (value == null ? Object.class : value.getClass()); 059 } 060 061 @SafeVarargs 062 private static <T> Class<? extends T> getComponentType(final T... values) { 063 @SuppressWarnings("unchecked") 064 final Class<? extends T> rootClass = (Class<? extends T>) Object.class; 065 if (values == null) { 066 return rootClass; 067 } 068 Class<? extends T> prevClass = values.length > 0 ? getClass(values[0]) : rootClass; 069 for (int i = 1; i < values.length; i++) { 070 final Class<? extends T> classI = getClass(values[i]); 071 if (prevClass != classI) { 072 return rootClass; 073 } 074 prevClass = classI; 075 } 076 return prevClass; 077 } 078 079 private static <T> T[] newArray(final T key1, final T key2) { 080 @SuppressWarnings("unchecked") 081 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2), 2); 082 array[0] = key1; 083 array[1] = key2; 084 return array; 085 } 086 087 private static <T> T[] newArray(final T key1, final T key2, final T key3) { 088 @SuppressWarnings("unchecked") 089 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3), 3); 090 array[0] = key1; 091 array[1] = key2; 092 array[2] = key3; 093 return array; 094 } 095 096 private static <T> T[] newArray(final T key1, final T key2, final T key3, final T key4) { 097 @SuppressWarnings("unchecked") 098 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3, key4), 4); 099 array[0] = key1; 100 array[1] = key2; 101 array[2] = key3; 102 array[3] = key4; 103 return array; 104 } 105 106 private static <T> T[] newArray(final T key1, final T key2, final T key3, final T key4, final T key5) { 107 @SuppressWarnings("unchecked") 108 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3, key4, key5), 5); 109 array[0] = key1; 110 array[1] = key2; 111 array[2] = key3; 112 array[3] = key4; 113 array[4] = key5; 114 return array; 115 } 116 117 /** The individual keys */ 118 private final K[] keys; 119 120 /** The cached hashCode */ 121 private transient int hashCode; 122 123 /** 124 * Constructor taking two keys. 125 * <p> 126 * The keys should be immutable. 127 * If they are not then they must not be changed after adding to the MultiKey. 128 * </p> 129 * 130 * @param key1 The first key 131 * @param key2 The second key 132 */ 133 public MultiKey(final K key1, final K key2) { 134 this(newArray(key1, key2), false); 135 } 136 137 /** 138 * Constructor taking three keys. 139 * <p> 140 * The keys should be immutable 141 * If they are not then they must not be changed after adding to the MultiKey. 142 * </p> 143 * 144 * @param key1 The first key 145 * @param key2 The second key 146 * @param key3 The third key 147 */ 148 public MultiKey(final K key1, final K key2, final K key3) { 149 this(newArray(key1, key2, key3), false); 150 } 151 152 /** 153 * Constructor taking four keys. 154 * <p> 155 * The keys should be immutable. 156 * If they are not then they must not be changed after adding to the MultiKey. 157 * </p> 158 * 159 * @param key1 The first key 160 * @param key2 The second key 161 * @param key3 The third key 162 * @param key4 The fourth key 163 */ 164 public MultiKey(final K key1, final K key2, final K key3, final K key4) { 165 this(newArray(key1, key2, key3, key4), false); 166 } 167 168 /** 169 * Constructor taking five keys. 170 * <p> 171 * The keys should be immutable. 172 * If they are not then they must not be changed after adding to the MultiKey. 173 * </p> 174 * 175 * @param key1 The first key 176 * @param key2 The second key 177 * @param key3 The third key 178 * @param key4 The fourth key 179 * @param key5 The fifth key 180 */ 181 public MultiKey(final K key1, final K key2, final K key3, final K key4, final K key5) { 182 this(newArray(key1, key2, key3, key4, key5), false); 183 } 184 185 /** 186 * Constructor taking an array of keys which is cloned. 187 * <p> 188 * The keys should be immutable. 189 * If they are not then they must not be changed after adding to the MultiKey. 190 * </p> 191 * <p> 192 * This is equivalent to {@code new MultiKey(keys, true)}. 193 * </p> 194 * 195 * @param keys The array of keys, not null 196 * @throws NullPointerException if the key array is null 197 */ 198 public MultiKey(final K[] keys) { 199 this(keys, true); 200 } 201 202 /** 203 * Constructor taking an array of keys, optionally choosing whether to clone. 204 * <p> 205 * <strong>If the array is not cloned, then it must not be modified.</strong> 206 * </p> 207 * <p> 208 * This method is public for performance reasons only, to avoid a clone. 209 * The hash code is calculated once here in this method. 210 * Therefore, changing the array passed in would not change the hash code but 211 * would change the equals method, which is a bug. 212 * </p> 213 * <p> 214 * This is the only fully safe usage of this constructor, as the object array 215 * is never made available in a variable: 216 * <pre> 217 * new MultiKey(new Object[] {...}, false); 218 * </pre> 219 * <p> 220 * The keys should be immutable. 221 * If they are not then they must not be changed after adding to the MultiKey. 222 * </p> 223 * 224 * @param keys The array of keys, not null 225 * @param makeClone true to clone the array, false to assign it 226 * @throws NullPointerException if the key array is null 227 * @since 3.1 228 */ 229 public MultiKey(final K[] keys, final boolean makeClone) { 230 Objects.requireNonNull(keys, "keys"); 231 this.keys = makeClone ? keys.clone() : keys; 232 calculateHashCode(keys); 233 } 234 235 /** 236 * Calculate the hash code of the instance using the provided keys. 237 * 238 * @param keys The keys to calculate the hash code for 239 */ 240 private void calculateHashCode(final Object[] keys) { 241 int total = 0; 242 for (final Object key : keys) { 243 if (key != null) { 244 total ^= key.hashCode(); 245 } 246 } 247 hashCode = total; 248 } 249 250 /** 251 * Compares this object to another. 252 * <p> 253 * To be equal, the other object must be a {@code MultiKey} with the 254 * same number of keys which are also equal. 255 * </p> 256 * 257 * @param other The other object to compare to 258 * @return true if equal 259 */ 260 @Override 261 public boolean equals(final Object other) { 262 if (other == this) { 263 return true; 264 } 265 if (other instanceof MultiKey) { 266 final MultiKey<?> otherMulti = (MultiKey<?>) other; 267 return Arrays.equals(keys, otherMulti.keys); 268 } 269 return false; 270 } 271 272 /** 273 * Gets the key at the specified index. 274 * <p> 275 * The key should be immutable. 276 * If it is not then it must not be changed. 277 * </p> 278 * 279 * @param index The index to retrieve 280 * @return The key at the index 281 * @throws IndexOutOfBoundsException if the index is invalid 282 * @since 3.1 283 */ 284 public K getKey(final int index) { 285 return keys[index]; 286 } 287 288 /** 289 * Gets a clone of the array of keys. 290 * <p> 291 * The keys should be immutable 292 * If they are not then they must not be changed. 293 * </p> 294 * 295 * @return The individual keys 296 */ 297 public K[] getKeys() { 298 return keys.clone(); 299 } 300 301 /** 302 * Gets the combined hash code that is computed from all the keys. 303 * <p> 304 * This value is computed once and then cached, so elements should not 305 * change their hash codes once created (note that this is the same 306 * constraint that would be used if the individual keys elements were 307 * themselves {@link Map Map} keys). 308 * </p> 309 * 310 * @return The hash code 311 */ 312 @Override 313 public int hashCode() { 314 return hashCode; 315 } 316 317 /** 318 * Recalculate the hash code after deserialization. The hash code of some 319 * keys might have change (hash codes based on the system hash code are 320 * only stable for the same process). 321 * 322 * @return The instance with recalculated hash code 323 */ 324 protected Object readResolve() { 325 calculateHashCode(keys); 326 return this; 327 } 328 329 /** 330 * Gets the size of the list of keys. 331 * 332 * @return The size of the list of keys 333 * @since 3.1 334 */ 335 public int size() { 336 return keys.length; 337 } 338 339 /** 340 * Gets a debugging string version of the key. 341 * 342 * @return A debugging string 343 */ 344 @Override 345 public String toString() { 346 return "MultiKey" + Arrays.toString(keys); 347 } 348}