Eigene equals+hashCode-Funktionen für Sets und Maps

Hallo

Schon häufiger habe ich mir gedacht, dass es cool wäre, wenn equals und hashCode nicht so mit Object verbandelt wären. Gerade die Frage, wann zwei Objekte gleich sein sollen, ist doch sehr stark vom Kontext abhängig.

Bei verschiedenen Vergleichskriterien wurde das ja schon gemacht: Man könnte Comparable mit seinem compareTo implementieren (was man aber IMHO nur seltenst tun sollte), oder verschiedene Comparator-Instanzen verwenden.

Leider gibt es aber für equals und hashCode nichts vergleichbares. Im speziellen gibt es eben keinen EqualityChecker und HashcodeComputer - und selbst wenn es sie gäbe, würden sie einem nichts bringen, weil sie in den relevanten Klassen (speziell den Set- und Map-Implementierungen) nicht verwendet werden. Traurig.

Deswegen habe ich gerade mal geschaut, was man da machen kann. Die Frage ist nicht neu…

java - Why not allow an external interface to provide hashCode/equals for a HashMap? - Stack Overflow
java - Is there a good way to have a Map<String, ?> get and put ignoring case? - Stack Overflow
java - Why not allow an external interface to provide hashCode/equals for a HashMap? - Stack Overflow
guava - Java: external class for determining equivalence? - Stack Overflow

… und es gibt verschieden Lösungsansätze, die entweder hackige Workarounds sind, oder als Speziallösungen oder Teile rieeesiger Libraries daherkommen…

HashingStrategy (Trove 3.0.0)
https://commons.apache.org/proper/commons-collections/javadocs/api-release/org/apache/commons/collections4/map/AbstractHashedMap.html

Ich dachte mir, dass das mit den neuen Funktionalen Interfaces doch leichter möglich sein sollte. Ein BiPredicate für equals und eine ToIntFunction für hashCode sollten es ja tun. Die Ergebnisse…:

import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.ToIntFunction;

/**
 * Implementation of the <code>Map</code> interface that wraps the keys in 
 * order to allow an external, functional definition of <code>equals</code> 
 * and <code>hashCode</code>.
 *
 * @param <K> The key type
 * @param <V> The value type
 */
class WrappingMap<K, V> extends AbstractMap<K, V> implements Map<K, V>
{
    // Note: This class extends AbstractMap, although most functions that are 
    // inherited from AbstractMap are overridden for performance reasons.
    // The functions from AbstractMap that are still used are
    // toString, equals, hashCode, putAll
    
    /**
     * The class that is wrapped around an object and uses the user-defined
     * <code>equals</code> and <code>hashCode</code> functions to compute
     * equality and hash codes of the objects.
     */
    private class Wrapper
    {
        /**
         * The actual object
         */
        private final K element;
        
        /**
         * Creates a new wrapper for the given object
         * 
         * @param object The object
         */
        Wrapper(K object)
        {
            this.element = object;
        }
        
        @Override
        public int hashCode()
        {
            return hashCode.applyAsInt(element);
        }
        
        @Override
        public boolean equals(Object object)
        {
            if (object == null)
            {
                return false;
            }
            if (object == this)
            {
                return true;
            }
            if (!(object instanceof WrappingMap.Wrapper))
            {
                return false;
            }
            WrappingMap<?, ?>.Wrapper other = 
                (WrappingMap<?, ?>.Wrapper)object;
            if (!keyClass.isInstance(other.element) && other.element != null)
            {
                return false;
            }
            return equals.test(element, keyClass.cast(other.element));
        }
    }
    
    /**
     * The class representing the type of the keys in this map
     */
    private final Class<K> keyClass;
    
    /**
     * The predicate that determines the equality of keys
     */
    private final BiPredicate<? super K, ? super K> equals;
    
    /**
     * The function that computes hash codes for keys
     */
    private final ToIntFunction<? super K> hashCode;

    /**
     * The delegate map that stores the wrapped keys
     */
    private final Map<Wrapper, V> delegateMap;


    /**
     * Default constructor, using the default <code>equals</code> and
     * <code>hashCode</code> functions
     * 
     * @param keyClass The type of the keys in this map 
     */
    public WrappingMap(Class<K> keyClass)
    {
        this(
            keyClass,
            (t0, t1) -> Objects.equals(t0, t1),
            (t) -> Objects.hashCode(t));
    }
    
    /**
     * Creates a new wrapping map that uses the given functions
     * to determine the equality and hash codes of keys
     * 
     * @param keyClass The type of the keys in this map 
     * @param equals The predicate that determines the equality of keys
     * @param hashCode The function that computes hash codes of keys
     */
    public WrappingMap(
        Class<K> keyClass,
        BiPredicate<? super K, ? super K> equals,
        ToIntFunction<? super K> hashCode)
    {
        this.keyClass = keyClass;
        this.equals = equals;
        this.hashCode = hashCode;
        this.delegateMap = new LinkedHashMap<Wrapper, V>();
    }
    

    /**
     * Wraps the given object into a {@link Wrapper}
     * 
     * @param object The object
     * @return The {@link Wrapper}
     * @throws ClassCastException If the given object can not be 
     * cast to <code>K</code>
     */
    private Wrapper wrap(Object object)
    {
        Wrapper wrapper = new Wrapper(keyClass.cast(object));
        return wrapper;
    }
    
    @Override
    public int size()
    {
        return delegateMap.size();
    }

    @Override
    public boolean isEmpty()
    {
        return delegateMap.isEmpty();
    }
    

    @Override
    public boolean containsKey(Object object)
    {
        if (!keyClass.isInstance(object) && object != null)
        {
            return false;
        }
        return delegateMap.containsKey(wrap(object));
    }

    @Override
    public boolean containsValue(Object value)
    {
        return delegateMap.containsValue(value);
    }

    @Override
    public V get(Object object)
    {
        if (!keyClass.isInstance(object) && object != null)
        {
            return null;
        }
        return delegateMap.get(wrap(object));
    }

    @Override
    public V put(K key, V value)
    {
        return delegateMap.put(wrap(key), value);
    }

    @Override
    public V remove(Object object)
    {
        if (!keyClass.isInstance(object) && object != null)
        {
            return null;
        }
        return delegateMap.remove(wrap(object));
    }

    @Override
    public void clear()
    {
        delegateMap.clear();
    }

