/* * Kodkod -- Copyright (c) 2005-present, Emina Torlak * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package kodkod.util.collections; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; /** * A map with a fixed set of keys that acts as an indexer, assigning a * unique integer in the range [0..#keys) to each key. This class implements the * <tt>Map</tt> interface with an array, using * reference-equality in place of object-equality when comparing keys (and * values). In other words, in a <tt>FixedMap</tt>, two keys * <tt>k1</tt> and <tt>k2</tt> are considered equal if and only if * <tt>(k1==k2)</tt>. (In normal <tt>Map</tt> implementations (like * <tt>HashMap</tt>) two keys <tt>k1</tt> and <tt>k2</tt> are considered equal * if and only if <tt>(k1==null ? k2==null : k1.equals(k2))</tt>.)</p> * * <p><b>This class is <i>not</i> a general-purpose <tt>Map</tt> * implementation! While this class implements the <tt>Map</tt> interface, it * intentionally violates <tt>Map's</tt> general contract, which mandates the * use of the <tt>equals</tt> method when comparing objects. </b></p> * * <p>This class provides {@link #put(Object, Object)} operation for the keys * within its fixed key set and permits * <tt>null</tt> values and the <tt>null</tt> key. The {@link #remove(Object)} * operation is not supported. This class guarantees * that the order of the map will remain constant over time.</p> * * <p>This class provides log-time performance for the basic * operations (<tt>get</tt> and <tt>put</tt>), assuming the system * identity hash function ({@link System#identityHashCode(Object)}) * disperses elements properly among the buckets.</p> * * <p>This implementation is not synchronized, and its iterators are not fail-fast.</p> * * @specfield keys: set K * @specfield map: keys -> one V * @specfield indices: keys lone->one [0..#keys) * @author Emina Torlak */ public final class FixedMap<K, V> extends AbstractMap<K, V> implements Indexer<K> { private final Object[] keys; private final Object[] values; /** * Constructs a new fixed map from the given map. * @ensures this.keys' = keys && this.map = map.map */ public FixedMap(Map<K, V> map) { this(map.keySet()); for(int i = 0, size = map.size(); i < size; i++) { values[i] = map.get(keys[i]); } } /** * Constructs a new fixed map for the given set of keys. * @ensures this.keys' = keys && no this.map' */ public FixedMap(Set<K> keys) { final int size = keys.size(); this.keys = Containers.identitySort(keys.toArray(new Object[size])); values = new Object[size]; } /** * Constructs a new fixed map, backed by the given array of keys. The provided * array must not contain duplicates, its entries must be sorted * in the increasing order of identity hashcodes, and it must not * be modified while in use by this map. The map will not behave correctly if * these requirements are violated. * @requires no disj i, j: [0..keys.length) | keys[i] == keys[j] * @requires all i, j: [0..keys.length) | i < j => System.identityHashCode(keys[i]) <= System.identityHashCode(keys[j]) * @ensures this.keys' = keys && no this.map' */ public FixedMap(K[] keys) { this.keys = keys; this.values = new Object[keys.length]; } /** * Returns the index of the given key, if it is in this.keys. * Otherwise returns a negative number. * @return key in this.keys => this.indices[key], {i: int | i < 0 } */ public final int indexOf(K key) { return Containers.identityBinarySearch(keys, key); } /** * Returns the key at the given index. * @return this.indices.index * @throws IndexOutOfBoundsException index !in this.indices[this.keys] */ @SuppressWarnings("unchecked") public final K keyAt(int index) { try { return (K)keys[index]; } catch (ArrayIndexOutOfBoundsException e) { throw new IndexOutOfBoundsException(); } } /** * Tests whether the specified object reference is a key in this fixed map. * * @return key in this.keys */ @SuppressWarnings("unchecked") public final boolean containsKey(Object key) { return indexOf((K)key) >= 0; } /** * Tests whether the specified object reference is a value in this fixed map. * * @return value in this.map[this.keys] */ public final boolean containsValue(Object value) { for(Object o : values) { if (o==value) return true; } return false; } /** * Returns a set view of the mappings contained in this map. Each element * in the returned set is a reference-equality-based <tt>Map.Entry</tt>. * The set is backed by the map, so changes to the map are reflected in * the set, and vice-versa. If the map is modified while an iteration * over the set is in progress, the results of the iteration are * undefined. The set does support neither removal nor addition. * * <p>Like the backing map, the <tt>Map.Entry</tt> objects in the set * returned by this method define key and value equality as * reference-equality rather than object-equality. This affects the * behavior of the <tt>equals</tt> and <tt>hashCode</tt> methods of these * <tt>Map.Entry</tt> objects. A reference-equality based <tt>Map.Entry * e</tt> is equal to an object <tt>o</tt> if and only if <tt>o</tt> is a * <tt>Map.Entry</tt> and <tt>e.getKey()==o.getKey() && * e.getValue()==o.getValue()</tt>. To accommodate these equals * semantics, the <tt>hashCode</tt> method returns * <tt>System.identityHashCode(e.getKey()) ^ * System.identityHashCode(e.getValue())</tt>. * * <p><b>Owing to the reference-equality-based semantics of the * <tt>Map.Entry</tt> instances in the set returned by this method, * it is possible that the symmetry and transitivity requirements of * the {@link Object#equals(Object)} contract may be violated if any of * the entries in the set is compared to a normal map entry, or if * the set returned by this method is compared to a set of normal map * entries (such as would be returned by a call to this method on a normal * map). However, the <tt>Object.equals</tt> contract is guaranteed to * hold among identity-based map entries, and among sets of such entries. * </b> * * @return a set view of the identity-mappings contained in this map. */ public final Set<Map.Entry<K, V>> entrySet() { return new AbstractSet<Map.Entry<K, V>>() { @SuppressWarnings("unchecked") public boolean contains(Object o) { final Map.Entry<K, V> e = (Map.Entry<K, V>) o; final int index = FixedMap.this.indexOf(e.getKey()); return index < 0 ? false : values[index]==e.getValue(); } public Iterator<java.util.Map.Entry<K, V>> iterator() { return new EntryIterator(); } public int size() { return keys.length; } public Object[] toArray() { int size = size(); Object[] result = new Object[size]; for (int i = 0; i < size; i++) result[i] = new Entry(i); return result; } @SuppressWarnings("unchecked") public <T> T[] toArray(T[] a) { int size = size(); if (a.length < size) a = (T[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size); for (int i = 0; i < size; i++) a[i] = (T) new Entry(i); if (a.length > size) a[size] = null; return a; } }; } /** * Returns the value to which the specified key is mapped in this fixed map, * or <tt>null</tt> if the map contains no mapping for * this key. A return value of <tt>null</tt> does not <i>necessarily</i> * indicate that the map contains no mapping for the key; it is also * possible that the map explicitly maps the key to <tt>null</tt>. The * <tt>containsKey</tt> method may be used to distinguish these two * cases. * * @return this.map[key] */ @SuppressWarnings("unchecked") public final V get(Object key) { final int index = indexOf((K)key); return index < 0 ? null : (V)values[index]; } /** * Returns the value to which the key at the specified index is mapped in this fixed map. * @requires index in this.indices[this.keys] * @return this.map[this.indices.index] * @throws IndexOutOfBoundsException index !in this.indices[this.keys] */ @SuppressWarnings("unchecked") public final V get(int index) { try { return (V) values[index]; } catch (ArrayIndexOutOfBoundsException e) { throw new IndexOutOfBoundsException(); } } /** * @see java.util.Map#isEmpty() */ public final boolean isEmpty() { return keys.length==0; } /** * Associates the specified value with the specified key in this fixed * map. If the map previously contained a mapping for this key, the * old value is replaced. This method assumes that the given key is * in the fixed keyset of this map. * * @requires key in this.keys * @return this.map[key] * @ensures this.map' = this.map ++ key->value * @throws IllegalArgumentException key !in this.keys */ @SuppressWarnings("unchecked") public final V put(K key, V value) { final int index = indexOf((K)key); if (index < 0) throw new IllegalArgumentException(); final V oldValue = (V) values[index]; values[index] = value; return oldValue; } /** * Throws an {@link UnsupportedOperationException} exception. * @see java.util.Map#remove(java.lang.Object) */ public final V remove(Object key) { throw new UnsupportedOperationException(); } /** * @see java.util.Map#size() */ public final int size() { return keys.length; } /** * Returns the hash code value for this map. The hash code of a map * is defined to be the sum of the hashcode of each entry in the map's * entrySet view. This ensures that <tt>t1.equals(t2)</tt> implies * that <tt>t1.hashCode()==t2.hashCode()</tt> for any two * <tt>FixedMap</tt> instances <tt>t1</tt> and <tt>t2</tt>, as * required by the general contract of {@link Object#hashCode()}. * * <p><b>Owing to the reference-equality-based semantics of the * <tt>Map.Entry</tt> instances in the set returned by this map's * <tt>entrySet</tt> method, it is possible that the contractual * requirement of <tt>Object.hashCode</tt> mentioned in the previous * paragraph will be violated if one of the two objects being compared is * an <tt>FixedMap</tt> instance and the other is a normal map.</b> * * @return the hash code value for this map. * @see Object#hashCode() * @see Object#equals(Object) * @see #equals(Object) */ public int hashCode() { int result = 0; for(int i = 0 ; i < keys.length; i++) result += System.identityHashCode(keys[i]) ^ System.identityHashCode(values[i]); return result; } /** * Compares the specified object with this map for equality. Returns * <tt>true</tt> if the given object is also a map and the two maps * represent identical object-reference mappings. More formally, this * map is equal to another map <tt>m</tt> if and only if * map <tt>this.entrySet().equals(m.entrySet())</tt>. * * <p><b>Owing to the reference-equality-based semantics of this map it is * possible that the symmetry and transitivity requirements of the * <tt>Object.equals</tt> contract may be violated if this map is compared * to a normal map. However, the <tt>Object.equals</tt> contract is * guaranteed to hold among <tt>FixedMap</tt> instances.</b> * * @param o object to be compared for equality with this map. * @return <tt>true</tt> if the specified object is equal to this map. * @see Object#equals(Object) */ public boolean equals(Object o) { if (o == this) { return true; } else if (o instanceof FixedMap) { FixedMap<?,?> m = (FixedMap<?,?>) o; if (m.size() != size()) return false; for(int i = 0; i < keys.length; i++) { if (keys[i] != m.keys[i] || values[i] != m.values[i]) return false; } return true; } else if (o instanceof Map) { Map<?,?> m = (Map<?,?>)o; return entrySet().equals(m.entrySet()); } else { return false; // o is not a Map } } private class Entry implements Map.Entry<K, V> { int index; Entry(int index) { this.index = index; } @SuppressWarnings("unchecked") public final K getKey() { return (K)keys[index]; } @SuppressWarnings("unchecked") public final V getValue() { return (V)values[index]; } public V setValue(V value) { throw new UnsupportedOperationException(); } public int hashCode() { return System.identityHashCode(keys[index]) ^ System.identityHashCode(values[index]); } public boolean equals(Object o) { if (o instanceof Map.Entry) { final Map.Entry<?,?> e = (Map.Entry<?,?>) o; return keys[index] == e.getKey() && values[index] == e.getValue(); } else return false; } public String toString() { return keys[index] + "=" + values[index]; } } private final class EntryIterator extends Entry implements Iterator<Map.Entry<K, V>> { int next = 0; EntryIterator() { super(-1); } @SuppressWarnings("unchecked") public V setValue(V value) { final V oldValue = (V) values[index]; values[index] = value; return oldValue; } public boolean hasNext() { return next < keys.length; } public Map.Entry<K, V> next() { if (!hasNext()) throw new NoSuchElementException(); index = next++; return this; } public int hashCode() { return index < 0 ? System.identityHashCode(this) : super.hashCode(); } public boolean equals(Object o) { return index < 0 ? this==o : super.equals(o); } public void remove() { throw new UnsupportedOperationException(); } public String toString() { return index < 0 ? "[]" : super.toString(); } } }