~/home of geeks

MapBuilder revisited

· 1410 Wörter · 7 Minute(n) Lesedauer

Dieser Artikel ist Teil der Artikel-Serie "MapBuilder".

patterns in nature

In diesem Artikel bohre ich den MapBuilder aus dem vorangegangenen Artikel noch weiter auf.

Nach dem Erstellen des initialen MapBuilders wollte ich noch ein paar Besonderheiten der verschiedenen Map-Implementierungen in dem Builder abbilden. So haben HashMap und LinkedHashMap eine initiale Kapazität und einen Befüllungsfaktor (Loadfactor), der bestimmt, ab wieviel Prozent der aktuellen Befüllung die Map ihren intern Ablageplatz vergrößern soll. Da ich den Builder für Maps, die üblicherweise einmalig und als unveränderliche Konstanten initialisiert werden, benutze, wollte ich auch die passende Initialgröße mitgeben, so dass keine Vergrößerung beim Hinzufügen der Elemente aus dem Builder notwendig sind. In der vorhergehenden Version war das ganze nicht möglich, da die Map bereits beim Erstellen des Builders erzeugt wurde und die benötigten Parameter nur bei Erzeugung mitgegeben werden können. Die Erzeugung stellte ich auf eine Lazy-Initialisierung in der build()-Methode um. Hierzu mussten aber die dem Builder per put() mitgegebenen Elemente zwischen gespeichert werden:

public abstract class AbstractMapBuilder {

    private final Map tmpMap = new LinkedHashMap();
    // ...
    public AbstractMapBuilder put(final K key, final V value) {
        tmpMap.put(key, value);
        return this;
    }
    // ...
    protected Map getTemporaryData() {
        return tmpMap;
    }
    // ...
    public abstract Map build();
    // ...

Diese müssen dann in einer konkreten Implementierung erst noch hinzugefügt werden. Hierzu doch erst später mehr.

An dieser Stelle stand für mich schon fest, dass ich eine abstrakte Basisklasse mit ein paar konkreten Implementierungen haben werde (HashMap, LinkedHashMap, TreeMap). Damit eine Unterklasse die Methoden der Oberklasse benutzen kann, diese aber dann, für die fluent Methoden, einen Builder mit dem richtigen Typen zurückliefert, musste ich die Typisierung der abstrakten Basisklasse anpassen:

public abstract class AbstractMapBuilder<K, V, B extends AbstractMapBuilder> {
  // ...
  public B put(final K key, final V value) {
      tmpMap.put(key, value);
      return (B) this;
  }
  // ...

Erst durch die Erweiterung um den Buildertypen B ist es möglich, später in einer konkreten Unterklasse folgendes zu machen:

public final class TreeMapBuilder extends AbstractMapBuilder<K, V, TreeMapBuilder> {
    private Comparator comparator;
    // ...
    public TreeMapBuilder comparator(Comparator comparator) {
        this.comparator = comparator;
        return this;
    }
    // ...
}

public static void main(String [] argv) {
    new TreeMapBuilder().put("mykey", "myvalue").comparator(new StringComparator()).build();
    new HashMapBuilder().put("mykey", "myvalue").initialCapacity(12).build();
}

Man beachte, dass der HashMapBuilder bei allen Methodenaufrufen einen HashMapBuilder zurück liefert, der TreeMapBuilder immer einen TreeMapBuilder.

Eine schönere Lösung, als in der Basisklasse das B extends ... und den expliziten Casts in den Methoden habe ich für das Problem von typisierten Buildern und Erbhierarchien ad hoc nicht gefunden.

Nun konnte ich in den Unterklassen für LinkedHashMap und HashMap die Größeninitialisierung angeben:

protected Map createMap(int size){
  return new HashMap(size);
}

public final Map build() {
        Map map = createMap(getTemporaryData().size());
        map.putAll(getTemporaryData());
        if (isUnmodifiable()) {
            map = Collections.unmodifiableMap(map);
        }
        return map;
}

Und damit stolperte ich in eine der häufigsten Fehler bei der Initialisierung von HashMaps. Gibt man als initial Kapazität einer HashMap die exakte Größe der hinzuzufügenden Objekte an, findet in jedem Fall eine Vergrößerung der Map beim Hinzufügen der Objekte statt. Denn der initiale Befüllungsfaktor von 0.75 von HashMap besagt, dass beim Erreichen der Befüllungskapazität von 75% die Map vergrößert werden soll. Um eine HashMap so zu befüllen, dass sie bei der Initialmenge nicht vergrößert werden muss (was Ressourcen kostet) gibt es zwei Möglichkeiten.

Erstens: Man setzt zugleich den Befüllungsfaktor auf 1

new HashMap(otherCollection.size() + 1, 1f);

Damit wird die Map nicht mehr vergrößert. Dies hat aber den Nachteil, dass wir eine Map zurückliefern, die evtl. unerwartetes Verhalten zeigt, weil sie nicht den gewohnten Befüllungsfaktor hat.

Zweitens:

new HashMap(1 + (int) (otherCollection.size() / 0.75));

Hier wird die Map unter Berücksichtigung des initialen Befüllungsfaktors größer gewählt als nötig, aber so, dass die Map nicht mehr vergrößert werden muss.

package mapbuilder;

import java.util.*;

/**
 * Generic MapBuilder base.
 *
 * @author Serhat Cinar
 */
public abstract class AbstractMapBuilder<K, V, B extends AbstractMapBuilder> {