    @Override
    public Set<K> keySet()
    {
        return new AbstractSet<K>()
        {
            @Override
            public Iterator<K> iterator()
            {
                Iterator<Wrapper> delegateKeySetIterator =
                    delegateMap.keySet().iterator();
                return new Iterator<K>()
                {
                    @Override
                    public boolean hasNext()
                    {
                        return delegateKeySetIterator.hasNext();
                    }

                    @Override
                    public K next()
                    {
                        Wrapper wrapper = delegateKeySetIterator.next();
                        return wrapper.element;
                    }
                    
                    @Override
                    public void remove()
                    {
                        delegateKeySetIterator.remove();
                    }
                };
            }

            @Override
            public int size()
            {
                return delegateMap.size();
            }
        };
    }
    
    @Override
    public Collection<V> values()
    {
        return delegateMap.values();
    }

    @Override
    public Set<Entry<K, V>> entrySet()
    {
        return new AbstractSet<Map.Entry<K,V>>()
        {
            @Override
            public Iterator<Entry<K, V>> iterator()
            {
                Iterator<Entry<Wrapper, V>> delegateEntrySetIterator =
                    delegateMap.entrySet().iterator();
                return new Iterator<Map.Entry<K,V>>()
                {
                    @Override
                    public boolean hasNext()
                    {
                        return delegateEntrySetIterator.hasNext();
                    }

                    @Override
                    public Entry<K, V> next()
                    {
                        Entry<Wrapper, V> entry = 
                            delegateEntrySetIterator.next();
                        return new Entry<K, V>()
                        {
                            @Override
                            public K getKey()
                            {
                                Wrapper wrapper = entry.getKey();
                                return wrapper.element;
                            }

                            @Override
                            public V getValue()
                            {
                                return entry.getValue();
                            }
                            
                            @Override
                            public V setValue(V value)
                            {
                                return entry.setValue(value);
                            }
                        };
                    }

                    @Override
                    public void remove()
                    {
                        delegateEntrySetIterator.remove();
                    }
                    
                };
            }

            @Override
            public int size()
            {
                return delegateMap.size();
            }
        };
    }
    
}
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.ToIntFunction;

/**
 * Implementation of the <code>Set</code> interface that wraps the elements in 
 * order to allow an external, functional definition of <code>equals</code> 
 * and <code>hashCode</code>.
 *
 * @param <T> The element type
 */
public class WrappingSet<T> extends AbstractSet<T> implements Set<T>
{
    // Note: This class extends AbstractSet, although most functions that are 
    // inherited from AbstractSet are overridden for performance reasons.
    // The functions from AbstractSet that are still used are toString, 
    // equals, hashCode, addAll, containsAll, toArray, retainAll, removeAll
    
    /**
     * The class that is wrapped around an object and uses the user-defined
     * <code>equals</code> and <code>hashCode</code> functions to compute
     * equality and hash codes of the objects.
     */
    private class Wrapper
    {
        /**
         * The actual object
         */
        private final T element;
        
        /**
         * Creates a new wrapper for the given object
         * 
         * @param object The object
         */
        Wrapper(T object)
        {
            this.element = object;
        }
        
        @Override
        public int hashCode()
        {
            return hashCode.applyAsInt(element);
        }
        
        @Override
        public boolean equals(Object object)
        {
            if (object == null)
            {
                return false;
            }
            if (object == this)
            {
                return true;
            }
            if (!(object instanceof WrappingSet.Wrapper))
            {
                return false;
            }
            WrappingSet<?>.Wrapper other = 
                (WrappingSet<?>.Wrapper)object;
            if (!elementClass.isInstance(other.element) && 
                other.element != null)
            {
                return false;
            }
            return equals.test(element, elementClass.cast(other.element));
        }
    }
    
    /**
     * The class representing the type of the elements in this set
     */
    private final Class<T> elementClass;
    
    /**
     * The predicate that determines the equality of elements
     */
    private final BiPredicate<? super T, ? super T> equals;
    
    /**
     * The function that computes hash codes for elements
     */
    private final ToIntFunction<? super T> hashCode;

    /**
     * The delegate set that stores the wrapped elements
     */
    private final Set<Wrapper> delegateSet;

    /**
     * Default constructor, using the default <code>equals</code> and
     * <code>hashCode</code> functions
     * 
     * @param elementClass The type of the elements in this set 
     */
    public WrappingSet(Class<T> elementClass)
    {
        this(
            elementClass,
            (t0, t1) -> Objects.equals(t0, t1),
            (t) -> Objects.hashCode(t));
    }
    
    /**
     * Creates a new wrapping set that uses the given functions
     * to determine the equality and hash codes of elements
     * 
     * @param elementClass The type of the elements in this set 
     * @param equals The predicate that determines the equality of elements
     * @param hashCode The function that computes hash codes of elements
     */
    public WrappingSet(
        Class<T> elementClass,
        BiPredicate<? super T, ? super T> equals,
        ToIntFunction<? super T> hashCode)
    {
        this.elementClass = elementClass;
        this.equals = equals;
        this.hashCode = hashCode;
        this.delegateSet = new LinkedHashSet<Wrapper>();
    }
    
    /**
     * Wraps the given object into a {@link Wrapper}
     * 
     * @param object The object
     * @return The {@link Wrapper}
     * @throws ClassCastException If the given object can not be 
     * cast to <code>T</code>
     */
    private Wrapper wrap(Object object)
    {
        Wrapper wrapper = new Wrapper(elementClass.cast(object));
        return wrapper;
    }
    
    @Override
    public int size()
    {
        return delegateSet.size();
    }

    @Override
    public boolean isEmpty()
    {
        return delegateSet.isEmpty();
    }

    @Override
    public boolean contains(Object object)
    {
        if (!elementClass.isInstance(object) && object != null)
        {
            return false;
        }
        return delegateSet.contains(wrap(object));
    }

    @Override
    public Iterator<T> iterator()
    {
        Iterator<Wrapper> delegateIterator =
            delegateSet.iterator();
        return new Iterator<T>()
        {
            @Override
            public boolean hasNext()
            {
                return delegateIterator.hasNext();
            }

            @Override
            public T next()
            {
                Wrapper wrapper = delegateIterator.next();
                return wrapper.element;
            }
            
            @Override
            public void remove()
            {
                delegateIterator.remove();
            }
        };
    }

    @Override
    public boolean add(T element)
    {
        return delegateSet.add(wrap(element));
    }

    @Override
    public boolean remove(Object object)
    {
        if (!elementClass.isInstance(object) && object != null)
        {
            return false;
        }
        return delegateSet.remove(wrap(object));
    }

