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.collections.keyvalue;
18  
19  import java.io.Serializable;
20  import java.util.Arrays;
21  
22  /** 
23   * A <code>MultiKey</code> allows multiple map keys to be merged together.
24   * <p>
25   * The purpose of this class is to avoid the need to write code to handle
26   * maps of maps. An example might be the need to look up a file name by 
27   * key and locale. The typical solution might be nested maps. This class
28   * can be used instead by creating an instance passing in the key and locale.
29   * <p>
30   * Example usage:
31   * <pre>
32   * // populate map with data mapping key+locale to localizedText
33   * Map map = new HashMap();
34   * MultiKey multiKey = new MultiKey(key, locale);
35   * map.put(multiKey, localizedText);
36   *
37   * // later retrieve the localized text
38   * MultiKey multiKey = new MultiKey(key, locale);
39   * String localizedText = (String) map.get(multiKey);
40   * </pre>
41   *
42   * @since 3.0
43   * @version $Id: MultiKey.java 1429905 2013-01-07 17:15:14Z ggregory $
44   */
45  public class MultiKey<K> implements Serializable {
46      // This class could implement List, but that would confuse it's purpose
47  
48      /** Serialisation version */
49      private static final long serialVersionUID = 4465448607415788805L;
50  
51      /** The individual keys */
52      private final K[] keys;
53      /** The cached hashCode */
54      private transient int hashCode;
55  
56      /**
57       * Constructor taking two keys.
58       * <p>
59       * The keys should be immutable
60       * If they are not then they must not be changed after adding to the MultiKey.
61       * 
62       * @param key1  the first key
63       * @param key2  the second key
64       */
65      @SuppressWarnings("unchecked")
66      public MultiKey(final K key1, final K key2) {
67          this((K[]) new Object[] { key1, key2 }, false);
68      }
69  
70      /**
71       * Constructor taking three keys.
72       * <p>
73       * The keys should be immutable
74       * If they are not then they must not be changed after adding to the MultiKey.
75       * 
76       * @param key1  the first key
77       * @param key2  the second key
78       * @param key3  the third key
79       */
80      @SuppressWarnings("unchecked")
81      public MultiKey(final K key1, final K key2, final K key3) {
82          this((K[]) new Object[] {key1, key2, key3}, false);
83      }
84  
85      /**
86       * Constructor taking four keys.
87       * <p>
88       * The keys should be immutable
89       * If they are not then they must not be changed after adding to the MultiKey.
90       * 
91       * @param key1  the first key
92       * @param key2  the second key
93       * @param key3  the third key
94       * @param key4  the fourth key
95       */
96      @SuppressWarnings("unchecked")
97      public MultiKey(final K key1, final K key2, final K key3, final K key4) {
98          this((K[]) new Object[] {key1, key2, key3, key4}, false);
99      }
100 
101     /**
102      * Constructor taking five keys.
103      * <p>
104      * The keys should be immutable
105      * If they are not then they must not be changed after adding to the MultiKey.
106      * 
107      * @param key1  the first key
108      * @param key2  the second key
109      * @param key3  the third key
110      * @param key4  the fourth key
111      * @param key5  the fifth key
112      */
113     @SuppressWarnings("unchecked")
114     public MultiKey(final K key1, final K key2, final K key3, final K key4, final K key5) {
115         this((K[]) new Object[] {key1, key2, key3, key4, key5}, false);
116     }
117 
118     /**
119      * Constructor taking an array of keys which is cloned.
120      * <p>
121      * The keys should be immutable
122      * If they are not then they must not be changed after adding to the MultiKey.
123      * <p>
124      * This is equivalent to <code>new MultiKey(keys, true)</code>.
125      *
126      * @param keys  the array of keys, not null
127      * @throws IllegalArgumentException if the key array is null
128      */
129     public MultiKey(final K[] keys) {
130         this(keys, true);
131     }
132 
133     /**
134      * Constructor taking an array of keys, optionally choosing whether to clone.
135      * <p>
136      * <b>If the array is not cloned, then it must not be modified.</b>
137      * <p>
138      * This method is public for performance reasons only, to avoid a clone.
139      * The hashcode is calculated once here in this method.
140      * Therefore, changing the array passed in would not change the hashcode but
141      * would change the equals method, which is a bug.
142      * <p>
143      * This is the only fully safe usage of this constructor, as the object array
144      * is never made available in a variable:
145      * <pre>
146      * new MultiKey(new Object[] {...}, false);
147      * </pre>
148      * <p>
149      * The keys should be immutable
150      * If they are not then they must not be changed after adding to the MultiKey.
151      *
152      * @param keys  the array of keys, not null
153      * @param makeClone  true to clone the array, false to assign it
154      * @throws IllegalArgumentException if the key array is null
155      * @since 3.1
156      */
157     public MultiKey(final K[] keys, final boolean makeClone) {
158         super();
159         if (keys == null) {
160             throw new IllegalArgumentException("The array of keys must not be null");
161         }
162         if (makeClone) {
163             this.keys = keys.clone();
164         } else {
165             this.keys = keys;
166         }
167 
168         calculateHashCode(keys);
169     }
170 
171     //-----------------------------------------------------------------------
172     /**
173      * Gets a clone of the array of keys.
174      * <p>
175      * The keys should be immutable
176      * If they are not then they must not be changed.
177      * 
178      * @return the individual keys
179      */
180     public K[] getKeys() {
181         return keys.clone();
182     }
183 
184     /**
185      * Gets the key at the specified index.
186      * <p>
187      * The key should be immutable.
188      * If it is not then it must not be changed.
189      * 
190      * @param index  the index to retrieve
191      * @return the key at the index
192      * @throws IndexOutOfBoundsException if the index is invalid
193      * @since 3.1
194      */
195     public K getKey(final int index) {
196         return keys[index];
197     }
198 
199     /**
200      * Gets the size of the list of keys.
201      * 
202      * @return the size of the list of keys
203      * @since 3.1
204      */
205     public int size() {
206         return keys.length;
207     }
208 
209     //-----------------------------------------------------------------------
210     /**
211      * Compares this object to another.
212      * <p>
213      * To be equal, the other object must be a <code>MultiKey</code> with the
214      * same number of keys which are also equal.
215      * 
216      * @param other  the other object to compare to
217      * @return true if equal
218      */
219     @Override
220     public boolean equals(final Object other) {
221         if (other == this) {
222             return true;
223         }
224         if (other instanceof MultiKey) {
225             final MultiKey<?> otherMulti = (MultiKey<?>) other;
226             return Arrays.equals(keys, otherMulti.keys);
227         }
228         return false;
229     }
230 
231     /**
232      * Gets the combined hash code that is computed from all the keys.
233      * <p>
234      * This value is computed once and then cached, so elements should not
235      * change their hash codes once created (note that this is the same 
236      * constraint that would be used if the individual keys elements were
237      * themselves {@link java.util.Map Map} keys.
238      * 
239      * @return the hash code
240      */
241     @Override
242     public int hashCode() {
243         return hashCode;
244     }
245 
246     /**
247      * Gets a debugging string version of the key.
248      * 
249      * @return a debugging string
250      */
251     @Override
252     public String toString() {
253         return "MultiKey" + Arrays.asList(keys).toString();
254     }
255 
256     /**
257      * Calculate the hash code of the instance using the provided keys.
258      * @param keys the keys to calculate the hash code for
259      */
260     private void calculateHashCode(final Object[] keys)
261     {
262         int total = 0;
263         for (final Object key : keys) {
264             if (key != null) {
265                 total ^= key.hashCode();
266             }
267         }
268         hashCode = total;
269     }
270     
271     /**
272      * Recalculate the hash code after deserialization. The hash code of some
273      * keys might have change (hash codes based on the system hash code are
274      * only stable for the same process). 
275      * @return the instance with recalculated hash code
276      */
277     private Object readResolve() {
278         calculateHashCode(keys);
279         return this;
280     }
281 }