    private final Map tmpMap = new LinkedHashMap();
    private boolean unmodifiable; // false


    protected Map getTemporaryData() {
        return tmpMap;
    }

    /**
     * Defines that the resulting Map should be unmodifiable as
     * done by {@link Collections#unmodifiableMap(Map)}.<br />
     * Default value is {@code false}.
     *
     * @param unmodifiable
     * @return
     */
    public B unmodifiable(final boolean unmodifiable) {
        this.unmodifiable = unmodifiable;
        return (B) this;
    }

    /**
     * Sets the {@link #unmodifiable(boolean)} to true.
     *
     * @return
     */
    public B unmodifiable() {
        return unmodifiable(true);
    }

    protected boolean isUnmodifiable() {
        return unmodifiable;
    }

    public B put(final K key, final V value) {
        tmpMap.put(key, value);
        return (B) this;
    }

    public B putAll(final Map valueMap) {
        tmpMap.putAll(valueMap);
        return (B) this;
    }

    /**
     * Expects both arrays to be equally long.
     *
     * @param keys
     * @param values
     * @return
     */
    public B putAll(final K[] keys, final V[] values) {
        for (int i = 0; i < keys.length; i++) {
            put(keys[i], values[i]);
        }
        return (B) this;
    }

    /**
     * Expects both Collections to have equal amounts of objects.
     *
     * @param keys
     * @param values
     * @return
     */
    public B putAll(final Collection keys, final Collection values) {
        Iterator keyIterator = keys.iterator();
        Iterator valuesIterator = values.iterator();
        while (keyIterator.hasNext()) {
            put(keyIterator.next(), valuesIterator.next());
        }
        return (B) this;
    }

    /**
     * Expects an array of arrays.<br />
     * The inner array must always have length 2 and the first element
     * must be the key, while the second one is the value.<br />
     * Example:<br />
     * <code>
     *     Object[][] keyValues = {{"key1", "value1"}, {"key2", "value2"}};
     * </code>
     *
     * @param keyValues
     * @return
     */
    public B putAll(final Object[][] keyValues) {
        for (int i = 0; i < keyValues.length; i++) {
            Object[] data = keyValues[i];
            put((K) data[0], (V) data[1]);
        }
        return (B) this;
    }

