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 }