Почему я получаю дубликаты ключей в Java HashMap?

Кажется, я получаю дубликаты ключей в стандартном Java HashMap. Под "duplicate" я подразумеваю, что ключи равны их методу equals(). Вот проблематичный код:

import java.util.Map;
import java.util.HashMap;

public class User {
    private String userId;
    public User(String userId) { 
        this.userId = userId;
    }
    public boolean equals(User other) {
        return userId.equals(other.getUserId());
    }
    public int hashCode() {
        return userId.hashCode();
    }
    public String toString() {
        return userId;
    }

    public static void main(String[] args) {
        User arvo1 = new User("Arvo-Part");
        User arvo2 = new User("Arvo-Part");
        Map<User,Integer> map = new HashMap<User,Integer>();
        map.put(arvo1,1);
        map.put(arvo2,2);

        System.out.println("arvo1.equals(arvo2): " + arvo1.equals(arvo2));
        System.out.println("map: " + map.toString());
        System.out.println("arvo1 hash: " + arvo1.hashCode());
        System.out.println("arvo2 hash: " + arvo2.hashCode());
        System.out.println("map.get(arvo1): " + map.get(arvo1));
        System.out.println("map.get(arvo2): " + map.get(arvo2));
        System.out.println("map.get(arvo2): " + map.get(arvo2));
        System.out.println("map.get(arvo1): " + map.get(arvo1));
    }
}

И вот результат:

arvo1.equals(arvo2): true
map: {Arvo-Part=1, Arvo-Part=2}
arvo1 hash: 164585782
arvo2 hash: 164585782
map.get(arvo1): 1
map.get(arvo2): 2
map.get(arvo2): 2
map.get(arvo1): 1

Как вы можете видеть, метод equals() для двух объектов User возвращает true, а их хэш-коды одинаковы, но каждый из них образует отдельный key в map. Кроме того, map продолжает различать две клавиши User в последних четырех вызовах get().

Это прямо противоречит документации:

Более формально, если эта карта содержит отображение из ключа k в значение v, такое что (ключ == null? k == null: key.equals(k)), то этот метод возвращает v; в противном случае он возвращает null. (Это может быть не более одного такого отображения.)

Это ошибка? Я что-то упустил? Я запускаю Java версию 1.8.0_92, которую я установил через Homebrew.

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

Ответы

Ответ 1

Проблема заключается в вашем методе equals(). Подпись Object.equals() равна equals(OBJECT), но в вашем случае это equals(USER), поэтому это два совершенно разных метода, и хешмап вызывает тот, у которого есть параметр Object. Вы можете проверить, что, помещая аннотацию @Override над вашими равными - это приведет к ошибке компилятора.

Метод equals должен быть:

  @Override
  public boolean equals(Object other) {
    if(other instanceof User){
        User user = (User) other;
        return userId.equals(user.userId);
    }

    return false;
}

Как наилучшая практика, вы всегда должны ставить @Override на методы, которые вы переопределяете - это может сэкономить вам много неприятностей.

Ответ 2

Ваш метод equals не переопределяет equals, а типы в Map стираются во время выполнения, поэтому метод фактического равенства равен equals(Object). Ваши равные должны выглядеть примерно так:

@Override
public boolean equals(Object other) {
    if (!(other instanceof User))
        return false;
    User u = (User)other;
    return userId.equals(u.userId);
}

Ответ 3

ОК, поэтому, прежде всего, код не компилируется. Отсутствует этот метод:

other.getUserId()

Но помимо этого вам понадобится метод @Override equals, IDE, например Eclipse, также может помочь генерировать equals и hashCode btw.

@Override
public boolean equals(Object obj)
{
  if(this == obj)
     return true;
  if(obj == null)
     return false;
  if(getClass() != obj.getClass())
     return false;
  User other = (User) obj;
  if(userId == null)
  {
     if(other.userId != null)
        return false;
  }
  else if(!userId.equals(other.userId))
     return false;
  return true;
}

Ответ 4

Как и другие, у вас возникла проблема с сигнатурой метода equals. В соответствии с наилучшей практикой Java, вы должны реализовать равные значения:

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    User user = (User) o;

    return userId.equals(user.userId);
  }

То же самое относится к методу hashCode(). см. Переопределение метода equals() и hashCode() в Java

Вторая проблема

у вас больше нет дубликатов, но у вас есть новая проблема, ваш HashMap содержит только один элемент:

map: {Arvo-Part=2}

Это связано с тем, что оба объекта User ссылаются на одну и ту же строку (JVM String Interning), а с точки зрения HashMap ваши два объекта одинаковы, поскольку оба объекта эквивалентны в методах hashcode и equals. поэтому, когда вы добавляете второй объект в HashMap, вы переопределяете свой первый. чтобы избежать этой проблемы, убедитесь, что вы используете уникальный идентификатор для каждого пользователя

Простая демонстрация ваших пользователей:

введите описание изображения здесь

Ответ 5

Как предложил Хриллис, добавив @Override к hashCode и equals, вы получите ошибку компиляции, потому что подпись метода equals равна public boolean equals(Object other), поэтому вы фактически не переопределяете значение по умолчанию (из класса Object) равен методу. Это приводит к тому, что оба пользователя попадают в один и тот же ковш внутри hashMap (hashCode переопределяется, и оба пользователя имеют одинаковый хеш-код), но при проверке на равенство они различаются, поскольку используется метод по умолчанию по умолчанию, что означает что адреса памяти сравниваются.

Для получения ожидаемого результата замените метод equals следующим образом:

@Override
public boolean equals(Object other) {
    return getUserId().equals(((User)other).getUserId());
}

Ответ 6

Ключи вместе со своими ассоциативными значениями хранятся в связанном списке node в ковше, и ключи по существу сравниваются в hashmap, используя метод equals(), а не по hashcode. Если arvo1.equals(arvo2) возвращает true arvo2, будет возвращено значение arvo1 и arvo2 (2), если arvo1.equals(arvo2) возвращает false, другой node будет создан в списке ведра, поэтому, когда вы вызываете get (arvo2 ) вы получите 2, так как arvo1.equals(arvo2) является ложным.