2015-02-09 3 views
6

Позвольте сказать, что у меня есть класс Car с цветом и моделью полей. Мне нужно хранить автомобили в коллекции, в которой у меня не будет дубликатов (нет двух одинаковых автомобилей). В приведенном ниже примере я использую HashMap.Переопределение hashCode() должно быть согласовано с equals(), когда equals() использует показатель подобия

Согласно документации на Java, если у нас есть 2 Автомобильных объекта car1 и car2, таких как car1.equals(car2) == true, тогда он также должен содержать это car1.hashCode() == car2.hashCode(). Поэтому в этом примере, если бы я хотел сравнить автомобили только по их цвету, тогда я бы использовал только поле цвета в equals() и hashCode(), так как я сделал это в своем коде, и он отлично работает.

public class Car { 
String color; 
String model; 

@Override 
public int hashCode() { 
    final int prime = 31; 
    int result = 1; 
    result = prime * result + ((color == null) ? 0 : color.hashCode()); 
    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; 
    return true; 
} 

public Car(String color, String model) { 
    super(); 
    this.color = color; 
    this.model = model; 
} 

@Override 
public String toString() { 
    return color + "\t" + model; 
} 

public static void main(String[] args) { 
    Map<Car, Car> cars = new HashMap<Car, Car>(); 
    Car a = new Car("red", "audi"); 
    Car b = new Car("red", "bmw"); 
    Car c = new Car("blue", "audi"); 
    cars.put(a, a); 
    cars.put(b, b); 
    cars.put(c, c); 
    for(Car car : cars.keySet()) { 
     System.out.println(cars.get(car)); 
    } 

} 

}

Выход:

  • красный БМВ
  • синяя ауди

, как и ожидалось.

Так хорошо пока. Теперь я экспериментирую с другими способами сравнения двух автомобилей. Я предоставил функцию для измерения сходства между двумя автомобилями. Для аргумента позвольте сказать, что у меня есть метод double similarity(Car car1, Car car2), который возвращает двойное значение в интервале [0,1]. Я считаю, что 2 машины должны быть равны, если их функция подобия возвращает значение больше 0,5. Затем я переопределить метод Equals:

@Override 
public boolean equals(Object obj) { 
    Car other = (Car) obj; 
    return similarity(this, other) > 0.5; 
} 

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

Я думал использовать TreeMap вместо HashMap, чтобы избежать переопределения hashCode, потому что я понятия не имею, как это сделать должным образом. Но мне не нужна сортировка, поэтому я считаю, что использование TreeMap в этой проблеме не подходит, и я думаю, что это будет дороже с точки зрения сложности.

Было бы очень полезно, если бы вы могли мне предложить: способ переопределить hashCode или альтернативу другой структуры, которая была бы более подходящей для моей проблемы.

Спасибо заранее!

+1

Что определяет 'сходство()> 0,5'? Как только мы это узнаем, мы можем построить новый 'hashCode()' – JLewkovich

+0

@J. Это была фактически упрощенная версия, потому что реальный проект, над которым я работаю, более сложный. Для этой проблемы это может иметь смысл, но, с технической точки зрения, пусть функция подобия определяет сходство строк между цветами. Например, если 2 автомобиля имеют цвета «синий» и «голубой», чем возврат значения более 0,5, но если цвета были «синими» и «красными», он возвращал 0. – giliev

+0

Как ваш метод «равно» будет нарушать общий контракт для 'equals', в глубине души этот вопрос является дубликатом http://stackoverflow.com/questions/27581/what-issues-should-be-considered-when-overriding-equals-and-hashcode- in-java – Raedwald

ответ

3

Здесь есть несколько моментов.

Во-первых, это необычное использование equals. Обычно equals интерпретируется как означающий, что это два экземпляра одного и того же объекта; можно заменить другого без удара.

Второй момент состоит в том, что a.equals(b) подразумевает, что a.hashCode() == b.hashCode(), но не наоборот. На самом деле совершенно законно (хотя и бессмысленно), чтобы все объекты возвращали один и тот же хэш-код. Поэтому в вашем случае, если все достаточно similar автомобилей возвращают один и тот же хэш-код, различные коллекции будут работать правильно.