    @Override
    public void clear()
    {
        delegateSet.clear();
    }

}

Und ein kurzer(!) Test:

import java.util.Map;
import java.util.Set;

public class WrappingCollectionsTest
{
    public static void main(String[] args)
    {
        testMap();
        testSet();
    }

    private static void testMap()
    {
        Map<String, Integer> map0 = new WrappingMap<String, Integer>(
            String.class,
            (k0, k1) -> k0 == null ? k1 == null : k0.equalsIgnoreCase(k1), 
            (k) -> k == null ? 0 : k.toLowerCase().hashCode()
        );
        
        map0.put("AAA", 111);
        map0.put(null, -666);
        map0.put("CCC", 333);
        map0.put("aaa", 112);
        map0.put("BBB", 222);
        
        System.out.println("map0: "+map0);
        
        Map<String, Integer> map1 = new WrappingMap<String, Integer>(
            String.class,
            (k0, k1) -> k0 == null ? k1 == null : k0.equalsIgnoreCase(k1), 
            (k) -> k == null ? 0 : k.toLowerCase().hashCode()
        );
        
        map1.put("AAA", 112);
        map1.put("bbb", 222);
        map1.put(null, -666);
        map1.put("CCC", 333);
        map1.put("DDD", 444);
        map1.remove(42);
        map1.remove("ddd");
        
        System.out.println("map1: "+map1);

        System.out.println("equal? "+map0.equals(map1));
        
        System.out.println("map1.containsKey(\"ccc\")? "+
            map1.containsKey("ccc"));
        
    }
    
    
    private static void testSet()
    {
        Set<String> set0 = new WrappingSet<String>(
            String.class,
            (k0, k1) -> k0 == null ? k1 == null : k0.equalsIgnoreCase(k1), 
            (k) -> k == null ? 0 : k.toLowerCase().hashCode()
        );
        
        set0.add("AAA");
        set0.add("aaa");
        set0.add("BBB");
        set0.add(null);
        set0.add("ccc");
        
        System.out.println("set0: "+set0);
        
        Set<String> set1 = new WrappingSet<String>(
            String.class,
            (k0, k1) -> k0 == null ? k1 == null : k0.equalsIgnoreCase(k1), 
            (k) -> k == null ? 0 : k.toLowerCase().hashCode()
        );
        
        set1.add("AAA");
        set1.add("BBB");
        set1.add(null);
        set1.add("CCC");

        System.out.println("set1: "+set1);
        
        System.out.println("equal? "+set0.equals(set1));
        
        System.out.println("set1.contains(\"ccc\")? "+
            set1.contains("ccc"));
        
    }
    
}

Gedanken dazu?

Dein Denkfehler ist: equals() ist nicht kontextabhängung, sondern Klassenabhängig. Eine generische equals()-Methode ist schlicht sinnlos.

Genau so kann hashcode() nicht generisch implementiert werden, weil zwei Objekte, für die euals() war ist, auch immer den selben Hashcode haben müssen und schlimmer: der Hashcode darf sich während der Lebenszeit eines Objekts nicht ändern.

Gern gemachter Gedankenfehler: veränderbare Datenobjekte.

Beispiel:
[spoiler]```public class Car {
private Color color;
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}

private int seats;
public Car(Color color,int seats){this.color=color;this.seats=seats;}
@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((color == null) ? 0 : color.hashCode());
    result = prime * result + seats;
    return result;
}
@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Car other = (Car) obj;
    if (color == null) {
        if (other.color != null)
            return false;
    } else if (!color.equals(other.color))
        return false;
    if (seats != other.seats)
        return false;
    return true;
}


@Override
public String toString() {
    return "Car [color=" + color + ", seats=" + seats + "]";
}
public static void main(String[] args) {
    Car myCar = new Car(Color.BLACK, 4);
    HashSet<Car> myCars = new HashSet<Car>();
    myCars.add(myCar);
    System.out.println(myCars);
    System.out.println(myCar);
    System.out.println(myCars.contains(myCar)); // true
    myCar.setColor(Color.RED);        
    System.out.println(myCar);
    System.out.println(myCars);
    System.out.println(myCars.contains(myCar)); // false!!!!
}

}```[/spoiler]

Für alles andere gibt es ja bereits Bibliotheksfunktionen, die man mit einem Comparator füttern kann.

bye
TT

Ich stolpere ebenfalls durchaus öfter darüber, dass ich gerne verschiedene Arten von (Un)Gleichheit hätte. Wenn man wie bei Map einen Comparator mitgeben kann, ist das ja unproblematisch, aber das geht leider nicht immer. Und dann geht die Frickelei los.

Equals als extra Interface wäre nur logisch (mit Comparator als Erweiterung), Haskell macht es z.B. so: https://hackage.haskell.org/package/base-4.8.1.0/docs/Data-Eq.html

Insbesondere hätte man dann nicht das Problem, das Leute versuchen, völlig ungeeignete Klassen als Schlüssel in HashMap u.s.w. zu verwenden.

Hm. Kapier’ ich nicht. Von einer generischen equals-Methode war ja nie die Rede. Und equals kann sehr wohl Kontextabhängig sein, und ist nicht Klassenabhängig (sofern du „Klasse“ hier (wie ich vermute) auf Java-Klassen beziehst, und nicht auf mathematische - denn genau um letztere geht es ja :wink: ). Mehr als irgendeine Äquivalenzrelation muss durch equals entsprechend den JavaDocs ja gar nicht beschrieben sein. Und wann man zwei Objekte als „äquivalent“ ansieht, ist abhängig davon, wie und wo man sie verwenden will. Das Beispiel zeigt ja einen möglichen Anwendungsfall: Man will ein Set mit Strings, und die sollen unabhängig von Groß- und Kleinschreibung als „gleich“ gelten. Woanders sollen vielleicht 4 und 16 gleich sein, weil sie eine 12-Stunden-Uhrzeit beschreiben. Zwei 3D-Vektoren (1,1,1) und (2,2,2) sollen vielleicht als „gleich“ gelten, weil sie die gleiche Richtung haben. Vielleicht gibt es eine Klasse „Person“ auf der equals so implementiert ist, dass sie Name und Alter vergleicht, und ich will zwei Personen als „gleich“ ansehen, wenn der Name gleich ist. Vielleicht gibt es eine (closed-source) Klasse „Person“ auf der equals gar nicht implementiert ist, aber ich würde gerne Objekte davon in eine Set packen. All das kann man lösen, wenn man von außen festlegen kann, was equals und hashCode genau machen.

