Метод arraylist contains () всегда возвращает false с настраиваемым объектом

У меня проблемы с продолжением кода, я приведу вам простой пример (хотя он будет немного сложнее, этот простой код тоже не работает должным образом).

class Sign {

  private String char;
  private Integer freq;

  public Sign(String c) {
  this.char = c; 
  }

  @Override
  public boolean equals(Object o) {

   String check = (String)o;
   return check.equals(this.char);
  }

  @Override
  public int hashCode() {

    int hash = 7;
    hash = 31 * hash + this.char.hashCode();
    return hash;
}

}

Я предполагаю, что для простоты всегда будет метод String in equals. Также есть hashCode (), чтобы убедиться, что метод contains () будет работать, и вот сам тест:

    ArrayList<Sign> queueOfSigns = new ArrayList<>();

    Sign test = new Sign("C");
    String c = "C";
    queueOfSigns.add(test);

    if(queueOfSigns.contains("C"))
        System.out.println("I am here!");

Несмотря ни на что, этот простой тестовый код в этом случае всегда возвращает false - поэтому сообщение «Я здесь» никогда не появляется. Я пробовал несколько разных способов подойти к моему коду, но это было потому, что идея этого состоит в том, чтобы получить отдельные символы из текста String и проверить, присутствует ли уже один символ в ArrayList. Тем не менее - без правильной работы этого простого теста я не могу двигаться дальше, поэтому я хотел бы спросить вас - что мне не хватает. На самом деле это мой первый раз, когда я использую методы equals () и hashCode (), чтобы мой собственный объект работал правильно с методом contains ().

Скорее всего, проблема в том, что new Sign("C").equals("C") не возвращает то же самое, что "C".equals(new Sign("C")). Для правильной работы equals должен быть симметричным: a.equals(b) == b.equals(a) всегда должен быть истинным.

Daniel Pryden 13.09.2018 18:06
1
1
2 154
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Ваша реализация equals неверна. equals имеет особый контракт; этот код пытается нарушить этот контракт. Из документации:

The equals method implements an equivalence relation on non-null object references:

  • It is reflexive: for any non-null reference value x, x.equals(x) should return true.
  • It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
  • It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • For any non-null reference value x, x.equals(null) should return false.

Невозможно преобразовать экземпляр класса Sign в строку.

Разумеется, возможный, чтобы сообщить объекту, что он равен объекту другого типа. Это просто не сработает правильно, если другой объект также не станет равным вашему объекту. Есть несколько мест, где вы можете изменить правила (например, в некоторых проектах использовались выделенные «объекты поиска», которые равны «достаточно» именно для таких случаев), но изменение правил - очень сложная задача.

Daniel Pryden 13.09.2018 18:10

@DanielPryden - Верно, и полезное наблюдение. Но Stringequals не поддерживает и не будет поддерживать класс Sign OP, так что ... :-)

T.J. Crowder 13.09.2018 18:12

Спасибо за ваш ответ. Я хорошо осведомлен об этих «отношениях» - проблема в том, что мне просто нужно, по моей собственной причине, чтобы фактическая String сравнивалась с моим объектом Sign, даже если они не совпадают. Я просто надеялся, что переопределения метода equals () будет достаточно, чтобы вызвать правильное сравнение, учитывая, что ArrayList в этом является «только для моего объекта». Итак, какое простое решение? Или есть хоть какой-то способ достичь того, что я ищу? А еще лучше я бы спросил вас, что мне набирать в Google, чтобы найти решение для себя. :)

LowProfile 13.09.2018 18:17

Для полноты из этих свойств реализация OP equals() демонстрирует только транзитивность. А там, где он сломан, он сломан и В самом деле: у него неприятная привычка кидать ClassCastException и NullPointerException.

John Bollinger 13.09.2018 18:17

@JohnBollinger Он также демонстрирует последовательность, не так ли?

Sweeper 13.09.2018 18:20

Технически нет, @Sweeper. Существует множество пар объектов, отличных от null, для которых equals() OP будет выдавать ClassCastException вместо того, чтобы возвращать либо true, либо false. Но если он делает возвращает true или false, да, он будет делать это последовательно.

