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 * https://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.Map;
23 import java.util.Objects;
24
25 /**
26 * A {@code MultiKey} allows multiple map keys to be merged together.
27 * <p>
28 * The purpose of this class is to avoid the need to write code to handle
29 * maps of maps. An example might be the need to look up a file name by
30 * key and locale. The typical solution might be nested maps. This class
31 * can be used instead by creating an instance passing in the key and locale.
32 * </p>
33 * <p>
34 * Example usage:
35 * </p>
36 * <pre>
37 * // populate map with data mapping key+locale to localizedText
38 * Map map = new HashMap();
39 * MultiKey multiKey = new MultiKey(key, locale);
40 * map.put(multiKey, localizedText);
41 *
42 * // later retrieve the localized text
43 * MultiKey multiKey = new MultiKey(key, locale);
44 * String localizedText = (String) map.get(multiKey);
45 * </pre>
46 *
47 * @param <K> The type of keys
48 * @since 3.0
49 */
50 public class MultiKey<K> implements Serializable {
51 // This class could implement List, but that would confuse its purpose
52
53 /** Serialization version */
54 private static final long serialVersionUID = 4465448607415788805L;
55
56 @SuppressWarnings("unchecked")
57 private static <T> Class<? extends T> getClass(final T value) {
58 return (Class<? extends T>) (value == null ? Object.class : value.getClass());
59 }
60
61 @SafeVarargs
62 private static <T> Class<? extends T> getComponentType(final T... values) {
63 @SuppressWarnings("unchecked")
64 final Class<? extends T> rootClass = (Class<? extends T>) Object.class;
65 if (values == null) {
66 return rootClass;
67 }
68 Class<? extends T> prevClass = values.length > 0 ? getClass(values[0]) : rootClass;
69 for (int i = 1; i < values.length; i++) {
70 final Class<? extends T> classI = getClass(values[i]);
71 if (prevClass != classI) {
72 return rootClass;
73 }
74 prevClass = classI;
75 }
76 return prevClass;
77 }
78
79 private static <T> T[] newArray(final T key1, final T key2) {
80 @SuppressWarnings("unchecked")
81 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2), 2);
82 array[0] = key1;
83 array[1] = key2;
84 return array;
85 }
86
87 private static <T> T[] newArray(final T key1, final T key2, final T key3) {
88 @SuppressWarnings("unchecked")
89 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3), 3);
90 array[0] = key1;
91 array[1] = key2;
92 array[2] = key3;
93 return array;
94 }
95
96 private static <T> T[] newArray(final T key1, final T key2, final T key3, final T key4) {
97 @SuppressWarnings("unchecked")
98 final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3, key4), 4);
99 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 }