Ein Comparator implementiert eine Ordnungsrelation, und keine Äquivalenzrelation. Abgesehen davon, dass man so eine Ordnung nicht immer (d.h. sehr oft nicht) angeben kann, sollte diese Ordnung - wie auch immer sie aussieht - „consistent with equals“ sein (ansonsten muss man zumindest damit rechnen, dass es die entsprechende Map/Set-Implementierung raushaut…). Das klappt halt nicht, wenn man von außen sagen kann, wann zwei Objekte zwar laut „comparator.compare(a,b)==0“ gleich sind, aber nicht, wann sie auch laut „a.equals(b)“ gleich sind - sofern man das nicht einem eigenen „equalityTest.test(a,b)“ überlassen kann.

Du könntest aber auch die entsprechenden Objekte per Komposition/Vererbung/Delegate in eine „Wrapper“-Klasse packen und equals/hashCode entsprechend modifizieren bzw. überschreiben. Aber das müsste man dann jedesmal aufs neue machen und ist wohl nicht so elegant wie deine „neue“ WrapperMaps/WrapperSet

setzt natürlich voraus, dass alles was du brauchst „public“ ist - sollte natürlich so sein, aber ob das immer gegeben ist?

[quote=Marco13]Das Beispiel zeigt ja einen möglichen Anwendungsfall: Man will ein Set mit Strings, und die sollen unabhängig von Groß- und Kleinschreibung als “gleich” gelten. Woanders sollen vielleicht 4 und 16 gleich sein, weil sie eine 12-Stunden-Uhrzeit beschreiben.[/quote]hashcode() und equals() sind für die Verwendung in den Java-Collections vorgesehen. Genau deswegen werden diese beiden Methoden von allen Objekten in Java implementiert. Was Du hier beschreibst ist der Anwendungsfall von compareTo() aus dem Comparable Intreface.

Das kann man natürlich auch ignorieren oder abstreiten und auf das blöde Design der Java API schimpfen…:ka:

bye
TT

Genau das wird ja gemacht. Und das einzig relevante - nämlich die Implementierung der beiden Methoden - wird von außen als Funktionen reingegeben, so dass das Wrappen dann transparent und unbemerkt stattfindet. (Noch schöner wäre es natürlich, wenn die Collections-Klassen schon von sich aus diese Punkte zum Einhaken bieten würden, so wie es z.B. die verlinkte AbstractHashedMap macht: Dort kann man Methoden überschreiben, die für Gleichheitstests und Hash-Berechnung verwendet werden)

Nun, man kann drüber streiten und philosophieren. Genauso, wie man IMHO Comparable seltenST implementieren sollte, und stattdessen Comparator verwenden sollte, wäre eine Auslagerung von hashcode() und equals() (analog dazu!) manchmal sinnvoll. Auf die Collections API schimpfe ich nicht (oder kaum ;)). Die ist time-tested und weitgehend stimmig. Aber sicher nicht perfekt. Die oben verlinkten Fragen und Libraries deuten ja darauf hin, dass ich zumindest nicht der einzige bin, der darin einen realistischen und sinnvollen Anwendungsfall sieht.

Nö, die Sunentwickler haben gesagt wir brauchen ein hashcode() und ein equals() in allen Objekten und haben das so umgesetzt. Gibts nichts drüber zu philosophieren.

Zu deinem Thema: Ja, finde das nützlich. Ist zwar nur ein kleiner Eingriff aber es macht doch praktisch jeder irgendwie irgendwo irgendwann einmal und dann schreibt man halt eine for-Schleife oder ein forEach-Lambda-Konstrukt, spart einem dann halt stattdessen ein paar Zeilen Code und Redundanz.
Unscheinbares aber nützliches Detail. Hart arbeiten und viel Bewegen kann jeder, aber die wichtigen Dinge liegen im Detail.

Hallo Marco,

meine Gedanken zu Deinem Code:
[ul]
[li]Du leitest von einer Zwischenklasse ab (AbstractSet/-Map). Du implementierst also nicht direkt das Interface. Weiters hast Du den Konstruktor selbst unter Kontrolle. Dort kannst Du den element type direkt herausfinden. Damit wäre die Übergabe der Klasse im Konstruktor überflüssig. Ich habe sowas mal aus einem Artikel im Internet geklaut, bei dem es um die Impelentierung generischer DAOs ging. Ich finde ihn um’s Verrecken nicht wieder. Deswegen nur der Pseudocode: Class.getGenericSuperclass() casten nach ParameterizedType, ParameterizedType.getActualTypeArguments()[0] casten nach Class bzw Class.[/li][li]Ansonsten merkt man, dass Du den Quellcode der Collections-Klassen mehr als nur einmal gelesen hast ;)[/li][/ul]

Meine Gedanken zur Implementierung:
[ul]
[li]Das Wrappen taugt für mich nur für einen Proof of Concept. Der ist Dir aber wahrlich gelungen! Man schleppt da nämlich für jedes Element einen Wrapper mit. Das ist bei größeren Mengen bemerkbar. Bei jedem Lookup muss gewprapped werden. Das kostet Zeit. Und es fliegt eine ganze Menge Müll auf dem Stack rum, der eingesammelt werden muss.[/li][li]In einer Echtwelt Implementierung würde man die HashMap wohl in einer flexibleren Variante quasi noch mal implementieren. Diese würde -anstatt der direkten Aufrufe von equals und hashCode- an die übergebenen Funktionen delegieren, um die Elemente auf die internen Datenstrukturen aufzuteilen und sie dort wieder zu finden.[/li][/ul]