    /**
     * Expects an array of keys and values,
     * where each key is followed by it&#039;s value.<br />
     * Logically the length of the array must be even (dividable by 2).<br />
     * Example:<br />
     * <code>
     * Object[] keyValues = {"key1", "value1", "key2", "value2"};
     * </code>
     *
     * @param keyValues
     * @return
     */
    public B putAll(final Object[] keyValues) {
        for (int i = 0; i < keyValues.length; i += 2) {
            put((K) keyValues[i], (V) keyValues[i + 1]);
        }
        return (B) this;
    }

    /**
     * Returns the underlying Map.<br />
     * If {@link #unmodifiable(boolean)} was used, the returned Map
     * will be wrapped by {@link Collections#unmodifiableMap(Map)} .
     *
     * @return
     */
    public abstract Map build();
}
/**
 * Builder implementation for {@link TreeMap}s.
 *
 * @author Serhat Cinar
 */
public final class TreeMapBuilder extends AbstractMapBuilder<K, V, TreeMapBuilder> {
    private Comparator comparator;

    /**
     * Defines a compatrator for the {@link TreeMap}.
     *
     * @param comparator
     * @return
     */
    public TreeMapBuilder comparator(Comparator comparator) {
        this.comparator = comparator;
        return this;
    }

    /**
     * Returns the underlying Map.<br />
     * If {@link #unmodifiable(boolean)} was used, the returned Map
     * will be wrapped by {@link Collections#unmodifiableMap(Map)} .
     *
     * @return
     */
    public final Map build() {
        SortedMap map = null;
        if (comparator != null) {
            map = new TreeMap(comparator);
        } else {
            map = new TreeMap();
        }
        map.putAll(getTemporaryData());
        if (isUnmodifiable()) {
            map = Collections.unmodifiableSortedMap(map);
        }
        return map;
    }
}
/**
 * Abstract base implementation for Maps with initial capacity.
 *
 * @author Serhat Cinar
 */
public abstract class AbstractSizeDependentMapBuilder extends AbstractMapBuilder<K, V, AbstractSizeDependentMapBuilder> {
    private int initialCapacity = -1;

    public AbstractSizeDependentMapBuilder initialCapacity(int size) {
        this.initialCapacity = size;
        return this;
    }

    /**
     * Create a Map implementation with the given initial Capacity that will not result in resizing
     * the underlying map.
     *
     * @param size
     * @return
     */
    protected abstract Map createMap(int size);

    /**
     * Returns the underlying Map.<br />
     * If {@link #unmodifiable(boolean)} was used, the returned Map
     * will be wrapped by {@link Collections#unmodifiableMap(Map)} .
     *
     * @return
     */
    @Override
    public final Map build() {
        // Fit initialCapacity
        // No sense in initializing with a lower capacity than needed by given data.
        final int tmpCapacity = Math.max(initialCapacity, 1 + (int) (getTemporaryData().size() / 0.75));
        Map map = createMap(initialCapacity);
        map.putAll(getTemporaryData());
        if (isUnmodifiable()) {
            map = Collections.unmodifiableMap(map);
        }
        return map;
    }
}
/**
 * Builder implementation for {@link HashMap}s.
 *
 * @author Serhat Cinar
 */
public class HashMapBuilder extends AbstractSizeDependentMapBuilder {
    @Override
    protected final Map createMap(int size) {
        return new HashMap(size);
    }
}
/**
 * Builder implementation for {@link LinkedHashMap}s.
 *
 * @author Serhat Cinar
 */
public class LinkedHashMapBuilder extends AbstractSizeDependentMapBuilder {
    @Override
    protected final Map createMap(int size) {
        return new LinkedHashMap(size);
    }
}
/**
 * Helper class to access all builder variants.
 *
 * @author Serhat Cinar
 */
public class Maps {
    public static  HashMapBuilder hashMap(){
        return new HashMapBuilder();
    }

    public static  LinkedHashMapBuilder linkedHashMap(){
        return new LinkedHashMapBuilder();
    }

    public static  TreeMapBuilder treeMap(){
        return new TreeMapBuilder();
    }
}