Я подозреваю, что более вероятно, что у вас должен быть отдельный класс для представления вашей «аналогичной» концепции. Затем вы можете проверить равенство сходства или карту, аналогичную спискам автомобилей.Это может быть лучшим представлением концепции, чем перегрузка equals для автомобилей.

3

hashCode() - всего лишь «короткий разрез» для equals(). Важно убедиться, что схема, в которой вы работаете, имеет смысл для equals. Рассмотрим автомобили a, b и c, где similarity(a, b) == 0.3 и similarity(b, c) == 0.3.

А что, если similarity(a, c) == 0.6? Тогда вы находитесь в ситуации, когда a.equals(b) и b.equals(c), но таинственно a.equals(c) является ложным.

Полное наружное транспортное имущество Object.equals(). Когда это произойдет, части стандартной библиотеки, такие как HashMap и TreeMap, вдруг начнут вести себя очень странно.

Если вы заинтересованы в подключении к различным схемам сортировки, вам гораздо лучше работать с разными Comparator<Car> s, которые каждый реализует вашу схему. Хотя такое же ограничение применяется в API Comparator , оно позволяет вам представлять меньше и больше, чем это звучит, как будто вы действительно после этого, и которое невозможно сделать с помощью Object.equals().

[1] Если compare(a,b) == compare(b,c) == 0, то compare(a,c) также должен быть 0.

+1

Интересно. Если у меня есть «похожее на b», «b похожее на c» и «a не похоже на c», то в зависимости от того, в каком порядке выполняется вставка, я могу получить в конце a и c в наборе (если я делаю вставку в порядке a, b, c) или только b, если я сначала вставлю b, а затем a и c. Японял твою точку зрения. Тем не менее, я думаю, что Comparator не поможет мне с этой проблемой, за исключением того, что я бы избегал переопределения hashCode(). В конце концов, я должен, вероятно, провести некоторое тестирование по моей проблеме, чтобы увидеть, повлияет ли эта проблема на мое решение. Благодаря! – giliev

4

Хотя спринтер покрыл некоторые проблемы вашей стратегией, с вашим методом возникает проблема с контрактом. Согласно Javadoc,

[equals] транзитивно: для любых ненулевых значений ссылок х, у и г, если x.equals (у) возвращает истину и y.equals (г) возвращает истину , то x.equals (г) должен возвращать истинный

Однако x может быть похож на y и y может быть похож на z с й слишком далеко от z быть похожи, так что ваш метод equals Безразлично» т работы.

+0

Несмотря на то, что договорные аспекты равных являются важными, в этом случае лучше всего отстранить ОП от взлома методов равных и хэш-кодов, как описано в сообщении. Это не хороший дизайн. –

4

Вы не должны нарушать методы equals и hashcode таким образом. Структуры данных Collection зависят от этих методов, и использование их в нестандартном режиме приведет к неожиданному поведению.

Я предлагаю вам создать реализацию Comparator, которая будет сравнивать две машины или реализовать интерфейс Comparable, где вы можете использовать метод similarity внизу.

+0

Спасибо за предложение! Транзитивность равных (упомянутых в других ответах) по-прежнему будет проблемой, но я думаю, что мое решение не будет сильно затронуто, поэтому, я думаю, я попробую это решение на базе компаратора. В конце концов, это не повлияет на какой-либо другой фрагмент моего кода. – giliev

+0

Если вы не знаете, как реализовать методы equals и hashcode, большинство IDE могут автоматически генерировать эти методы для вашего класса. Eclipse и Intellij могут автоматически генерировать их для вас. –

+0

Транзитивность - это проблема, когда вы имеете дело с расширяемыми классами. Это когда вы имеете дело с иерархией наследования. Если вы этого не сделаете, то обычная реализация equals() и hashcode() будет в порядке для вас. Прочтите эту статью для получения дополнительной информации: http://www.artima.com/lejava/articles/equality.html –

0

Основываясь на моем понимании вашего метода similarity(), я думаю, что это может быть лучше держать hashCode() функцию примерно то же самое, но вместо того, чтобы использовать color.hashCode(), создать вспомогательный метод, который будет генерировать «подобный цвет», и использование, что хэш-код:

