Tue, 30 Apr 2013 10:05:42 -0300
8006220: Simplify PropertyMaps
Reviewed-by: hannesw, lagergren
Contributed-by: james.laskey@oracle.com
1 /*
2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
26 package jdk.nashorn.internal.runtime;
28 import java.util.Arrays;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.HashSet;
32 import java.util.Map;
33 import java.util.Set;
35 /**
36 * Immutable hash map implementation for properties. Properties are keyed on strings.
37 * Copying and cloning is avoided by relying on immutability.
38 * <p>
39 * When adding an element to a hash table, only the head of a bin list is updated, thus
40 * an add only requires the cloning of the bins array and adding an element to the head
41 * of the bin list. Similarly for removal, only a portion of a bin list is updated.
42 * <p>
43 * A separate chronological list is kept for quick generation of keys and values, and,
44 * for rehashing.
45 * <p>
46 * Details:
47 * <p>
48 * The main goal is to be able to retrieve properties from a map quickly, keying on
49 * the property name (String.) A secondary, but important goal, is to keep maps
50 * immutable, so that a map can be shared by multiple objects in a context.
51 * Sharing maps allows objects to be categorized as having similar properties, a
52 * fact that call site guards rely on. In this discussion, immutability allows us
53 * to significantly reduce the amount of duplication we have in our maps.
54 * <p>
55 * The simplest of immutable maps is a basic singly linked list. New properties
56 * are simply added to the head of the list. Ancestor maps are not affected by the
57 * addition, since they continue to refer to their own head. Searching is done by
58 * walking linearly though the elements until a match is found, O(N).
59 * <p>
60 * A hash map can be thought of as an optimization of a linked list map, where the
61 * linked list is broken into fragments based on hashCode(key) . An array is use
62 * to quickly reference these fragments, indexing on hashCode(key) mod tableSize
63 * (tableSize is typically a power of 2 so that the mod is a fast masking
64 * operation.) If the size of the table is sufficient large, then search time
65 * approaches O(1). In fact, most bins in a hash table are typically empty or
66 * contain a one element list.
67 * <p>
68 * For immutable hash maps, we can think of the hash map as an array of the shorter
69 * linked list maps. If we add an element to the head of one of those lists, it
70 * doesn't affect any ancestor maps. Thus adding an element to an immutable hash
71 * map only requires cloning the array and inserting an element at the head of one
72 * of the bins.
73 * <p>
74 * Using Java HashMaps we don't have enough control over the entries to allow us to
75 * implement this technique, so we are forced to clone the entire hash map.
76 * <p>
77 * Removing elements is done similarly. We clone the array and then only modify
78 * the bin containing the removed element. More often than not, the list contains
79 * only one element (or is very short), so this is not very costly. When the list
80 * has several items, we need to clone the list portion prior to the removed item.
81 * <p>
82 * Another requirement of property maps is that we need to be able to gather all
83 * properties in chronological (add) order. We have been using LinkedHashMap to
84 * provide this. For the implementation of immutable hash map, we use a singly
85 * linked list that is linked in reverse chronological order. This means we simply
86 * add new entries to the head of the list. If we need to work with the list in
87 * forward order, it's simply a matter of allocating an array (size is known) and
88 * back filling in reverse order. Removal of elements from the chronological list
89 * is trickier. LinkedHashMap uses a doubly linked list to give constant time
90 * removal. Immutable hash maps can't do that and maintain immutability. So we
91 * manage the chronological list the same way we manage the bins, cloning up to the
92 * point of removal. Don't panic. This cost is more than offset by the cost of
93 * cloning an entire LinkedHashMap. Plus removal is far more rare than addition.
94 * <p>
95 * One more optimization. Maps with a small number of entries don't use the hash
96 * map at all, the chronological list is used instead.
97 * <p>
98 * So the benefits from immutable arrays are; fewer objects and less copying. For
99 * immutable hash map, when no removal is involved, the number of elements per
100 * property is two (bin + chronological elements). For LinkedHashMap it is one
101 * (larger element) times the number of maps that refer to the property. For
102 * immutable hash map, addition is constant time. For LinkedHashMap it's O(N+C)
103 * since we have to clone the older map.
104 */
105 public final class PropertyHashMap implements Map <String, Property> {
106 /** Number of initial bins. Power of 2. */
107 private static final int INITIAL_BINS = 32;
109 /** Threshold before using bins. */
110 private static final int LIST_THRESHOLD = 8;
112 /** Initial map. */
113 public static final PropertyHashMap EMPTY_HASHMAP = new PropertyHashMap();
115 /** Number of properties in the map. */
116 private final int size;
118 /** Threshold before growing the bins. */
119 private final int threshold;
121 /** Reverse list of all properties. */
122 private final Element list;
124 /** Hash map bins. */
125 private final Element[] bins;
127 /** All properties as an array (lazy). */
128 private Property[] properties;
130 /**
131 * Empty map constructor.
132 */
133 private PropertyHashMap() {
134 this.size = 0;
135 this.threshold = 0;
136 this.bins = null;
137 this.list = null;
138 }
140 /**
141 * Clone Constructor
142 *
143 * @param map Original {@link PropertyHashMap}.
144 */
145 private PropertyHashMap(final PropertyHashMap map) {
146 this.size = map.size;
147 this.threshold = map.threshold;
148 this.bins = map.bins;
149 this.list = map.list;
150 }
152 /**
153 * Constructor used internally to extend a map
154 *
155 * @param size Size of the new {@link PropertyHashMap}.
156 * @param bins The hash bins.
157 * @param list The {@link Property} list.
158 */
159 private PropertyHashMap(final int size, final Element[] bins, final Element list) {
160 this.size = size;
161 this.threshold = bins != null ? threeQuarters(bins.length) : 0;
162 this.bins = bins;
163 this.list = list;
164 }
166 /**
167 * Clone a {@link PropertyHashMap} and add a {@link Property}.
168 *
169 * @param property {@link Property} to add.
170 *
171 * @return New {@link PropertyHashMap}.
172 */
173 public PropertyHashMap immutableAdd(final Property property) {
174 final int newSize = size + 1;
175 PropertyHashMap newMap = cloneMap(newSize);
176 newMap = newMap.addNoClone(property);
177 return newMap;
178 }
180 /**
181 * Clone a {@link PropertyHashMap} and add an array of properties.
182 *
183 * @param newProperties Properties to add.
184 *
185 * @return New {@link PropertyHashMap}.
186 */
187 public PropertyHashMap immutableAdd(final Property... newProperties) {
188 final int newSize = size + newProperties.length;
189 PropertyHashMap newMap = cloneMap(newSize);
190 for (final Property property : newProperties) {
191 newMap = newMap.addNoClone(property);
192 }
193 return newMap;
194 }
196 /**
197 * Clone a {@link PropertyHashMap} and add a collection of properties.
198 *
199 * @param newProperties Properties to add.
200 *
201 * @return New {@link PropertyHashMap}.
202 */
203 public PropertyHashMap immutableAdd(final Collection<Property> newProperties) {
204 if (newProperties != null) {
205 final int newSize = size + newProperties.size();
206 PropertyHashMap newMap = cloneMap(newSize);
207 for (final Property property : newProperties) {
208 newMap = newMap.addNoClone(property);
209 }
210 return newMap;
211 }
212 return this;
213 }
215 /**
216 * Clone a {@link PropertyHashMap} and remove a {@link Property}.
217 *
218 * @param property {@link Property} to remove.
219 *
220 * @return New {@link PropertyHashMap}.
221 */
222 public PropertyHashMap immutableRemove(final Property property) {
223 return immutableRemove(property.getKey());
224 }
226 /**
227 * Clone a {@link PropertyHashMap} and remove a {@link Property} based on its key.
228 *
229 * @param key Key of {@link Property} to remove.
230 *
231 * @return New {@link PropertyHashMap}.
232 */
233 public PropertyHashMap immutableRemove(final String key) {
234 if (bins != null) {
235 final int binIndex = binIndex(bins, key);
236 final Element bin = bins[binIndex];
237 if (findElement(bin, key) != null) {
238 final int newSize = size - 1;
239 Element[] newBins = null;
240 if (newSize >= LIST_THRESHOLD) {
241 newBins = bins.clone();
242 newBins[binIndex] = removeFromList(bin, key);
243 }
244 final Element newList = removeFromList(list, key);
245 return new PropertyHashMap(newSize, newBins, newList);
246 }
247 } else if (findElement(list, key) != null) {
248 final int newSize = size - 1;
249 return newSize != 0 ? new PropertyHashMap(newSize, null, removeFromList(list, key)) : EMPTY_HASHMAP;
250 }
251 return this;
252 }
254 /**
255 * Find a {@link Property} in the {@link PropertyHashMap}.
256 *
257 * @param key Key of {@link Property} to find.
258 *
259 * @return {@link Property} matching key or {@code null} if not found.
260 */
261 public Property find(final String key) {
262 final Element element = findElement(key);
263 return element != null ? element.getProperty() : null;
264 }
266 /**
267 * Return an array of properties in chronological order of adding.
268 *
269 * @return Array of all properties.
270 */
271 Property[] getProperties() {
272 if (properties == null) {
273 final Property[] array = new Property[size];
274 int i = size;
275 for (Element element = list; element != null; element = element.getLink()) {
276 array[--i] = element.getProperty();
277 }
278 properties = array;
279 }
280 return properties;
281 }
283 /**
284 * Returns the bin index from the key.
285 *
286 * @param bins The bins array.
287 * @param key {@link Property} key.
288 *
289 * @return The bin index.
290 */
291 private static int binIndex(final Element[] bins, final String key) {
292 return key.hashCode() & (bins.length - 1);
293 }
295 /**
296 * Calculate the number of bins needed to contain n properties.
297 *
298 * @param n Number of elements.
299 *
300 * @return Number of bins required.
301 */
302 private static int binsNeeded(final int n) {
303 // 50% padding
304 return 1 << (32 - Integer.numberOfLeadingZeros((n + (n >>> 1)) | (INITIAL_BINS - 1)));
305 }
307 /**
308 * Used to calculate the current capacity of the bins.
309 *
310 * @param n Number of bin slots.
311 *
312 * @return 75% of n.
313 */
314 private static int threeQuarters(final int n) {
315 return (n >>> 1) + (n >>> 2);
316 }
318 /**
319 * Regenerate the bin table after changing the number of bins.
320 *
321 * @param list // List of all properties.
322 * @param binSize // New size of bins.
323 *
324 * @return Populated bins.
325 */
326 private static Element[] rehash(final Element list, final int binSize) {
327 final Element[] newBins = new Element[binSize];
328 for (Element element = list; element != null; element = element.getLink()) {
329 final Property property = element.getProperty();
330 final String key = property.getKey();
331 final int binIndex = binIndex(newBins, key);
332 newBins[binIndex] = new Element(newBins[binIndex], property);
333 }
334 return newBins;
335 }
337 /**
338 * Locate an element based on key.
339 *
340 * @param key {@link Element} key.
341 *
342 * @return {@link Element} matching key or {@code null} if not found.
343 */
344 private Element findElement(final String key) {
345 if (bins != null) {
346 final int binIndex = binIndex(bins, key);
347 return findElement(bins[binIndex], key);
348 }
349 return findElement(list, key);
350 }
352 /**
353 * Locate an {@link Element} based on key from a specific list.
354 *
355 * @param elementList Head of {@link Element} list
356 * @param key {@link Element} key.
357 * @return {@link Element} matching key or {@code null} if not found.
358 */
359 private static Element findElement(final Element elementList, final String key) {
360 final int hashCode = key.hashCode();
361 for (Element element = elementList; element != null; element = element.getLink()) {
362 if (element.match(key, hashCode)) {
363 return element;
364 }
365 }
366 return null;
367 }
369 /**
370 * Clone {@link PropertyHashMap} to accommodate new size.
371 *
372 * @param newSize New size of {@link PropertyHashMap}.
373 *
374 * @return Cloned {@link PropertyHashMap} with new size.
375 */
376 private PropertyHashMap cloneMap(final int newSize) {
377 Element[] newBins;
378 if (bins == null && newSize <= LIST_THRESHOLD) {
379 newBins = null;
380 } else if (newSize > threshold) {
381 newBins = rehash(list, binsNeeded(newSize));
382 } else {
383 newBins = bins.clone();
384 }
385 return new PropertyHashMap(newSize, newBins, list);
386 }
388 /**
389 * Add a {@link Property} to a temporary {@link PropertyHashMap}, that has
390 * been already cloned. Removes duplicates if necessary.
391 *
392 * @param property {@link Property} to add.
393 *
394 * @return New {@link PropertyHashMap}.
395 */
396 private PropertyHashMap addNoClone(final Property property) {
397 int newSize = size;
398 final String key = property.getKey();
399 Element newList = list;
400 if (bins != null) {
401 final int binIndex = binIndex(bins, key);
402 Element bin = bins[binIndex];
403 if (findElement(bin, key) != null) {
404 newSize--;
405 bin = removeFromList(bin, key);
406 newList = removeFromList(list, key);
407 }
408 bins[binIndex] = new Element(bin, property);
409 } else {
410 if (findElement(list, key) != null) {
411 newSize--;
412 newList = removeFromList(list, key);
413 }
414 }
415 newList = new Element(newList, property);
416 return new PropertyHashMap(newSize, bins, newList);
417 }
419 /**
420 * Removes an {@link Element} from a specific list, avoiding duplication.
421 *
422 * @param list List to remove from.
423 * @param key Key of {@link Element} to remove.
424 *
425 * @return New list with {@link Element} removed.
426 */
427 private static Element removeFromList(final Element list, final String key) {
428 if (list == null) {
429 return null;
430 }
431 final int hashCode = key.hashCode();
432 if (list.match(key, hashCode)) {
433 return list.getLink();
434 }
435 final Element head = new Element(null, list.getProperty());
436 Element previous = head;
437 for (Element element = list.getLink(); element != null; element = element.getLink()) {
438 if (element.match(key, hashCode)) {
439 previous.setLink(element.getLink());
440 return head;
441 }
442 final Element next = new Element(null, element.getProperty());
443 previous.setLink(next);
444 previous = next;
445 }
446 return list;
447 }
449 /*
450 * Map implementation
451 */
453 @Override
454 public int size() {
455 return size;
456 }
458 @Override
459 public boolean isEmpty() {
460 return size == 0;
461 }
463 @Override
464 public boolean containsKey(final Object key) {
465 if (key instanceof String) {
466 return findElement((String)key) != null;
467 }
468 assert key instanceof String;
469 return false;
470 }
472 /**
473 * Check if the map contains a key.
474 *
475 * @param key {@link Property} key.
476 *
477 * @return {@code true} of key is in {@link PropertyHashMap}.
478 */
479 public boolean containsKey(final String key) {
480 return findElement(key) != null;
481 }
483 @Override
484 public boolean containsValue(final Object value) {
485 if (value instanceof Property) {
486 final Property property = (Property) value;
487 final Element element = findElement(property.getKey());
488 return element != null && element.getProperty().equals(value);
489 }
490 return false;
491 }
493 @Override
494 public Property get(final Object key) {
495 if (key instanceof String) {
496 final Element element = findElement((String)key);
497 return element != null ? element.getProperty() : null;
498 }
499 assert key instanceof String;
500 return null;
501 }
503 /**
504 * Get the {@link Property} given a key that is an explicit {@link String}.
505 * See also {@link PropertyHashMap#get(Object)}
506 *
507 * @param key {@link Property} key.
508 *
509 * @return {@link Property}, or {@code null} if no property with that key was found.
510 */
511 public Property get(final String key) {
512 final Element element = findElement(key);
513 return element != null ? element.getProperty() : null;
514 }
516 @Override
517 public Property put(final String key, final Property value) {
518 throw new UnsupportedOperationException("Immutable map.");
519 }
521 @Override
522 public Property remove(final Object key) {
523 throw new UnsupportedOperationException("Immutable map.");
524 }
526 @Override
527 public void putAll(final Map<? extends String, ? extends Property> m) {
528 throw new UnsupportedOperationException("Immutable map.");
529 }
531 @Override
532 public void clear() {
533 throw new UnsupportedOperationException("Immutable map.");
534 }
536 @Override
537 public Set<String> keySet() {
538 final HashSet<String> set = new HashSet<>();
539 for (Element element = list; element != null; element = element.getLink()) {
540 set.add(element.getKey());
541 }
542 return Collections.unmodifiableSet(set);
543 }
545 @Override
546 public Collection<Property> values() {
547 return Collections.unmodifiableList(Arrays.asList(getProperties()));
548 }
550 @Override
551 public Set<Entry<String, Property>> entrySet() {
552 final HashSet<Entry<String, Property>> set = new HashSet<>();
553 for (Element element = list; element != null; element = element.getLink()) {
554 set.add(element);
555 }
556 return Collections.unmodifiableSet(set);
557 }
559 /**
560 * List map element.
561 */
562 static final class Element implements Entry<String, Property> {
563 /** Link for list construction. */
564 private Element link;
566 /** Element property. */
567 private final Property property;
569 /** Element key. Kept separate for performance.) */
570 private final String key;
572 /** Element key hash code. */
573 private final int hashCode;
575 /*
576 * Constructors
577 */
579 Element(final Element link, final Property property) {
580 this.link = link;
581 this.property = property;
582 this.key = property.getKey();
583 this.hashCode = this.key.hashCode();
584 }
586 boolean match(final String otherKey, final int otherHashCode) {
587 return this.hashCode == otherHashCode && this.key.equals(otherKey);
588 }
590 /*
591 * Entry implmentation.
592 */
594 @Override
595 public boolean equals(final Object other) {
596 assert property != null && other != null;
597 return other instanceof Element && property.equals(((Element)other).property);
598 }
600 @Override
601 public String getKey() {
602 return key;
603 }
605 @Override
606 public Property getValue() {
607 return property;
608 }
610 @Override
611 public int hashCode() {
612 return hashCode;
613 }
615 @Override
616 public Property setValue(final Property value) {
617 throw new UnsupportedOperationException("Immutable map.");
618 }
620 /*
621 * Accessors
622 */
624 Element getLink() {
625 return link;
626 }
628 void setLink(final Element link) {
629 this.link = link;
630 }
632 Property getProperty() {
633 return property;
634 }
635 }
637 }