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 /** Serialization 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 }