View Javadoc
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 }