Meine Gedanken zum Konzept:
[ul]
[li]Timothy Tuckles’ Anmerkungen zum Thema Comparator mögen unpassend erscheinen. Bei Deinem Konzept geht es gerade darum, dass man 1. Hash-basierte Lookups haben möchte und keine Tree-basierten und 2. dass man eine Sortierreihenfolge der Elemente eben nicht angeben kann oder will. Aber, wenn Du über reale Anwendungsbeispiele nachdenkst -und auch das von Dir anprogrammierte equalsIgnoreCase gehört dazu-, dann wird Dir wenig einfallen, für das man nicht doch einen Comparator schreiben kann. Bei denen ist man dann natürlich auf Trees festgelegt. Als Argument bleibt also noch, dass man unbedingt Hashes will. Rechtfertigt das den Aufwand? (Kann man sicher so und so beantworten)[/li][li]Ich habe lange darüber nachgedacht. Ich finde das Problem “equals und hashCode stehen fest” ist eher ein “Forenproblem”. Meistens kommt man damit gut zurecht. Nur, weil andere funktionale Sprachen da flexibler sind, macht man sich in Java dazu plötzlich Gedanken. Kann man auch anders sehen, ich weiß. Sind hier halt meine Gedanken. Das ist ansonsten ja auch eine eigene Diskussion.[/li][li]Du näherst Dich der Funktionalität Bottom-Up. Du gehst also von einer nicht funktionalen Klasse her und pimpst sie funktional. Deine Klassen können jetzt equals und hashCode redefinieren. Nicht weniger, aber leider auch nicht mehr. Wenn ich schon über Funktionalität und neue Konzepte nachdenke, warum dann nur equals für das finden von Elementen? Warum keine Regex-, oder startsWith-Matches? Klar wären das dann keine Maps. Mir kommt es nur auf die Top-Down Denkweise an. Die bringt einen glaube ich schneller zum Ziel[/li][li]Nicht funktional genug: Die Implementierungen des Collections API sind geniale Gebilde. Sie ermöglichen den Zugriff auf alle gängigen Datenstrukturen ohne Kenntnis deren inneren Aufbaus. Mann kann die komplette Stunde zum Thema “doppelt verkettete Listen” verpennt haben und kann trotzdem eine LinkedList verwenden.[/li]
Aber aus Sicht eines Implementierers sind sie allesamt Monster. Man muss schon sehr lange den Quellcode und die Javadocs lesen, bevor man in der Lage ist, eine eigene fehlerfreie Implementierung zu hinzubekommen. Außerdem sind sie aufgrund der großen Methodenzahl eine Qual. Das wirst Du merken, weil Du viel Zeit brauchen wirst, um die Unittests für Deine Klassen zu schreiben.

Auch hinsichtlich Clientcodes ist die große Anzahl Methden problematisch. Dazu ein Code Beispiel:

//
// Was macht dieser Code? Verändert er die Map?
// Schaut er nur rein?
// Ich muss den Methodenrumpf lesen
public void useMap(Map<K,V> map) {..}

// Funktionale API, Konzentration auf die benötigte Funktionalität
//
// Aha, hier wird irgendwas für Lookups gebraucht.
// Ich muss den Methodenrumpf nicht unbedingt lesen.
// Auf keinen Fall brauche ich Angst zu haben, dass meine Map verändert wird.
// Und das ganz ohne Collections.unmodifiableBLUBBER!!!
public void useFunction(Function<K,V> lookup){...}

Das Beispiel soll verdeutlichen, was funktional bedeutet: Die Konzentration auf genau die (eine) Funktionalität, die ich an einer bestimmten Stelle brauche. Radikal abstrahiert durch die Verwendung funktionaler Interfaces mit genau einer abstrakten Methode. Dein Code wird noch lesbarer werden, wenn Du Deine APIs so designst. Und wenn Du Deine APIs so designed hast, wirst Du merken, wie schnuckelig Methodenreferenzen sind. Du wirst dann keine Instanz einer Map mehr übergeben, sondern map::get oder sonst eine Referenz zu einer Methode, deren Signatur passt. Da ist die Auswahl schon im JDK groß…

Das hat auch nichts mit Deinem Map-Thema zu tun. Aber es soll verdeutlichen, warum ich schon den Ansatz falsch finde, eine Map funktional zu pimpen. Wenn Du einen Lookup haben willst, der anders auswählt als equals, dann definier ihn Dir. Dahinter kann gerne eine Map hängen, aber vielleicht tut’s ja auch ein Array, wenn es nicht zu viele Elemente werden
[/ul]
Ich fand Deinen Post übrigens gerade für mich sehr passend. Ich habe mich nämlich auch gerade mit Lookups und Maps befasst. Ich wollte einen startsWith-Lookup implementieren. Und stand vor der Frage, wie man das funktional am besten macht. Herausgekommen ist ein interface ArraySupplier extends Supplier<E[]> erweit um ein paar sehr nützliche default-Methods. Meine Replik ist darum etwas lang ausgefallen.

Ah - doch noch mehr Feedback :slight_smile:

Hmnee, das geht in dem Fall AFAIK leider nicht, weil die Klasse selbst ja noch parameterisiert ist (ich hatte es auch probiert, weil ich die Notwendigkeit, dort die Class zu übergeben, gräßlich finde). Das würde nur funktionieren, wenn man von einer Klasse erben und dabei die Typparameter der Elternklasse festtackern würde (also sowas wie MyMap implements Map<String,Integer>). Lasse mich aber gern eines besseren belehren :smiley:

Dessen bin ich mir bewußt. Sowohl in bezug auf Performance als auch in Bezug auf den Speicherbedarf ist das natürlich schlecht. Man muss das IMHO unter Berücksichtigung des Anwendungsfalls betrachten - und zwar

  1. des Anwendungsfalls in bezug auf die Fragen „Welche/Wieviele Daten sind da drin?“ und „Wie performance/speicherkritisch ist das?“,
  2. des Anwendungsfalls in bezug auf die Frage, wie und wann man diese Klassen für welchen Zweck verwenden will
    Die Unterscheidung mag im ersten Moment künstlich wirken. Aber es geht darum, dass Performance und Speicher in den seltensten Fällen die wichtigsten Kriterien sind, im Vergleich dazu, ob die Klasse ihren Zweck erfüllt, leicht zu verwenden und vielseitig einsetzbar ist - bzw. ob die Klasse überhaupt die richtige Herangehensweise für ein Problem ist.

Als Beispiel (auch eingehend auf das Infragestellen des ganzen Ansatzes bisher - vielleicht klärt das einiges), in der Hoffnung dass es nicht zuuu suggestiv wirkt: Wenn man eine Klasse „Person“ hat, ganz klassisch

class Person {
    String getFirstName() { ... }
    String getLastName() { ... }
    int getAge() { ... }
    float getHeight() { ... }
}

Dann könnte man z.B. vor der Aufgabe stehen: „Fasse alle Personen zusammen, die den gleichen Vornamen haben“. (Oder anders formuliert: „Clustere die Personen nach dem Vornamen“, oder auch „Bilde die Äquivalenzklassen bezüglich der Relation ‚hatGleichenVornamen‘“). Das wäre dann einfach:

Map<String, List<Person>> firstNamesToPersonsWithThisFirstName = ...;

