Пользовательский хэш-код / ​​равнозначная операция для HashMap

Существует ли реализация класса HashMap (или интерфейса Map), которая позволит мне использовать альтернативный хэш-код и операции равенства... По аналогии с тем, как коллекции одного и того же типа могут быть отсортированы несколькими способами с помощью Comparator в Collections.sort (список, компаратор).

Я хотел бы по возможности избежать создания оболочки ключей, обеспечивающей желаемый хэш-код и операции равенства.


В моем случае один из сценариев, почему мне нужно что-то вроде этого:

В моем веб-приложении для каждого запроса я загружаю Местоположение / Интернет-провайдера и другие данные. В разных частях кода (на уровне сервисов и репозитория) я "минимизировал" кеши, соответствующие его требованиям.

Вот упрощенный пример кода:

class GeoIpData{
    private String countryName;
    private String state;
    private String city;
    private String isp;
    @Override
    public int hashCode() {
        //countryName hashCode
        //state hashCode
        //city hashCode
        //isp hashCode
    }
    @Override
    public boolean equals(Object obj) {
        // compare countryName
        // compare state
        // compare city
        // compare isp
    }
}

 Map<GeoIpData,#Type1> fullCache = ... //This cache needs to be unique per countryName,state,city and isp
 Map<GeoIpData,#Type2> countryCache = ... //This cache needs to be unique per countryName
 Map<GeoIpData,#Type2> ispCache = ... //This cache needs to be unique per countryName,isp

Для достижения этих 3 карт требуется 3 различных метода хэш-кода и метода равно.

fullCache:
hashCode -> GeoIpData.hashCode();
equals   -> GeoIpData.equals(Object obj);

countryCache:
hashCode -> {countryName hashCode }
equals   -> {compare countryName }

ispCache:
hashCode -> {countryName hashCode & isp hashCode }
equals   -> {compare countryName & compare isp hashCode }

3 ответа

Решение

GNU Trove позволяет вам предоставлять конкретную TObjectHashingStrategy с вашим собственным хешем и равнозначными функциями для TCustomHashMap.

Начальный Java API ограничен equals / hashCode поведение типа (класса), потому что в те дни у них не было лямбда-выражений.

Чтобы повторно использовать существующие API / реализации - сделайте эфемерные ключи:

public CountryKey {
    private String country;
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof CountryKey)) { return false; }
        CountryKey that = (CountryKey) obj;
        return this.country.equals(that.country);
    }
    @Override int hashCode() {
         return country.hashCode();
    }
}

или обертки:

public CountryKey {
    private GeoIpData holder;
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof CountryKey)) { return false; }
        CountryKey that = (CountryKey) obj;
        return this.holder.getCountry().equals(that.holder.getCountry());
    }
    @Override int hashCode() {
         return holder.getCountry().hashCode();
    }
}

и напишите вспомогательные конструкторы для ключей:

public class GeoIpData {
     public CountryKey buildCountryKey() {
          return new CountryKey(this.country);
     }
}

Существует довольно много непараллельных сторонних хэш-карт, некоторые из которых поддерживают пользовательские равенства и хеш-код, как тот, который упомянут в ответе Михаила. Однако я не знаю ни одной параллельной реализации.

Вот что-то вроде хака, который позволяет вам использовать стандартный ConcurrentHashMap с собственными реализациями Equals/HashCode. Он использует локальную переменную потока для передачи того, какие ключи реализации Equals/hashCode следует использовать в любой заданный момент времени.

Сначала декоратор, добавляющий функциональность любомувыполнение:

      public class CustomEqualsAndHashCodeConcurrentMapDecorator<K, V>
        implements ConcurrentMap<K, V> {

    public interface EqualsAndHashCode<X> {
        boolean equals(X x, X y);
        int hashCode(X x);
    }
    private final ConcurrentMap<K, V> decoratee;
    private final EqualsAndHashCode<K> equalsAndHashCode;
    private final Consumer<EqualsAndHashCode<K>> equalsAndHashCodeSetter;

    public CustomEqualsAndHashCodeConcurrentMapDecorator(ConcurrentMap<K, V> decoratee, EqualsAndHashCode<K> equalsAndHashCode, Consumer<EqualsAndHashCode<K>> equalsAndHashCodeSetter) {
        this.decoratee = decoratee;
        this.equalsAndHashCode = equalsAndHashCode;
        this.equalsAndHashCodeSetter = equalsAndHashCodeSetter;
    }

    @Override
    public V get(Object key) {
        equalsAndHashCodeSetter.accept(equalsAndHashCode);
        final V ret = decoratee.get(key);
        equalsAndHashCodeSetter.accept(null);
        return ret;
    }

    public Set<K> keySet() {
// Note that this is potentially dangerous, as the caller will have to make sure
// equalsAndHashCodeSetter.accept() is called before/after calling some of the methods of the returned set
        return decoratee.keySet();
    }

    public Set<Entry<K, V>> entrySet() {
// Note that this is potentially dangerous, as the caller will have to make sure
// equalsAndHashCodeSetter.accept() is called before/after calling some of the methods of the returned set
        return decoratee.entrySet();
    }

    // omitting the other methods, most of which need to be
    // wrapped in equalsAndHashCodeSetter.accept() calls as well
}