John Bollinger 13.09.2018 18:22

@LowProfile - Я бы вообще не стал использовать ArrayList. Я бы использовал Map и сохранил объект Sign, используя его свойство char в качестве ключа. Затем вы используете containsKey, чтобы узнать, есть ли на карте объект, или get, чтобы получить объект на основе ключа.

T.J. Crowder 13.09.2018 18:23

@JohnBollinger Ах, недостаточно внимательно прочитал контракт ... Согласованность определяется как всегда, возвращающий одинаковое значение true или false.

Sweeper 13.09.2018 18:24

@LowProfile, то, что ваш ArrayList предназначен только для моего объекта, на самом деле не имеет отношения к ситуации. Как и другие классы коллекций, ArrayList зависит от элементов для реализации equals(Object) в соответствии с его документированным контрактом. Их собственное задокументированное поведение зависит от этого.

John Bollinger 13.09.2018 18:27

@ T.J. Краудер - в конце концов, я мог бы использовать карту - это была моя первая идея. Я просто хотел немного поработать с Arraylist и собственным equals (), hashCode ().

LowProfile 13.09.2018 18:29

Ваш метод equals реализован неправильно. Это нарушает генеральный договор Object.equals:

  • Он не является рефлексивным - поскольку он вызывает исключение, когда аргумент не является строкой, x.equals(x), где x - это Sign, выйдет из строя с исключением.
  • Он не симметричен - x.equals(y) не возвращает то же значение, что и y.equals(x), если y - это строка, а x - это Sign.
  • Это непоследовательно - поскольку оно может вызывать исключения, когда аргумент не является строкой, а не просто возвращать истину или ложь.

На низком уровне абстракции причиной этой проблемы является реализация contains. Согласно документам:

Returns true if this list contains the specified element. More formally, returns true if and only if this list contains at least one element e such that (o==null ? e==null : o.equals(e)).

ArrayList фактически вызывает o.equals(e), где o является строкой, которую вы передали. Таким образом, он фактически вызывает метод equals в String.

Если бы contains вызывал e.equals(o), тогда ваша программа напечатала бы «Я здесь», но ваш equals по-прежнему нарушает контракт.

Лучшая реализация equals выглядит примерно так:

@Override
public boolean equals(Object o) {
    if (o == null) {
        return false;
    }

    if (o.getClass() == this.getClass()) {
        Sign other = (Sign)o;
        return other.$char.equals($char); // I have renamed 'char' to '$char' since the former is not a valid identifier
    } else {
        return false;
    }
}

И ваш клиентский код:

    ArrayList<Sign> queueOfSigns = new ArrayList<>();

    Sign test = new Sign("C");
    Sign c = new Sign("C");
    queueOfSigns.add(test);

    if(queueOfSigns.contains(c))
        System.out.println("I am here!");

Обновлено:

Думаю, это то, что вы ищете:

arrayList.stream()
    .filter(x -> x.getChar().equals("C"))
    .findFirst().isPresent() // this returns true if a sign with C is found in the array list

Даже использование instanceof в equals - это хитрость. Если я создам класс NiftySign, который расширяет Sign, экземпляр NiftySign не равен экземпляру Sign, но Sign's equals (согласно приведенному выше) подумает, что это так.

T.J. Crowder 13.09.2018 18:26

Спасибо, проблема понятна, но код не выполняет то, что я на самом деле ищу. Вот в чем дело - мне нужно передать одну букву (например) как String или Character в метод contains () для ArrayList, который должен содержать объекты Sign (ArrayList <Sign>). Проблема сохраняется, потому что я просто хотел иметь свой собственный переопределенный метод equals (), который фактически сравнивал бы String из объекта Sign с заданным String, как в примере. Я просто хотел избежать instanceof, потому что в моем случае нет способа передать что-либо alse - даже если это никоим образом не является хорошей практикой - я знаю.

LowProfile 13.09.2018 18:26

Тогда вам не следует отменять equals. Вам нужно найти элемент в списке массивов, который имеет поле char из "C". Смотрите редактирование. @Низкопрофильный

Sweeper 13.09.2018 18:37

Другие вопросы по теме