und los. Um später die Gruppe zu finden, zu der eine Person gehört, kann man in dieser Map einfach mit map.get(person.getFirstName()); nachschauen.

Schwieriger wird es schon, wenn die Aufgabe ist: „Fasse alle Personen zusammen, die den gleichen Vor- und Nachnamen haben“. Was macht man dann? Die Strings konkatenieren? :wink: Oder eine

Map<Pair<String,String>, List<Person>> firstAndLastNamesToPersonsWithTheseNames = ...;

erstellen? Und bei mehr Attributen dann auf javatuples - Main zurückgreifen? Man sieht schon: Das skaliert nicht mit den Kombinationsmöglichkeiten, die es da geben kann. Mal abgesehen von der Frage, wie die Signatur einer Methode aussehen sollte, die so ein „Clustering“ übergeben bekommt: void process(Map<???, List<Person>> map).

Spätestens, wenn das Kriterium, nach dem gruppiert werden soll, flexibel sein soll (also ggf. erst zur Laufzeit bestimmt wird, weil der Benutzer im GUI bei „Vorname“ und „Nachname“ ein Häkchen gesetzt hat!) ist man mit diesen Ansätzen ohnehin schnell am Ende.

Eine allgemeine Lösung wäre da eben, eine

Map<Person, List<Person>> personsToTheirGroups...

bei der das Gruppierungskriterium (aka hashCode und equals) frei übergeben werden kann (und nebenbei für denjenigen, der diese Map verwenden will, keine Rolle mehr spielt).

Nochmal: Das ist ein recht künstliches Beispiel - aber andererseits schon SEHR nah an dem, wofür ich diese Map/Set ggf. einzusetzen gedachte. Einerseits sehe ich schon voraus, dass jetzt Antworten kommen die mit „Jaaaa, aber in diesem Fall könntest du auch einfach… … …“, und ich sehe auch voraus, dass einige dieser Antworten zu stark an diesem Beispiel orientiert sind, aber wenn jemand im abstraktesten Sinne Alternativlösungen wüßte, würde ich die gerne hören :slight_smile: (Ich könnte mir auch Alternativen überlegen (und werde auch darüber nachdenken, bevor ich diese Klassen für die tatsächliche Verwendung in Betracht ziehe), aber … das hier ist ein Forum, und dieser Thread hat einen Grund :slight_smile: ). (Um das vorwegzunehmen: Ja, das erinnert an Datenbanken, „groupBy“ usw. - und tatsächlich könnte eine in-Memory-DB hier eine echte Alternative sein!)

Sicher. Das hatte ich ja (bezugnehmend auf die verlinkten Libs) schon angedeutet. Damit würde der Speicher- und Performanceoverhead wegfallen. So eine „eigene“ Implementierung einer LinkedHashMapWithCustomHashCodeAndEquals hätte dann aber nicht 50 Zeilen, sondern 500 oder mehr. (Der beste Code ist der, den man NICHT schreibt. So eine Klasse dann Collections-Spec-Konform zu machen wäre praktisch auch nur möglich, wenn man die LinkedHashMap kopiert und alle Aufrufe von hashCode und equals mechanisch durch den Aufruf einer eigenen (überschreibbaren) Methode ersetzt…)

Abgesehen von dem Punkt, den ich schon erwähnt hatte (Nur ein Comparator reicht nicht: Man MUSS sich auch um’s equals kümmern - sonst kracht’s!) reicht vielleicht schon das obige „Person“-Beispiel als Gegenargument. Es gibt oft Fälle, wo man auf den Entitäten um die es geht, keine Ordnungsrelation definieren kann. Eine Ordnung ist etwas sehr starkes, bedeutsames, im Vergleich zu reinen Gleichheits/Äquivalenztests (auch wenn die Kriterien für eine Ordnungsrelation – Wikipedia und eine Äquivalenzrelation – Wikipedia erstmal recht ähnlich aussehen)

Nun, der Ansatz ging von einer (mehr oder weniger) konkreten Anforderung aus: Ich will definieren können, wann zwei Objekte als „gleich“ zu gelten haben. Und das nicht (nur) für eine reine Suche, sondern um die gesammelte Funktionalität der Collections-API verwenden zu können. Ich will einer Methode eine Map übergeben können, und keine „RegexMatchingMap“…

Irgendwann hatte ich schonmal nach „Unittest für die Java Collection Klassen“ gesucht, und dabei „nur“ die von Guava gefunden. Die Quintessenz ist: Das macht niemand. Niemand, der mal kurz in einem 5-Zeiler (!) von AbstractList erbt, wird da die „theoretisch notwendige“ Maschinerie von Unit-Tests für Iterable, Collection und List drauf loslassen. Dass bei den obigen Klassen einiges schiefgehen kann, und man sie „eigentlich“ testen „müßte“ ist klar, aber es ist schlicht unpraktikabel. (Ich sehe einen Hauptzweck der Abstract*-Klassen gerade darin, dass man das nicht machen muss).

Einerseits stimme ich da voll zu. Über die Fragen der best practices in dieser Hinsicht habe ich schon viel nachgedacht. Ich hadere oft noch an vielen Stellen. Und es könnte sein, dass eine Diskussion darüber den Rahmen hier sprengen würde. Der Fall, wo so etwas wie eine Map nur verwendet wird, ist da noch der einfachste. Klar: Statt eine Methode

String someGenericMethod(SomeSpecificClass c) { c.getSomeString()+" Boo"; }

zu schreiben, kann man auch schreiben

String someGenericMethod(Supplier<String> s) { s.get()+" Boo"; }

Aber diese Abstraktion und dieses Generisch-Machen und Funktional-Machen kann man beliebig(!) weit treiben. Suggestiv: Dort, wo diese Methode aufgerufen wird, könnte man sie auch einfach als Function<String,String> bekanntmachen, und den Rückgabewert nur irgendeinem Consumer übergeben… Das ganz konsequent immer weitergedacht bedeutet, dass man irgendwann nur noch anonyme functions, suppliers, consumers, sets, processors und predicates rumreicht, und der Code ähnlich viel Struktur hat, wie ein LISP-Programm. Es gibt auch Gründe dafür, warum OOP so erfolgreich ist, und warum Klassen und Methoden Namen haben. Komplexe Konzepte benennen zu können ist wichtig, um die Komplexität handhabbar zu halten, und „noch seinen Kopf drumwickeln zu können“. Ja, das hat auch mit (Übung und) Gewohnheiten zu tun, aber … das driftet jetzt schon recht weit vom Thema ab…

