Java tutorial
// Copyright 2007 The Apache Software Foundation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import java.io.Serializable; import java.util.*; /** * An mapped collection where the keys are always strings and access to values is case-insensitive. The case of keys in * the map is <em>maintained</em>, but on any access to a key (directly or indirectly), all key comparisons are * performed in a case-insensitive manner. The map implementation is intended to support a reasonably finite number * (dozens or hundreds, not thousands or millions of key/value pairs. Unlike HashMap, it is based on a sorted list of * entries rather than hash bucket. It is also geared towards a largely static map, one that is created and then used * without modification. * * @param <V> the type of value stored */ public class CaseInsensitiveMap<V> extends AbstractMap<String, V> implements Serializable { private static final long serialVersionUID = 3362718337611953298L; private static final int NULL_HASH = Integer.MIN_VALUE; private static final int DEFAULT_SIZE = 20; private static class CIMEntry<V> implements Map.Entry<String, V>, Serializable { private static final long serialVersionUID = 6713986085221148350L; private String key; private final int hashCode; V value; public CIMEntry(final String key, final int hashCode, V value) { this.key = key; this.hashCode = hashCode; this.value = value; } public String getKey() { return key; } public V getValue() { return value; } public V setValue(V value) { V result = this.value; this.value = value; return result; } /** * Returns true if both keys are null, or if the provided key is the same as, or case-insensitively equal to, * the entrie's key. * * @param key to compare against * @return true if equal */ @SuppressWarnings({ "StringEquality" }) boolean matches(String key) { return key == this.key || (key != null && key.equalsIgnoreCase(this.key)); } boolean valueMatches(Object value) { return value == this.value || (value != null && value.equals(this.value)); } } private class EntrySetIterator implements Iterator { int expectedModCount = modCount; int index; int current = -1; public boolean hasNext() { return index < size; } public Object next() { check(); if (index >= size) throw new NoSuchElementException(); current = index++; return entries[current]; } public void remove() { check(); if (current < 0) throw new NoSuchElementException(); new Position(current, true).remove(); expectedModCount = modCount; } private void check() { if (expectedModCount != modCount) throw new ConcurrentModificationException(); } } @SuppressWarnings("unchecked") private class EntrySet extends AbstractSet { @Override public Iterator iterator() { return new EntrySetIterator(); } @Override public int size() { return size; } @Override public void clear() { CaseInsensitiveMap.this.clear(); } @Override public boolean contains(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry) o; Position position = select(e.getKey()); return position.isFound() && position.entry().valueMatches(e.getValue()); } @Override public boolean remove(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry) o; Position position = select(e.getKey()); if (position.isFound() && position.entry().valueMatches(e.getValue())) { position.remove(); return true; } return false; } } private class Position { private final int cursor; private final boolean found; Position(int cursor, boolean found) { this.cursor = cursor; this.found = found; } boolean isFound() { return found; } CIMEntry<V> entry() { return entries[cursor]; } V get() { return found ? entries[cursor].value : null; } V remove() { if (!found) return null; V result = entries[cursor].value; // Remove the entry by shifting everything else down. System.arraycopy(entries, cursor + 1, entries, cursor, size - cursor - 1); // We shifted down, leaving one (now duplicate) entry behind. entries[--size] = null; // A structural change for sure modCount++; return result; } @SuppressWarnings("unchecked") V put(String key, int hashCode, V newValue) { if (found) { CIMEntry<V> e = entries[cursor]; V result = e.value; // Not a structural change, so no change to modCount // Update the key (to maintain case). By definition, the hash code // will not change. e.key = key; e.value = newValue; return result; } // Not found, we're going to add it. int newSize = size + 1; if (newSize == entries.length) { // Time to expand! int newCapacity = (size * 3) / 2 + 1; CIMEntry<V>[] newEntries = new CIMEntry[newCapacity]; System.arraycopy(entries, 0, newEntries, 0, cursor); System.arraycopy(entries, cursor, newEntries, cursor + 1, size - cursor); entries = newEntries; } else { // Open up a space for the new entry System.arraycopy(entries, cursor, entries, cursor + 1, size - cursor); } CIMEntry<V> newEntry = new CIMEntry<V>(key, hashCode, newValue); entries[cursor] = newEntry; size++; // This is definately a structural change modCount++; return null; } } // The list of entries. This is kept sorted by hash code. In some cases, there may be different // keys with the same hash code in adjacent indexes. private CIMEntry<V>[] entries; private int size = 0; // Used by iterators to check for concurrent modifications private transient int modCount = 0; private transient Set<Map.Entry<String, V>> entrySet; public CaseInsensitiveMap() { this(DEFAULT_SIZE); } @SuppressWarnings("unchecked") public CaseInsensitiveMap(int size) { entries = new CIMEntry[Math.max(size, 3)]; } public CaseInsensitiveMap(Map<String, ? extends V> map) { this(map.size()); for (Map.Entry<String, ? extends V> entry : map.entrySet()) { put(entry.getKey(), entry.getValue()); } } @Override public void clear() { for (int i = 0; i < size; i++) entries[i] = null; size = 0; modCount++; } @Override public boolean isEmpty() { return size == 0; } @Override public int size() { return size; } @SuppressWarnings("unchecked") @Override public V put(String key, V value) { int hashCode = caseInsenitiveHashCode(key); return select(key, hashCode).put(key, hashCode, value); } @Override public boolean containsKey(Object key) { return select(key).isFound(); } @Override public V get(Object key) { return select(key).get(); } @Override public V remove(Object key) { return select(key).remove(); } @SuppressWarnings("unchecked") @Override public Set<Map.Entry<String, V>> entrySet() { if (entrySet == null) entrySet = new EntrySet(); return entrySet; } private Position select(Object key) { if (key == null || key instanceof String) { String keyString = (String) key; return select(keyString, caseInsenitiveHashCode(keyString)); } return new Position(0, false); } /** * Searches the elements for the index of the indicated key and (case insensitive) hash code. Sets the _cursor and * _found attributes. */ private Position select(String key, int hashCode) { if (size == 0) return new Position(0, false); int low = 0; int high = size - 1; int cursor; while (low <= high) { cursor = (low + high) >> 1; CIMEntry e = entries[cursor]; if (e.hashCode < hashCode) { low = cursor + 1; continue; } if (e.hashCode > hashCode) { high = cursor - 1; continue; } return tunePosition(key, hashCode, cursor); } return new Position(low, false); } /** * select() has located a matching hashCode, but there's an outlying possibility that multiple keys share the same * hashCode. Backup the cursor until we get to locate the initial hashCode match, then march forward until the key * is located, or the hashCode stops matching. * * @param key * @param hashCode */ private Position tunePosition(String key, int hashCode, int cursor) { boolean found = false; while (cursor > 0) { if (entries[cursor - 1].hashCode != hashCode) break; cursor--; } while (true) { if (entries[cursor].matches(key)) { found = true; break; } // Advance to the next entry. cursor++; // If out of entries, if (cursor >= size || entries[cursor].hashCode != hashCode) break; } return new Position(cursor, found); } static int caseInsenitiveHashCode(String input) { if (input == null) return NULL_HASH; int length = input.length(); int hash = 0; // This should end up more or less equal to input.toLowerCase().hashCode(), unless String // changes its implementation. Let's hope this is reasonably fast. for (int i = 0; i < length; i++) { int ch = input.charAt(i); int caselessCh = Character.toLowerCase(ch); hash = 31 * hash + caselessCh; } return hash; } }