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?