Hallo Marco,

Beim Thema der funktionalen Programmierung habe ich im Einzelnen andere Sichtweisen als Du. Das von Dir befürchtete Codechaos sehe ich nicht als Risiko. Im Gegenteil trägt radikale Abstraktion zur besseren Lesbarkeit bei. Aber das gehört in eine andere Diskussion. Lassen wir das an dieser Stelle.

Abgesehen von dem Punkt, den ich schon erwähnt hatte (Nur ein Comparator reicht nicht: Man MUSS sich auch um’s equals kümmern - sonst kracht’s!)

Nur der Vollständigkeit halber, nicht als weiteres Argument für Comparatoren: Im Fall des Insertierens in eine Tree-Struktur reicht der Comparator. Schau Dir den Quellcode von TreeMap an. dort wird niemals equals auf den Keys aufgerufen. Es wird immer der Comparator konsultiert. Es „kracht“ hier also nicht. Es „kracht“ nur, wenn Du den Tree neben eine andere Struktur legst, die equals verwendet und Dich dann über unterschiedliche Ergebnisse wunderst.

Das würde nur funktionieren, wenn man von einer Klasse erben und dabei die Typparameter der Elternklasse festtackern würde (also sowas wie MyMap implements Map<String,Integer>).

Habe meinen alten Code rausgekramt. Du hast Recht, bei GenericDaos tackert man den Typen fest. Das hatte ich nicht bedacht.

Mir hat die elementClass aber dennoch keine Ruhe gelassen, denn sie fühlte sich so „falsch“ an. Und tatsächlich ist sie unnötig. Konzeptionell soll dein BiPredicate ja die Standard equals-Methode ersetzen. Diese nimmt Object entgegen. Also sollte das BiPredicate dies auch tun. Der generische Typ ist also abgewandelt zu BiPredicate<? super T, Object>. Dieses kannst Du füttern, ohne den zweiten Parameter casten zu müssen. Auch von der Funktionalität her ist der Ansatz besser. Bei einer echten Freiheit, equals zu definieren, müssen nämlich auch inkompatible Typen äquivalent sein dürfen. Das war bei Deiner Implementierung bisher ausgeschlossen. Mit der neuen Version werden alle Entscheidungen dem Implementierer des BiPredicate überlassen. Nachfolgend der geänderte Code für’s WrappingSet.

import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.ToIntFunction;

/**
 * Implementation of the <code>Set</code> interface that wraps the elements in
 * order to allow an external, functional definition of <code>equals</code> and
 * <code>hashCode</code>.
 *
 * @param <T>
 *            The element type
 */
public class WrappingSet<T> extends AbstractSet<T> implements Set<T> {
	// Note: This class extends AbstractSet, although most functions that are
	// inherited from AbstractSet are overridden for performance reasons.
	// The functions from AbstractSet that are still used are toString,
	// equals, hashCode, addAll, containsAll, toArray, retainAll, removeAll

	/**
	 * The class that is wrapped around an object and uses the user-defined
	 * <code>equals</code> and <code>hashCode</code> functions to compute
	 * equality and hash codes of the objects.
	 */
	private class Wrapper {
		/**
		 * The actual object
		 */
		private final T element;

		/**
		 * Creates a new wrapper for the given object
		 * 
		 * @param object
		 *            The object
		 */
		Wrapper(T object) {
			this.element = object;
		}

		@Override
		public int hashCode() {
			return hashCode.applyAsInt(element);
		}

		@Override
		public boolean equals(Object object) {
			if (object == null) {
				return false;
			}
			if (object == this) {
				return true;
			}
			if (!(object instanceof WrappingSet.Wrapper)) {
				return false;
			}
			WrappingSet<?>.Wrapper other = (WrappingSet<?>.Wrapper) object;
			return equals.test(element, other.element);
		}

	}

	/**
	 * The predicate that determines the equality of elements
	 */
	private final BiPredicate<? super T, Object> equals;

	/**
	 * 
	 * The function that computes hash codes for elements
	 */
	private final ToIntFunction<? super T> hashCode;

	/**
	 * 
	 * The delegate set that stores the wrapped elements
	 */
	private final Set<Wrapper> delegateSet;

	/**
	 * Default constructor, using the default <code>equals</code> and
	 * 
	 * <code>hashCode</code> functions
	 * 
	 * @param elementClass
	 *            The type of the elements in this set
	 */
	public WrappingSet() {
		this(Objects::equals, Objects::hashCode);
	}

	/**
	 * 
	 * Creates a new wrapping set that uses the given functions to determine the
	 * equality and hash codes of elements
	 * 
	 * @param elementClass
	 *            The type of the elements in this set
	 * 
	 * @param equals
	 *            The predicate that determines the equality of elements
	 * 
	 * @param hashCode
	 *            The function that computes hash codes of elements
	 */
	public WrappingSet(BiPredicate<? super T, Object> equals,
			ToIntFunction<? super T> hashCode) {
		this.equals = equals;
		this.hashCode = hashCode;
		this.delegateSet = new LinkedHashSet<Wrapper>();

	}

	/**
	 * Wraps the given object into a {@link Wrapper}
	 * 
	 * @param object
	 *            The object
	 * 
	 * @return The {@link Wrapper}
	 * 
	 * @throws ClassCastException
	 *             If the given object can not be cast to <code>T</code>
	 */
	private Wrapper wrap(Object object) {
		@SuppressWarnings("unchecked")
		Wrapper wrapper = new Wrapper((T) object);
		return wrapper;
	}

	@Override
	public int size() {
		return delegateSet.size();
	}

	@Override
	public boolean isEmpty() {
		return delegateSet.isEmpty();
	}

	@Override
	public boolean contains(Object object) {
		return delegateSet.contains(wrap(object));
	}

	@Override
	public Iterator<T> iterator() {
		Iterator<Wrapper> delegateIterator = delegateSet.iterator();
		return new Iterator<T>() {
			@Override
			public boolean hasNext() {
				return delegateIterator.hasNext();
			}

			@Override
			public T next() {
				Wrapper wrapper = delegateIterator.next();
				return wrapper.element;
			}

			@Override
			public void remove() {
				delegateIterator.remove();
			}
		};

	}

	@Override
	public boolean add(T element) {
		return delegateSet.add(wrap(element));
	}

	@Override
	public boolean remove(Object object) {
		return delegateSet.remove(wrap(object));
	}

	@Override
	public void clear() {
		delegateSet.clear();
	}

}