Вот пример того, как использовать одни и те же экземпляры ключевых объектов а) со значением по умолчанию равного/хэш-кода и б) с пользовательским:

      class CustomEqualsAndHashCodeConcurrentMapDecoratorTest {

    private static abstract class DynamicEqualsAndHashCodeObject<K> {
        @SuppressWarnings("rawtypes")
        private static final ThreadLocal<CustomEqualsAndHashCodeConcurrentMapDecorator.EqualsAndHashCode> equalsAndHashCode = new ThreadLocal<>();

        static void setEqualsAndHashCode(@SuppressWarnings("rawtypes") CustomEqualsAndHashCodeConcurrentMapDecorator.EqualsAndHashCode equalsAndHashCode) {
            DynamicEqualsAndHashCodeObject.equalsAndHashCode.set(equalsAndHashCode);
        }

        private CustomEqualsAndHashCodeConcurrentMapDecorator.EqualsAndHashCode<K> getComparator() {
            //noinspection rawtypes
            final CustomEqualsAndHashCodeConcurrentMapDecorator.EqualsAndHashCode raw = DynamicEqualsAndHashCodeObject.equalsAndHashCode.get();
            //noinspection unchecked
            return raw == null ? null : (CustomEqualsAndHashCodeConcurrentMapDecorator.EqualsAndHashCode<K>) DynamicEqualsAndHashCodeObject.equalsAndHashCode.get();
        }

        @Override
        public boolean equals(Object obj) {
            var equalsAndHashCode = getComparator();
            //noinspection unchecked
            return equalsAndHashCode != null ? equalsAndHashCode.equals(dis(), (K) obj) : super.equals(obj);
        }

        @Override
        public int hashCode() {
            var equalsAndHashCode = getComparator();
            return equalsAndHashCode != null ? equalsAndHashCode.hashCode(dis()) : super.hashCode();
        }

        abstract K dis();
    }

    static class Key extends DynamicEqualsAndHashCodeObject<Key> {
        final String a;
        final String b;

        public Key(String a, String b) {
            this.a = a;
            this.b = b;
        }

        @Override
        public String toString() {
            return a + b;
        }

        @Override
        Key dis() {
            return this;
        }

        static class AEqualsAndHashCode implements CustomEqualsAndHashCodeConcurrentMapDecorator.EqualsAndHashCode<Key> {
            public boolean equals(Key x, Key y) {return Objects.equals(x.a, y.a);}
            public int hashCode(Key key) {return Objects.hashCode(key.a);}
        }
    }

    @Test
    void test() {
        var key11 = new Key("1", "1");
        var key12 = new Key("1", "2");
        var key21 = new Key("2", "1");
        var key22 = new Key("2", "2");
        var mapDefault = new ConcurrentHashMap<Key, String>();
        mapDefault.put(key11, key11.toString());
        mapDefault.put(key12, key12.toString());
        mapDefault.put(key21, key21.toString());
        mapDefault.put(key22, key22.toString());
        mapDefault.forEach((k, v) -> System.out.println("mapDefault: " + k + " -> " + v));
        var mapA = new CustomEqualsAndHashCodeConcurrentMapDecorator<>(new ConcurrentHashMap<Key, String>(), new Key.AEqualsAndHashCode(), Key::setEqualsAndHashCode);
        mapA.put(key11, key11.toString());
        mapA.put(key12, key12.toString());
        mapA.put(key21, key21.toString());
        mapA.put(key22, key22.toString());
        mapA.forEach((k, v) -> System.out.println("mapA: " + k + " -> " + v));
    }
}

Результат таков:

      mapDefault: 12 -> 12
mapDefault: 11 -> 11
mapDefault: 21 -> 21
mapDefault: 22 -> 22
mapA: 11 -> 12
mapA: 21 -> 22
Другие вопросы по тегам