KeyFMap: equals & hashCode

This commit is contained in:
Tagir Valeev
2017-05-16 12:25:35 +07:00
parent cf28cc4419
commit d15b92d4f9
7 changed files with 157 additions and 56 deletions
@@ -19,8 +19,11 @@ import com.intellij.openapi.util.Key;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
public class ArrayBackedFMap implements KeyFMap {
static final int ARRAY_THRESHOLD = 8;
// Invariant: keys are always sorted
private final int[] keys;
private final Object[] values;
@@ -34,29 +37,19 @@ public class ArrayBackedFMap implements KeyFMap {
public <V> KeyFMap plus(@NotNull Key<V> key, @NotNull V value) {
int oldSize = size();
int keyCode = key.hashCode();
int[] newKeys = null;
Object[] newValues = null;
int i;
for (i = 0; i < oldSize; i++) {
int oldKey = keys[i];
if (keyCode == oldKey) {
if (value == values[i]) return this;
newKeys = new int[oldSize];
newValues = new Object[oldSize];
System.arraycopy(keys, 0, newKeys, 0, oldSize);
System.arraycopy(values, 0, newValues, 0, oldSize);
newValues[i] = value;
break;
}
int keyPos = Arrays.binarySearch(keys, keyCode);
if (keyPos >= 0) {
Object[] newValues = values.clone();
newValues[keyPos] = value;
// Can reuse keys as it is never mutated
return new ArrayBackedFMap(keys, newValues);
}
if (i == oldSize) {
if (oldSize == ARRAY_THRESHOLD) {
return new MapBackedFMap(keys, keyCode, values, value);
}
newKeys = ArrayUtil.append(keys, keyCode);
newValues = ArrayUtil.append(values, value, ArrayUtil.OBJECT_ARRAY_FACTORY);
if (oldSize < ARRAY_THRESHOLD) {
int[] newKeys = ArrayUtil.insert(keys, -keyPos - 1, keyCode);
Object[] newValues = ArrayUtil.insert(values, -keyPos - 1, value);
return new ArrayBackedFMap(newKeys, newValues);
}
return new ArrayBackedFMap(newKeys, newValues);
return new MapBackedFMap(keys, keyCode, values, value);
}
private int size() {
@@ -105,13 +98,13 @@ public class ArrayBackedFMap implements KeyFMap {
@Override
public String toString() {
String s = "";
StringBuilder s = new StringBuilder("(");
for (int i = 0; i < keys.length; i++) {
int key = keys[i];
Object value = values[i];
s += (s.isEmpty() ? "" : ", ") + Key.getKeyByIndex(key) + " -> " + value;
s.append((s.length() == 1) ? "" : ", ").append(Key.getKeyByIndex(key)).append(" -> ").append(value);
}
return "(" + s + ")";
return s.append(")").toString();
}
@Override
@@ -145,4 +138,30 @@ public class ArrayBackedFMap implements KeyFMap {
return result;
}
@Override
public int hashCode() {
int hash = 0;
int length = keys.length;
for (int i = 0; i < length; i++) {
// key index is its hashcode
hash += (keys[i] ^ values[i].hashCode());
}
return hash;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ArrayBackedFMap)) return false;
ArrayBackedFMap map = (ArrayBackedFMap)o;
if (map.size() != size()) return false;
int length = keys.length;
for (int i = 0; i < length; i++) {
if (keys[i] != map.keys[i] || !values[i].equals(map.values[i])) return false;
}
return true;
}
}
@@ -56,4 +56,9 @@ class EmptyFMap implements KeyFMap {
public boolean isEmpty() {
return true;
}
@Override
public int hashCode() {
return 0;
}
}
@@ -23,6 +23,11 @@ import org.jetbrains.annotations.Nullable;
* An immutable map optimized for storing few {@link Key} entries with relatively rare updates
* To construct a map, start with {@link KeyFMap#EMPTY_MAP} and call {@link #plus} and {@link #minus}
*
* <p>
* The hashCode() contract conforms to the hashCode() contract of the {@link java.util.Map} interface:
* it's the sum of hash codes of its entries, which in turn is calculated as key.hashCode() ^ value.hashCode()
* </p>
*
* @author peter
*/
public interface KeyFMap {
@@ -21,9 +21,11 @@ import gnu.trove.TIntObjectHashMap;
import gnu.trove.TIntObjectProcedure;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import static com.intellij.util.keyFMap.ArrayBackedFMap.getKeysByIndices;
class MapBackedFMap extends TIntObjectHashMap<Object> implements KeyFMap {
final class MapBackedFMap extends TIntObjectHashMap<Object> implements KeyFMap {
private MapBackedFMap(@NotNull MapBackedFMap oldMap, final int exclude) {
super(oldMap.size());
oldMap.forEachEntry(new TIntObjectProcedure<Object>() {
@@ -73,10 +75,12 @@ class MapBackedFMap extends TIntObjectHashMap<Object> implements KeyFMap {
}
if (oldSize == ArrayBackedFMap.ARRAY_THRESHOLD + 1) {
int[] keys = keys();
Object[] values = getValues();
int i = ArrayUtil.indexOf(keys, keyCode);
keys = ArrayUtil.remove(keys, i);
values = ArrayUtil.remove(values, i);
keys = ArrayUtil.remove(keys, ArrayUtil.indexOf(keys, keyCode));
Arrays.sort(keys);
Object[] values = new Object[keys.length];
for (int i = 0; i < keys.length; i++) {
values[i] = get(keys[i]);
}
return new ArrayBackedFMap(keys, values);
}
return new MapBackedFMap(this, keyCode);
@@ -18,11 +18,11 @@ package com.intellij.util.keyFMap;
import com.intellij.openapi.util.Key;
import org.jetbrains.annotations.NotNull;
public class OneElementFMap<V> implements KeyFMap {
public final class OneElementFMap<VV> implements KeyFMap {
private final Key myKey;
private final V myValue;
private final VV myValue;
public OneElementFMap(@NotNull Key key, @NotNull V value) {
public OneElementFMap(@NotNull Key key, @NotNull VV value) {
myKey = key;
myValue = value;
}
@@ -66,7 +66,7 @@ public class OneElementFMap<V> implements KeyFMap {
return myKey;
}
public V getValue() {
public VV getValue() {
return myValue;
}
@@ -76,17 +76,11 @@ public class OneElementFMap<V> implements KeyFMap {
if (!(o instanceof OneElementFMap)) return false;
OneElementFMap map = (OneElementFMap)o;
if (myKey != map.myKey) return false;
if (!myValue.equals(map.myValue)) return false;
return true;
return myKey == map.myKey && myValue.equals(map.myValue);
}
@Override
public int hashCode() {
int result = myKey.hashCode();
result = 31 * result + myValue.hashCode();
return result;
return myKey.hashCode() ^ myValue.hashCode();
}
}
@@ -18,18 +18,27 @@ package com.intellij.util.keyFMap;
import com.intellij.openapi.util.Key;
import org.jetbrains.annotations.NotNull;
public class PairElementsFMap implements KeyFMap {
private final Key key1;
private final Key key2;
private final Object value1;
private final Object value2;
public final class PairElementsFMap implements KeyFMap {
// invariant: key1.hashCode() < key2.hashCode()
private final @NotNull Key key1;
private final @NotNull Key key2;
private final @NotNull Object value1;
private final @NotNull Object value2;
PairElementsFMap(@NotNull Key key1, @NotNull Object value1, @NotNull Key key2, @NotNull Object value2) {
this.key1 = key1;
this.value1 = value1;
this.key2 = key2;
this.value2 = value2;
assert key1 != key2;
// Key hashCodes are unique and ordered
if(key1.hashCode() < key2.hashCode()) {
this.key1 = key1;
this.value1 = value1;
this.key2 = key2;
this.value2 = value2;
} else {
this.key1 = key2;
this.value1 = value2;
this.key2 = key1;
this.value2 = value1;
}
}
@NotNull
@@ -37,6 +46,11 @@ public class PairElementsFMap implements KeyFMap {
public <V> KeyFMap plus(@NotNull Key<V> key, @NotNull V value) {
if (key == key1) return new PairElementsFMap(key, value, key2, value2);
if (key == key2) return new PairElementsFMap(key, value, key1, value1);
if(key.hashCode() < key1.hashCode()) {
return new ArrayBackedFMap(new int[]{key.hashCode(), key1.hashCode(), key2.hashCode()}, new Object[]{value, value1, value2});
} else if(key.hashCode() < key2.hashCode()) {
return new ArrayBackedFMap(new int[]{key1.hashCode(), key.hashCode(), key2.hashCode()}, new Object[]{value1, value, value2});
}
return new ArrayBackedFMap(new int[]{key1.hashCode(), key2.hashCode(), key.hashCode()}, new Object[]{value1, value2, value});
}
@@ -70,19 +84,38 @@ public class PairElementsFMap implements KeyFMap {
return false;
}
@NotNull
public Key getKey1() {
return key1;
}
@NotNull
public Key getKey2() {
return key2;
}
@NotNull
public Object getValue1() {
return value1;
}
@NotNull
public Object getValue2() {
return value2;
}
@Override
public int hashCode() {
return (key1.hashCode() ^ value1.hashCode()) + (key2.hashCode() ^ value2.hashCode());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PairElementsFMap)) return false;
PairElementsFMap map = (PairElementsFMap)o;
return key1 == map.key1 && value1.equals(map.value1) && key2 == map.key2 && value2.equals(map.value2);
}
}
@@ -20,10 +20,15 @@ import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import junit.framework.TestCase;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class KeyFMapTest extends TestCase {
private static KeyFMap createKeyFMap(List<Key> keys, List<Object> values) {
private static final List<Key<Object>> KEYS =
IntStream.range(0, 20).mapToObj(i -> Key.create("Key#"+i)).collect(Collectors.toList());
private static KeyFMap createKeyFMap(List<Key<Object>> keys, List<Object> values) {
KeyFMap map = KeyFMap.EMPTY_MAP;
for (int i = 0; i < keys.size(); i++) {
@@ -34,24 +39,60 @@ public class KeyFMapTest extends TestCase {
}
private static void doTestGetKeys(int size) {
List<Key> keys = ContainerUtil.newArrayList();
List<Object> values = ContainerUtil.newArrayList();
for (int i = 0; i < size; i++) {
keys.add(Key.create("Key#" + i));
values.add("Value#" + i);
}
KeyFMap map = createKeyFMap(keys, values);
KeyFMap map = createKeyFMap(KEYS.subList(0, size), values);
Key[] actualKeys = map.getKeys();
assertEquals(size, actualKeys.length);
for (Key key : keys) {
for (Key key : KEYS.subList(0, size)) {
assertTrue("Key not found: " + key, ArrayUtil.contains(key, actualKeys));
}
}
public void testHashCodeEquals() {
Random r = new Random(1);
for(int n=0; n<15; n++) {
Map<Key<Object>, Object> hashMap = new HashMap<>();
KeyFMap fMap = KeyFMap.EMPTY_MAP;
for(int i=0; i<n; i++) {
Object value = "Value#" + i;
Key<Object> key;
while (true) {
key = KEYS.get(r.nextInt(KEYS.size()));
if (hashMap.putIfAbsent(key, value) == null) break;
}
KeyFMap newFMap = fMap.plus(key, value);
assertNotSame(fMap, newFMap); // new key is added: must be not same
fMap = newFMap;
}
assertEquals(hashMap + ":" + fMap, hashMap.hashCode(), fMap.hashCode());
KeyFMap fMap2 = KeyFMap.EMPTY_MAP;
for (Map.Entry<Key<Object>, Object> entry : hashMap.entrySet()) {
fMap2 = fMap2.plus(entry.getKey(), entry.getValue());
}
assertEquals(fMap, fMap2);
Iterator<Key<Object>> iterator = hashMap.keySet().iterator();
while(iterator.hasNext()) {
Key<Object> key = iterator.next();
iterator.remove();
assertEquals(fMap, fMap2);
fMap = fMap.minus(key);
assertFalse(fMap.equals(fMap2));
fMap2 = fMap2.minus(key);
assertEquals(fMap, fMap2);
assertEquals(fMap.hashCode(), fMap2.hashCode());
}
assertTrue(fMap.isEmpty());
}
}
public void testGetKeysOnEmptyFMap() {
doTestGetKeys(0);
}