Wenn Du bei Deinem Konzept der „freien Wahl von Gleichheit“ damit leben könntest, dass feststeht, null und null sind gleich und null und nicht null verschieden, könnte ich Dir auch noch eine Version bauen, die mit einem Predicate auskommt, falls Dich das interessiert.

Das ist („zufällig, in der aktuellen Implementierung“) bei TreeSet so. Aber das dann an den Tag gelegte Verhalten deckt sich nur mit dem, was im Set-Interface spezifiziert ist, wenn der Vergleich „consistent with equals“ ist.

[QUOTE=nillehammer;121795]
Mir hat die elementClass aber dennoch keine Ruhe gelassen, denn sie fühlte sich so „falsch“ an. Und tatsächlich ist sie unnötig. Konzeptionell soll dein BiPredicate ja die Standard equals-Methode ersetzen. Diese nimmt Object entgegen. [/QUOTE]

Joa, manchmal sieht man den Wald vor lauter Bäumen nicht. Das stimmt natürlich.

so viel Text, nicht alles gelesen,
zwei Punkte anzumerken:

wie schon genannt und bekannt gehören ja equals und hashCode ziemlich zusammen,
allgemein vielleicht auch mal nicht, da bin ich für Freiheit der Methoden,

aber hier für Maps wiederum essentiell dass an beides zusammenpassend gedacht wird,
da hat man die Macht der Objektiorientierung, ein Interface mit beiden Methoden zu verlangen, gute Hilfe für den Anwender

wie wäre es mit nur einem Interface welches beides, evtl. optional auch gleich compare, kann?
bisher schlicht eingespart, weil es BiPredicate & Co. schon gibt,
oder nur für Java 8-Funktionen getrennt geschrieben, bedenklich, oder andere gute Gründe für Verzicht?


würdest du noch die Funktionalität null → Hash 0 sowie null + null → equals true als Standard festlegen und automatisch einbauen,
dann wären alle zu übergebenen Methoden deutlich einfacher, kommt das in Frage?

falls es variabel bleiben soll, dann zumindest Adapter für das (oder die?) Interface anbieten,
kommt Java 8-Notation vielleicht wieder etwas in die Quere, zwischen allgemeinen Interface und dem Adapter zu unterscheiden,
das ist einfach in allem unhandlich :wink:

Hmja, das ist ja das was in der (im ersten Post verlinkten) „HashingStrategy“ von Trove gemacht wird. Ich fand es halt nett, diese Funktionen in Form von schon existierenden, funktionalen interfaces dort reingeben zu können. Gegenargumente, das zu kombinieren, wären ggf. „Vielleicht will man mal irgendwo NUR equals verwenden (z.B. in einer List)“, oder eben einfach Interface-Segregation-Prinzip – Wikipedia :wink: . Aber spätestens bei einem „compare“ würde ich widersprechen: Das geht eben oft einfach nicht, d.h. „es passt nicht dazu“. Hybrid-Lösungen wären natürlich auch denkbar, nahe liegend erscheint das (ähnlich wie das ja auch in der Mathematik gemacht wird) das Kombinatorisch aufzubauen:

interface EqualityTester { ... }
interface Hasher extends EqualityTester { ... }
interface Comparer extends EqualityTester { ... }

[QUOTE=SlaterB;121856]
würdest du noch die Funktionalität null → Hash 0 sowie null + null → equals true als Standard festlegen und automatisch einbauen,
dann wären alle zu übergebenen Methoden deutlich einfacher, kommt das in Frage?

falls es variabel bleiben soll, dann zumindest Adapter für das (oder die?) Interface anbieten,
kommt Java 8-Notation vielleicht wieder etwas in die Quere, zwischen allgemeinen Interface und dem Adapter zu unterscheiden,
das ist einfach in allem unhandlich ;)[/QUOTE]

Du meinst den Test auf „null“ um den eigentlichen Aufruf der Hash-Funktion „drumzuwickeln“, damit man da DRIN nicht nochmal auf „!=null“ testen muss? Das ist (wie auch einige andere Punkte) schon fast ein „Detail“, aber könnte eine Option sein. Die Klassen in der aktuellen Form waren ein „first shot“, um mal die allgemeinen Gedanken dazu zu hören. Bevor das (und sei es nur als nicht-public-Klasse in irgendeiner Lib) auf einem public SCM landet, ist noch einiges zu tun.

was schon so gut ist, kann eben nur noch in Details verbessert werden :wink:

Der dahinter zu vermutende Sarkasmus ist der Hauptgrund für diesen Thread: Ist das (deiner (unwichtigen :stuck_out_tongue_winking_eye: ) Meinung nach) insgesamt kompletter Unfug? :confused:

war jetzt eigentlich mehr Schleimen als Sarkasmus,
und auch das nur geschrieben um noch irgendwie kurz zurückzumelden

Unfug kann das nun wirklich nicht sein, klar definierte Funktionalität und auch umgesetzt,
über Art oder ‘Details’ kann man noch streiten, aber geht ja bereits


um noch was zu schreiben ist die auch denkbare Alternative oft ein individueller Wrapper um etwa Map herum,
für String-Keys mit ignoreCase habe ich das kürzlich gemacht, beim Einfügen Key als toLowerCase() ablegen,

beim Suchen könnte man nur mit toLowerCase() suchen,
oder als kleine Steigerung wenn mit toLowerCase() gefunden dann auch unter der normalen Schreibweise des aktuellen Such-Keys neu ablegen,
damit nächstes Mal direkt gefunden, toLowerCase() gespart

Hmja. Das bringt mich auf einen Gedanken, den ich (nicht mehr heute, aber) nochmal weiterdenken muss: Ob man diese Wrapping-Funktionalität nicht als solche rausziehen könnte - ganz grob im Sinn einer Function<Key, InternalKey> die man anbietet. Damit könnte man (mit einer geigneten Implementierung dieser WrapperFunction) das emulieren, was jetzt mit den equals/hashCode-Funktionen gemacht wird, oder z.B. eben einfach s -> s.toLowerCase() verwenden. Spontan würde ich sagen, dass das so erstmal KEINEN Sinn macht, weil man nicht weiß, was im “keySet” sein sollte, aber… in dieser Gegend kann man nochmal weiterdenken.

Das ist bei deiner Methode doch eh schon im Eimer:

dieMap.containsKey

dieMap.keySet (du lieferst ja eine mehr oder weniger zufällige Menge der als keys verwendeten Objekte via wrapper.element zurück?)

passen doch gar nicht mehr zusammen…