public int getSimilarColor(String color) { 
    if(color == "blue" || color == "light blue" || color == "dark blue" /* add more blue colors*/) { 
     return "blue"; 
    } else if(color == "red" || color == "light red" || color == "dark red" /* add more red colors*/) { 
     return "red"; 
    } 
    /* 
    else if(yellow...) 
    else if(etc...) 
    */ 
    else { 
     return color; 
    } 
} 

И затем использовать его в методе Hashcode:

@Override 
public int hashCode() { 
    final int prime = 31; 
    int result = 1; 
    result = prime * result + ((color == null) ? 0 : getSimilarColor(color).hashCode()); 
    return result; 
} 

Этот вспомогательный метод также может быть полезным в similarity().Если вам не нравятся жестко подобранные цвета в вашем методе, вы можете использовать некоторые другие средства для их создания, например, для сопоставления с образцом.

+0

Спасибо за советы! Однако мой список не будет конечным, поэтому я должен попытаться найти более общий способ проверить равенство. – giliev

2

Как указано другими, ваша последняя реализация .equals() нарушает его договор. Вы просто не можете реализовать его таким образом. И если вы перестанете думать об этом, это имеет смысл, поскольку ваша реализация .equals() не предназначена для возврата true, когда два объекта на самом деле равны, но когда они аналогичны. Но достаточно аналогичный is не тот как равный, ни на Java, ни где-либо еще.

Проверить .equals() javadocs и вы увидите, что любой объект, который реализует его должен придерживаться своего контракта:

Равных метод реализует отношение эквивалентности ссылок непустых объекта:

  • это рефлексивно: для любого ненулевого опорного значения х, x.equals (х) должна возвращать верно.

  • Это симметрично: для любых ненулевых опорных значений x и y x.equals (y) должно возвращать истинное тогда и только тогда, когда y.equals (x) возвращает true.

  • Это транзитивно: для любых ненулевых опорных значений x, y и z, если x.equals (y) возвращает true, а y.equals (z) возвращает true, тогда x.equals (z) должно return true.

  • Это согласуется: для любых ненулевых опорных значений x и y несколько вызовов x.equals (y) последовательно возвращают true или последовательно возвращают false, если информация, используемая при равных сравнениях с объектами, не изменяется.

  • Для любого ненулевого опорного значения х, x.equals (NULL) должен возвращать ложь.

Ваша реализация .equals() не выполняет этот контракт:

  • В зависимости от вашей реализации double similarity(Car car1, Car car2), она не может быть симметричным
  • Это явно не транзитивно (хорошо объяснено в предыдущих ответах)
  • Возможно, это не соответствует:

Рассмотрим пример несколько иной, чем тот, который вы дали в комментарии:

«кобальта» будет равен «синий», а «красный» будет отличаться от «синего»

Если вы использовал некоторый внешний источник для вычисления сходства, например словаря, и если один день «кобальт» не был найден как запись, вы можете вернуть сходство около 0.0, поэтому автомобили не будут равны. Однако на следующий день вы поймете, что «кобальт» - особый вид «синего», поэтому вы добавляете его в словарь, и на этот раз, когда вы сравниваете одни и те же автомобили, сходство очень велико (или около 1.0), поэтому они равны. Это было бы несоответствием .Я не знаю, как работает функция сходства, но если это зависит от чего-либо другого, чем данные, содержащиеся в двух объектах, которые вы сравниваете, вы можете нарушать также ограничение консистенции .equals().

Что касается использования TreeMap<Car, Whatever>, я не вижу, как это может быть полезно. Из TreeMap javadocs:

... интерфейс Карты определяется в терминах операции РАВНО, но отсортированная карта выполняет все основные сравнения, используя его СотрагеТо (или сравнить) метод, поэтому два ключа, которые считаются равным этим метод, с точки зрения сортированной карты, равен.

Другими словами, в TreeMap<Car, Whatever> map, map.containsKey(car1) вернуться бы true тогда и только тогда car1.compareTo(car2) вернулся точно 0 для некоторого car2, который принадлежит map. Однако, если сравнение не вернулось 0, map.containsKey(car1) может вернуть false, несмотря на то, что car1 и car2 были очень похожи с точки зрения вашей функции подобия. Это связано с тем, что .compareTo() предназначен для использования , заказывая, а не для сходства.

