blob: fc7571ba8ba01a91f7a52d9ab84ccf53302aa011 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2013 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0, which accompanies this distribution
* and is available at https://www.eclipse.org/legal/epl-2.0/.
*
* Contributors:
* Oracle - initial API and implementation
******************************************************************************/
package org.eclipse.jpt.common.utility.internal.collection;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import org.eclipse.jpt.common.utility.internal.ArrayTools;
import org.eclipse.jpt.common.utility.internal.ObjectTools;
/**
* A {@link Map} implementation that uses the least amount of space possible,
* at the expense of slower execution and increased object creation.
* @param <K> the type of keys maintained by the map
* @param <V> the type of values maintained by the map
*/
public class TightMap<K,V>
implements Map<K,V>
{
@SuppressWarnings("unchecked")
K[] keys = (K[]) ObjectTools.EMPTY_OBJECT_ARRAY;
@SuppressWarnings("unchecked")
V[] values = (V[]) ObjectTools.EMPTY_OBJECT_ARRAY;
/**
* Construct an empty map.
*/
public TightMap() {
super();
}
/**
* Construct a map initialized with the entries from the specified map.
*/
public TightMap(Map<? extends K, ? extends V> map) {
super();
this.putAll(map);
}
public int size() {
return this.keys.length;
}
public boolean isEmpty() {
return this.keys.length == 0;
}
public boolean containsValue(Object value) {
return ArrayTools.contains(this.values, value);
}
public boolean containsKey(Object key) {
return ArrayTools.contains(this.keys, key);
}
public V get(Object key) {
int index = this.indexOfKey(key);
return (index == -1) ? null : this.values[index];
}
/* CU private */ int indexOfKey(Object key) {
return ArrayTools.indexOf(this.keys, key);
}
public V put(K key, V value) {
V prev = null;
int index = this.indexOfKey(key);
if (index == -1) {
this.keys = ArrayTools.add(this.keys, key);
this.values = ArrayTools.add(this.values, value);
} else {
prev = this.values[index];
this.values[index] = value;
}
return prev;
}
public V remove(Object key) {
int index = this.indexOfKey(key);
return (index == -1) ? null : this.remove(index);
}
/**
* Pre-condition: specified key is present
*/
/* CU private */ V removeKey(Object key) {
return this.remove(this.indexOfKey(key));
}
/**
* Pre-condition: specified value is present
*/
/* CU private */ void removeValue(Object value) {
this.remove(this.indexOfValue(value));
}
/* CU private */ int indexOfValue(Object value) {
return ArrayTools.indexOf(this.values, value);
}
/**
* Pre-condition: specified index is valid
*/
V remove(int index) {
V prev = this.values[index];
this.keys = ArrayTools.removeElementAtIndex(this.keys, index);
this.values = ArrayTools.removeElementAtIndex(this.values, index);
if (this.keys.length == 0) {
// replace the global empty object array so we can detect a concurrent modification
this.clear();
}
return prev;
}
public void putAll(Map<? extends K, ? extends V> map) {
int mapSize = map.size();
if (mapSize == 0) {
return;
}
int index = this.keys.length;
this.keys = ArrayTools.expand(this.keys, mapSize);
this.values = ArrayTools.expand(this.values, mapSize);
for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) {
this.keys[index] = entry.getKey();
this.values[index] = entry.getValue();
index++;
}
}
@SuppressWarnings("unchecked")
public void clear() {
// build *new* arrays here so we can detect a concurrent modification
this.keys = (K[]) new Object[0];
this.values = (V[]) new Object[0];
}
public Set<K> keySet() {
return new KeySet();
}
public Collection<V> values() {
return new ValueCollection();
}
public Set<Map.Entry<K, V>> entrySet() {
return new EntrySet();
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if ( ! (o instanceof Map)) {
return false;
}
@SuppressWarnings("unchecked")
Map<K,V> other = (Map<K,V>) o;
int size = this.keys.length;
if (other.size() != size) {
return false;
}
for (int i = size; i-- > 0; ) {
K key = this.keys[i];
V value = this.values[i];
if (value == null) {
if ( ! ((other.get(key) == null) && other.containsKey(key))) {
return false;
}
} else {
if ( ! value.equals(other.get(key))) {
return false;
}
}
}
return true;
}
@Override
public int hashCode() {
int hash = 0;
for (int i = this.keys.length; i-- > 0; ) {
hash += (ObjectTools.hashCode(this.keys[i]) ^ ObjectTools.hashCode(this.values[i]));
}
return hash;
}
@Override
public String toString() {
int len = this.keys.length;
if (len == 0) {
return EMPTY_MAP_STRING;
}
StringBuilder sb = new StringBuilder();
sb.append('{');
for (int i = 0; i < len; i++) {
K key = this.keys[i];
V value = this.values[i];
if (key == this) {
sb.append(THIS_MAP_STRING);
} else {
sb.append(key);
}
sb.append('=');
if (value == this) {
sb.append(THIS_MAP_STRING);
} else {
sb.append(value);
}
sb.append(", "); //$NON-NLS-1$
}
sb.setLength(sb.length() - 2); // strip off extra comma
sb.append('}');
return sb.toString();
}
private static final String EMPTY_MAP_STRING = "{}"; //$NON-NLS-1$
private static final String THIS_MAP_STRING = "(this Map)"; //$NON-NLS-1$
// ********** array iterator **********
private abstract static class ArrayIterator<E>
implements Iterator<E>
{
E[] localArray;
private E next;
private boolean done = false;
private int cursor = 0;
private E current;
private boolean currentRemoved = true;
ArrayIterator() {
super();
this.localArray = this.getTightMapArray();
this.loadNext();
}
abstract E[] getTightMapArray();
public boolean hasNext() {
return ! this.done;
}
public E next() {
if (this.localArray != this.getTightMapArray()) {
throw new ConcurrentModificationException();
}
if (this.done) {
throw new NoSuchElementException();
}
E element = this.next;
this.loadNext();
this.current = element;
this.currentRemoved = false;
return element;
}
private void loadNext() {
if (this.cursor < this.localArray.length) {
this.next = this.localArray[this.cursor];
this.cursor++;
} else {
this.next = null;
this.done = true;
}
}
public void remove() {
if (this.currentRemoved) {
throw new IllegalStateException();
}
if (this.localArray != this.getTightMapArray()) {
throw new ConcurrentModificationException();
}
E element = this.current;
this.current = null;
this.currentRemoved = true;
this.removeTightMapElement(element);
this.localArray = this.getTightMapArray(); // point at new array
this.cursor--;
}
abstract void removeTightMapElement(E e);
}
// ********** keys **********
private class KeySet
extends AbstractSet<K>
{
KeySet() {
super();
}
@Override
public Iterator<K> iterator() {
return TightMap.this.buildKeyIterator();
}
@Override
public int size() {
return TightMap.this.keys.length;
}
@Override
public boolean contains(Object o) {
return TightMap.this.containsKey(o);
}
@Override
public boolean remove(Object o) {
int index = TightMap.this.indexOfKey(o);
if (index == -1) {
return false;
}
TightMap.this.remove(index);
return true;
}
@Override
public void clear() {
TightMap.this.clear();
}
}
/* CU private */ Iterator<K> buildKeyIterator() {
return new KeyIterator();
}
private class KeyIterator
extends ArrayIterator<K>
{
KeyIterator() {
super();
}
@Override
K[] getTightMapArray() {
return TightMap.this.keys;
}
@Override
void removeTightMapElement(K key) {
TightMap.this.removeKey(key);
}
}
// ********** values **********
private class ValueCollection
extends AbstractCollection<V>
{
ValueCollection() {
super();
}
@Override
public Iterator<V> iterator() {
return TightMap.this.buildValueIterator();
}
@Override
public int size() {
return TightMap.this.keys.length;
}
@Override
public boolean contains(Object o) {
return TightMap.this.containsValue(o);
}
@Override
public boolean remove(Object o) {
int index = TightMap.this.indexOfValue(o);
if (index == -1) {
return false;
}
TightMap.this.remove(index);
return true;
}
@Override
public void clear() {
TightMap.this.clear();
}
}
/* CU private */ Iterator<V> buildValueIterator() {
return new ValueIterator();
}
private class ValueIterator
extends ArrayIterator<V>
{
ValueIterator() {
super();
}
@Override
V[] getTightMapArray() {
return TightMap.this.values;
}
@Override
void removeTightMapElement(V value) {
TightMap.this.removeValue(value);
}
}
// ********** entries **********
private class EntrySet
extends AbstractSet<Map.Entry<K, V>>
{
EntrySet() {
super();
}
@Override
public Iterator<Map.Entry<K, V>> iterator() {
return TightMap.this.buildEntryIterator();
}
@Override
public int size() {
return TightMap.this.keys.length;
}
@Override
public boolean contains(Object o) {
return TightMap.this.containsEntry(o);
}
@Override
public boolean remove(Object o) {
return TightMap.this.removeEntry(o);
}
@Override
public void clear() {
TightMap.this.clear();
}
}
/* CU private */ Iterator<Map.Entry<K, V>> buildEntryIterator() {
return new EntryIterator();
}
private class EntryIterator
implements Iterator<Map.Entry<K, V>>
{
K[] localKeys;
V[] localValues;
private Entry next;
private int cursor = 0;
private Entry current;
EntryIterator() {
super();
this.localKeys = TightMap.this.keys;
this.localValues = TightMap.this.values;
this.next = this.buildNext();
}
public boolean hasNext() {
return this.next != null;
}
public Map.Entry<K, V> next() {
if (this.localKeys != TightMap.this.keys) {
throw new ConcurrentModificationException();
}
Entry entry = this.next;
if (entry == null) {
throw new NoSuchElementException();
}
this.next = this.buildNext();
this.current = entry;
return entry;
}
private Entry buildNext() {
return (this.cursor < this.localKeys.length) ? new Entry(this.localKeys[this.cursor++]) : null;
}
public void remove() {
if (this.current == null) {
throw new IllegalStateException();
}
if (this.localKeys != TightMap.this.keys) {
throw new ConcurrentModificationException();
}
Object key = this.current.getKey();
this.current = null;
TightMap.this.removeKey(key);
this.localKeys = TightMap.this.keys; // point at new arrays
this.localValues = TightMap.this.values;
this.cursor--;
}
private class Entry
implements Map.Entry<K,V>
{
// we hold the key (instead of the index) because the key/value pair
// can move when entries are removed via the iterator
final K key;
Entry(K key) {
super();
this.key = key;
}
public K getKey() {
return this.key;
}
private int getIndex() {
return ArrayTools.indexOf(EntryIterator.this.localKeys, this.key);
}
public V getValue() {
return EntryIterator.this.localValues[this.getIndex()];
}
public V setValue(V value) {
int index = this.getIndex();
V old = EntryIterator.this.localValues[index];
EntryIterator.this.localValues[index] = value;
return old;
}
@Override
public boolean equals(Object o) {
if ( ! (o instanceof Map.Entry)) {
return false;
}
@SuppressWarnings("unchecked")
Map.Entry<K,V> other = (Map.Entry<K,V>) o;
return ObjectTools.equals(this.getKey(), other.getKey()) &&
ObjectTools.equals(this.getValue(), other.getValue());
}
@Override
public int hashCode() {
return ObjectTools.hashCode(this.getKey()) ^ ObjectTools.hashCode(this.getValue());
}
@Override
public String toString() {
return this.getKey() + "=" + this.getValue(); //$NON-NLS-1$
}
}
}
@SuppressWarnings("unchecked")
/* CU private */ boolean containsEntry(Object o) {
return (o instanceof Map.Entry) && this.containsEntry((Map.Entry<K,V>) o);
}
private boolean containsEntry(Map.Entry<K,V> entry) {
int index = TightMap.this.indexOfKey(entry.getKey());
return (index != -1) && ObjectTools.equals(this.values[index], entry.getValue());
}
@SuppressWarnings("unchecked")
/* CU private */ boolean removeEntry(Object o) {
return (o instanceof Map.Entry) && this.removeEntry((Map.Entry<K,V>) o);
}
private boolean removeEntry(Map.Entry<K,V> entry) {
int index = TightMap.this.indexOfKey(entry.getKey());
if (index == -1) {
return false;
}
if (ObjectTools.notEquals(this.values[index], entry.getValue())) {
return false;
}
this.keys = ArrayTools.removeElementAtIndex(this.keys, index);
this.values = ArrayTools.removeElementAtIndex(this.values, index);
return true;
}
}