Freitag, 19. April 2013

Indexable HashMap Implementation

Recently I needed a hash map with some sort of indexed access. I searched for a solution and wondered that I found nothing. Maybe my implementation can save sombody else some work:


import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;

public class IndexableHashMap<K, V> extends java.util.HashMap<K, V> {

  private static final long serialVersionUID = 1L;

  public static <K, V> IndexableHashMap<K, V> unmodifiable(IndexableHashMap<K, V> map) {
    return map.setImmutable(true);
  }

  private boolean immutable;
  private Map<MapIndex<?, V>, Multimap<Object, V>> idxMap; // immutable

  public IndexableHashMap(MapIndex<?, V>... indexes) {
    this(Arrays.asList(indexes));
  }

  public IndexableHashMap(Collection<MapIndex<?, V>> indexes) {
    init(indexes);
  }

  private void init(Collection<MapIndex<?, V>> indexes) {
    Map<MapIndex<?, V>, Multimap<Object, V>> newIdxMap = new HashMap<>();
    for (MapIndex<?, V> index : indexes) {
      Multimap<Object, V> idxKey2Val = ArrayListMultimap.create();
      newIdxMap.put(index, idxKey2Val);
    }
    idxMap = Collections.unmodifiableMap(newIdxMap);
  }

  private IndexableHashMap<K, V> setImmutable(boolean immutable) {
    this.immutable = immutable;
    return this;
  }

  private void checkMutable() {
    if (immutable) {
      throw new UnsupportedOperationException("IndexableHashMap " + this + " is immutable!");
    }
  }

  @Override
  public V put(K key, V value) {
    checkMutable();
    for (Map.Entry<MapIndex<?, V>, Multimap<Object, V>> e : idxMap.entrySet()) {
      Object idxKey = e.getKey().val2IdxKey(value);
      e.getValue().put(idxKey, value);
    }
    return super.put(key, value);
  }

  @Override
  public void putAll(Map<? extends K, ? extends V> m) {
    checkMutable();
    // since Java8 we can no no longer delegate to super...
    for (java.util.Map.Entry<? extends K, ? extends V> entry : m.entrySet()) {
      put(entry.getKey(), entry.getValue());
    }
  }


  @Override
  public V remove(Object key) {
    checkMutable();
    V removed = super.remove(key);
    for (Map.Entry<MapIndex<?, V>, Multimap<Object, V>> e : idxMap.entrySet()) {
      Object idxKey = e.getKey().val2IdxKey(removed);
      e.getValue().remove(idxKey, removed);
    }
    return removed;
  }

  @Override
  public void clear() {
    checkMutable();
    for (Multimap<Object, V> idxVals : idxMap.values()) {
      idxVals.clear();
    }
    super.clear();
  }

  public Set<MapIndex<?, V>> getIndices() {
    return idxMap.keySet();
  }

  public <IK> Collection<V> getByIdx(MapIndex<?, V> index, IK idxKey) {
    return idxMap.get(index).get(idxKey);
  }

  public <IK> V getByUniqueIdx(MapIndex<?, V> idx, IK idxKey) {
    Collection<V> vals = getByIdx(idx, idxKey);
    if (vals == null || vals.isEmpty()) {
      return null;
    } else if (vals.size() == 1) {
      return vals.iterator().next();
    } else if (!idx.isUnique()) {
      throw new IllegalArgumentException("Index " + idx + " not unique! Values for key " + idxKey + ": " + vals);
    } else {
      throw new IllegalStateException("Multiple values found for unique index " + idx + "! Values for key " + idxKey + ": " + vals);
    }
  }

  public static abstract class MapIndex<IK, V> {
    private final boolean unique;

    public MapIndex(boolean unique) {
      this.unique = unique;
    }

    protected abstract IK val2IdxKey(V value);

    public boolean isUnique() {
      return unique;
    }

    @Override
    public String toString() {
      return getClass().getSimpleName();
    }
  }

}


Usage:

  public static class PrefixIndex extends MapIndex<String, String> {
    private final int prefixLength;

    public PrefixIndex(int prefixLength) {
      super(false);
      this.prefixLength = prefixLength;
    }

    @Override
    protected String val2IdxKey(String value) {
      return value == null ? "" : value.substring(0, Math.min(prefixLength, value.length()));
    }

  }

  @Test
  public void testUsingIndex() {
    PrefixIndex index = new PrefixIndex(1);
    IndexableHashMap<String, String> map = new IndexableHashMap<String, String>(index);
    map.put("Key1", "AX-Val");
    map.put("Key2", "BY-Val");
    map.put("Key3", "BZ-Val");
    assertEquals("AX-Val", map.get("Key1"));
    assertEquals("BY-Val", map.get("Key2"));
    assertEquals("BZ-Val", map.get("Key3"));
    assertEquals(1, map.getByIdx(index, "A").size());
    assertEquals(2, map.getByIdx(index, "B").size());
  }


1 Kommentar: