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