Таким образом, ключевой момент заключается в том, что вы не можете использовать только Map в соответствии с вашим прецедентом, потому что это просто неправильная структура. На самом деле, вы не можете использовать какую-либо структуру Java, которая полагается на .hashCode() и .equals(), потому что вы никогда не сможете найти объект, соответствующий вашему ключу.


Теперь, если вы хотите, чтобы найти автомобиль, который является наиболее близким к данной машине с помощью вашей similarity() функции, я предлагаю вам использовать Guava's HashBasedTable structure построить таблицу коэффициентов подобия (или любой другой вам понравится ваше воображаемое имя) между каждой машиной вашего набора.

Этот подход необходимо будет Car реализовать .hashCode() и .equals() как обычно (т.е. не проверять только по цвету, и, конечно же, не прибегая к вашей similarity() функции). Например, вы можете проверить новый номер номерCar.

Идея заключается в том, чтобы иметь таблицу которая хранит сходство между каждого автомобиля, с его диагональ чистой, так как мы уже знаем, что автомобиль похож на себя (на самом деле, это равное к себе). Например, для следующих автомобилей:

Car a = new Car("red", "audi", "plate1"); 
Car b = new Car("red", "bmw", "plate2"); 
Car c = new Car("light red", "audi", "plate3"); 

таблица будет выглядеть следующим образом:

 a  b  c 

a ---- 0.60 0.95 

b 0.60 ---- 0.45 

c 0.95 0.45 ---- 

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

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

алгоритм, чтобы найти автомобиль, который является наиболее близким к данной машине можно было бы в общих чертах следующим образом:

  1. Получить строку данного автомобиля
  2. вернуть автомобиль, который наиболее близок к данной машине в возвращаемой строке, т.е. автомобиль строки с наибольшим коэффициентом подобия

Вот код, показывающий общие идеи:

public class SimilarityTest { 

    Table<Car, Car, Double> table; 

    void initialize(Car... cars) { 
     int size = cars.length - 1; // implicit null check 
     this.table = HashBasedTable.create(size, size); 
     for (Car rowCar : cars) { 
      for (Car columnCar : cars) { 
       if (!rowCar.equals(columnCar)) { // add only different cars 
        double similarity = this.similarity(rowCar, columnCar); 
        this.table.put(rowCar, columnCar, similarity); 
       } 
      } 
     } 
    } 

    double similarity(Car car1, Car car2) { 
     // Place your similarity calculation here 
    } 

    Car mostSimilar(Car car) { 
     Map<Car, Double> row = this.table.row(car); 
     Map.Entry mostSimilar = Maps.immutableEntry(car, Double.MIN_VALUE); 
     for (Map.Entry<Car, Double> entry : row.entrySet()) { 
      double mostSimilarCoefficient = mostSimilar.getValue(); 
      double currentCoefficient = entry.getValue(); 
      if (currentCoefficient > mostSimilarCoefficient) { 
       mostSimilar = entry; 
      } 
     } 
     return mostSimilar.getKey(); 
    } 

    public static void main(String... args) { 
     SimilarityTest test = new SimilarityTest(); 

     Car a = new Car("red", "audi", "plate1"); 
     Car b = new Car("red", "bmw", "plate2"); 
     Car c = new Car("light red", "audi", "plate3"); 

     test.initialize(a, b, c); 

     Car mostSimilarToA = test.mostSimilar(a); 
     System.out.println(mostSimilarToA); // should be c 

     Car mostSimilarToB = test.mostSimilar(b); 
     System.out.println(mostSimilarToB); // should be a 

     Car mostSimilarToC = test.mostSimilar(c); 
     System.out.println(mostSimilarToC); // should be a 
    } 
} 

Что касается сложности ... Инициализация таблицы занимает O (n2), при поиске наиболее сходного автомобиля занимает O (N). Я уверен, что это может быть улучшено, то есть зачем ставить автомобили в таблице, которые, как известно, не похожи друг на друга? (мы могли бы поставить только автомобили с коэффициентом подобия выше заданного порога), или вместо того, чтобы найти автомобиль с самым высоким коэффициентом подобия, мы могли бы остановить поиск, когда найдем автомобиль, коэффициент подобия которого выше другого заданного порога, и т.д.

Смежные вопросы