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      private static <T> Class<? extends T> getComponentType(final T... values) {
61          @SuppressWarnings("unchecked")
62          final Class<? extends T> rootClass = (Class<? extends T>) Object.class;
63          if (values == null) {
64              return rootClass;
65          }
66          Class<? extends T> prevClass = values.length > 0 ? getClass(values[0]) : rootClass;
67          for (int i = 1; i < values.length; i++) {
68              final Class<? extends T> classI = getClass(values[i]);
69              if (prevClass != classI) {
70                  return rootClass;
71              }
72              prevClass = classI;
73          }
74          return prevClass;
75      }
76  
77      private static <T> T[] newArray(final T key1, final T key2) {
78          @SuppressWarnings("unchecked")
79          final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2), 2);
80          array[0] = key1;
81          array[1] = key2;
82          return array;
83      }
84  
85      private static <T> T[] newArray(final T key1, final T key2, final T key3) {
86          @SuppressWarnings("unchecked")
87          final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3), 3);
88          array[0] = key1;
89          array[1] = key2;
90          array[2] = key3;
91          return array;
92      }
93  
94      private static <T> T[] newArray(final T key1, final T key2, final T key3, final T key4) {
95          @SuppressWarnings("unchecked")
96          final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3, key4), 4);
97          array[0] = key1;
98          array[1] = key2;
99          array[2] = key3;
100         array[3] = key4;
101         return array;
102     }
103 
104     private static <T> T[] newArray(final T key1, final T key2, final T key3, final T key4, final T key5) {
105         @SuppressWarnings("unchecked")
106         final T[] array = (T[]) Array.newInstance(getComponentType(key1, key2, key3, key4, key5), 5);
107         array[0] = key1;
108         array[1] = key2;
109         array[2] = key3;
110         array[3] = key4;
111         array[4] = key5;
112         return array;
113     }
114 
115     /** The individual keys */
116     private final K[] keys;
117 
118     /** The cached hashCode */
119     private transient int hashCode;
120 
121     /**
122      * Constructor taking two keys.
123      * <p>
124      * The keys should be immutable
125      * If they are not then they must not be changed after adding to the MultiKey.
126      *
127      * @param key1  the first key
128      * @param key2  the second key
129      */
130     public MultiKey(final K key1, final K key2) {
131         this(newArray(key1, key2), false);
132     }
133 
134     /**
135      * Constructor taking three keys.
136      * <p>
137      * The keys should be immutable
138      * If they are not then they must not be changed after adding to the MultiKey.
139      *
140      * @param key1  the first key
141      * @param key2  the second key
142      * @param key3  the third key
143      */
144     public MultiKey(final K key1, final K key2, final K key3) {
145         this(newArray(key1, key2, key3), false);
146     }
147 
148     /**
149      * Constructor taking four keys.
150      * <p>
151      * The keys should be immutable
152      * If they are not then they must not be changed after adding to the MultiKey.
153      *
154      * @param key1  the first key
155      * @param key2  the second key
156      * @param key3  the third key
157      * @param key4  the fourth key
158      */
159     public MultiKey(final K key1, final K key2, final K key3, final K key4) {
160         this(newArray(key1, key2, key3, key4), false);
161     }
162 
163     /**
164      * Constructor taking five keys.
165      * <p>
166      * The keys should be immutable
167      * If they are not then they must not be changed after adding to the MultiKey.
168      *
169      * @param key1  the first key
170      * @param key2  the second key
171      * @param key3  the third key
172      * @param key4  the fourth key
173      * @param key5  the fifth key
174      */
175     public MultiKey(final K key1, final K key2, final K key3, final K key4, final K key5) {
176         this(newArray(key1, key2, key3, key4, key5), false);
177     }
178 
179     /**
180      * Constructor taking an array of keys which is cloned.
181      * <p>
182      * The keys should be immutable
183      * If they are not then they must not be changed after adding to the MultiKey.
184      * <p>
185      * This is equivalent to {@code new MultiKey(keys, true)}.
186      *
187      * @param keys  the array of keys, not null
188      * @throws NullPointerException if the key array is null
189      */
190     public MultiKey(final K[] keys) {
191         this(keys, true);
192     }
193 
194     /**
195      * Constructor taking an array of keys, optionally choosing whether to clone.
196      * <p>
197      * <b>If the array is not cloned, then it must not be modified.</b>
198      * <p>
199      * This method is public for performance reasons only, to avoid a clone.
200      * The hash code is calculated once here in this method.
201      * Therefore, changing the array passed in would not change the hash code but
202      * would change the equals method, which is a bug.
203      * <p>
204      * This is the only fully safe usage of this constructor, as the object array
205      * is never made available in a variable:
206      * <pre>
207      * new MultiKey(new Object[] {...}, false);
208      * </pre>
209      * <p>
210      * The keys should be immutable
211      * If they are not then they must not be changed after adding to the MultiKey.
212      *
213      * @param keys  the array of keys, not null
214      * @param makeClone  true to clone the array, false to assign it
215      * @throws NullPointerException if the key array is null
216      * @since 3.1
217      */
218     public MultiKey(final K[] keys, final boolean makeClone) {
219         Objects.requireNonNull(keys, "keys");
220         this.keys = makeClone ? keys.clone() : keys;
221         calculateHashCode(keys);
222     }
223 
224     /**
225      * Calculate the hash code of the instance using the provided keys.
226      * @param keys the keys to calculate the hash code for
227      */
228     private void calculateHashCode(final Object[] keys) {
229         int total = 0;
230         for (final Object key : keys) {
231             if (key != null) {
232                 total ^= key.hashCode();
233             }
234         }
235         hashCode = total;
236     }
237 
238     /**
239      * Compares this object to another.
240      * <p>
241      * To be equal, the other object must be a {@code MultiKey} with the
242      * same number of keys which are also equal.
243      *
244      * @param other  the other object to compare to
245      * @return true if equal
246      */
247     @Override
248     public boolean equals(final Object other) {
249         if (other == this) {
250             return true;
251         }
252         if (other instanceof MultiKey) {
253             final MultiKey<?> otherMulti = (MultiKey<?>) other;
254             return Arrays.equals(keys, otherMulti.keys);
255         }
256         return false;
257     }
258 
259     /**
260      * Gets the key at the specified index.
261      * <p>
262      * The key should be immutable.
263      * If it is not then it must not be changed.
264      *
265      * @param index  the index to retrieve
266      * @return the key at the index
267      * @throws IndexOutOfBoundsException if the index is invalid
268      * @since 3.1
269      */
270     public K getKey(final int index) {
271         return keys[index];
272     }
273 
274     /**
275      * Gets a clone of the array of keys.
276      * <p>
277      * The keys should be immutable
278      * If they are not then they must not be changed.
279      *
280      * @return the individual keys
281      */
282     public K[] getKeys() {
283         return keys.clone();
284     }
285 
286     /**
287      * Gets the combined hash code that is computed from all the keys.
288      * <p>
289      * This value is computed once and then cached, so elements should not
290      * change their hash codes once created (note that this is the same
291      * constraint that would be used if the individual keys elements were
292      * themselves {@link java.util.Map Map} keys).
293      *
294      * @return the hash code
295      */
296     @Override
297     public int hashCode() {
298         return hashCode;
299     }
300 
301     /**
302      * Recalculate the hash code after deserialization. The hash code of some
303      * keys might have change (hash codes based on the system hash code are
304      * only stable for the same process).
305      * @return the instance with recalculated hash code
306      */
307     protected Object readResolve() {
308         calculateHashCode(keys);
309         return this;
310     }
311 
312     /**
313      * Gets the size of the list of keys.
314      *
315      * @return the size of the list of keys
316      * @since 3.1
317      */
318     public int size() {
319         return keys.length;
320     }
321 
322     /**
323      * Gets a debugging string version of the key.
324      *
325      * @return a debugging string
326      */
327     @Override
328     public String toString() {
329         return "MultiKey" + Arrays.toString(keys);
330     }
331 }