Вызов equals в ArrayList после сериализации

Я столкнулся со странной проблемой, связанной с равными для объекта, передаваемого через RMI. Уже несколько дней это ломает мне голову, и мне было интересно, может ли кто-нибудь помочь пролить свет на проблему.

У меня есть класс Garage (который также является сущностью JPA, если это имеет значение), который я нажимаю на java-процесс под названием X через RMI (так что этот объект сериализуется). Объект Garage хранит список объектов с именем Car (также объекты JPA), которые также являются сериализуемыми.

Метод equals в Garage в основном вызывает equals в своем списке автомобилей (ArrayList).

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

Странно то, что при модульном тестировании он вызывает equals для всех членов Cars ArrayList. Я даже сериализовал объекты как часть модульного теста, и это тоже сработало. Любые идеи? Надеюсь, я разобрался с проблемой, не стесняйтесь запрашивать любую информацию, чтобы что-то уточнить.

Редактировать: я почти уверен, что его ArrayList странный, поскольку, когда я вручную выполняю equals в своем объекте вместо вызова equals в списке автомобилей, я выполнял цикл foreach в списке автомобилей и вызывал equals для каждого Автомобиль (в любом случае я ожидал, что ArrayList equals сделает, и он работал, как и ожидалось)

@Entity
@Table(schema="pdw", name="garage")
public class Garage
    implements Comparable<Garage> , 
    Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private String id;

    private String name;


    @OneToMany(cascade = CascadeType.ALL)
    @JoinTable(schema="pdw")
    private List<Car> cars = new ArrayList<Car>();

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public List<Car> getCars() {
        return cars;
    }
    public void setCars(List<Car> cars) {
        this.cars = cars;
    }

    @Override
    public String toString() {

        StringBuffer buffer = new StringBuffer();
        buffer.append("[");
        buffer.append("Garage:");
        buffer.append("[id:" + id + "]");
        buffer.append("[Cars:" + cars + "]");
        buffer.append("]");
        return buffer.toString();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof Garage))
            return false;
        Garage other = (Garage) obj;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        if (cars == null) {
            if (other.cars != null)
                return false;
        } else if (!cars.equals(other.cars))
            return false;
        return true;
    }

    @Override
    public int compareTo(Garage other) {
        return this.getName().compareTo(other.getName());
    }
}

@Entity
@Table(schema="pdw", name="car")
public class Car 
    implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private String id;

    private String name;

    @OneToOne(fetch = FetchType.LAZY)
    private Garage garage;

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Garage getGarage() {
        return garage;
    }
    public void setGarage(Garage garage) {
        this.garage = garage;
    }

    @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 (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

    @Override
    public String toString() {

        StringBuffer buffer = new StringBuffer();
        buffer.append("[");
        buffer.append("Car:");
        buffer.append("[id:" + id + "]");
        buffer.append("[name:" + name + "]");
        buffer.append("[garage:" + garage.getName() + "]");
        buffer.append("]");
        return buffer.toString();
    }   
}

person Paul Whelan    schedule 08.12.2009    source источник
comment
Действительно очень странно, как вы увидели, что equals не вызывается в процессе X? Единственное, что я мог себе представить, это то, что в процессе определение класса Garage отличается и что он не вызывает равных на автомобилях...   -  person Fried Hoeben    schedule 08.12.2009
comment
Трудно предоставить всю логику Rmi, однако я приведу пример взаимосвязи между объектами.   -  person Paul Whelan    schedule 08.12.2009
comment
Вызывает ли он equals для любого из объектов Car?   -  person MHarris    schedule 08.12.2009
comment
проверьте мой обновленный ответ - у вас может быть PersistentBag, а не ArrayList.   -  person Bozho    schedule 08.12.2009
comment
так ты решил свою проблему? Если да, разместите ответ здесь, чтобы другие участники могли получить пользу позже.   -  person Bozho    schedule 15.12.2009
comment
Эй, Божо, я работал над другим проектом в последнее время, пока я вернусь, чтобы посмотреть на этот, надеюсь, в понедельник. Проблема все еще присутствует, код сейчас работает с обходным путем, но я ненавижу включать обходной путь в проект, не имея возможности объяснить поведение. Я буду держать вас в курсе решения   -  person Paul Whelan    schedule 16.12.2009


Ответы (8)


  • Убедитесь, что ваш List не пуст после десериализации.
  • Поставьте точку останова в методе equals и посмотрите, не происходит ли что-то не так.
  • Убедитесь, что ваша реализация equals на Car верна.
  • Проверьте, нет ли transient полей

  • Убедитесь, что то, что вы ожидаете от ArrayList, на самом деле не является PersistentBag. Потому что его equals не будет делать то, что вы хотите. Если это PersistentBag, вы можете либо передать его в ArrayList перед отправкой по сети (тем самым предотвратив потенциальное LazyInitializationException), либо вызвать equals для каждого элемента, а не для самого List. Hibernate использует PersistentBag для переноса ваших коллекций, чтобы обеспечить ленивую загрузку.

P.S. Если вы используете поставщика JPA, отличного от Hibernate, возможно, у него есть аналогичные оболочки коллекций. Укажите, какой у вас поставщик постоянства.

person Bozho    schedule 08.12.2009
comment
Спасибо, я могу подтвердить, что вышеперечисленное в порядке. - person Paul Whelan; 08.12.2009

Чтобы развернуть Божо:

  • Проверьте, имеют ли классы с обеих сторон одинаковые serialVersionUID (т. е. имеют ли они одни и те же скомпилированные версии). При необходимости жестко закодируйте его как переменную класса private static final long.

Подробнее об этом читайте в java.io.Serializable API и статья Sun о сериализации.

person BalusC    schedule 08.12.2009
comment
да, я могу подтвердить, что serialVersionUID в порядке, спасибо за предложение - person Paul Whelan; 08.12.2009
comment
Другими словами, вы жестко запрограммировали его? Вы уверены, что путь к классам чист, то есть в пути к классам нет более старых версий классов? - person BalusC; 08.12.2009
comment
Да, у меня есть чистый путь к классам, и у меня есть serialVersionUID 1 - person Paul Whelan; 08.12.2009

Вы упомянули, что используете JPA... убедитесь, что объект содержит полный ArrayList перед сериализацией, возможно, вы лениво загружаете список, и он пуст после сериализации и десериализации списка? Единственное, чего я не понимаю (если это так), это почему вы не получаете ошибок при попытке лениво создать экземпляр списка, когда он не в сеансе (как я подозреваю, это имеет место на стороне десериализации).

person PaulP1975    schedule 08.12.2009
comment
Спасибо, Пол, я могу подтвердить, что у объекта есть все автомобили до того, как он перешел на rmi. - person Paul Whelan; 08.12.2009

ArrayList использует AbstractList реализацию equals(). Это определяется так:

Сравнивает указанный объект с этим списком на предмет равенства. Возвращает true тогда и только тогда, когда указанный объект также является списком, оба списка имеют одинаковый размер и все соответствующие пары элементов в двух списках равны. (Два элемента e1 и e2 равны, если (e1==null ? e2==null : e1.equals(e2)).) Другими словами, два списка определяются как > равные, если они содержат одни и те же элементы в одном и том же заказ.

Эта реализация сначала проверяет, является ли указанный объект этим списком. Если это так, он возвращает true; если нет, он проверяет, является ли указанный объект списком. Если нет, возвращается false; если это так, он выполняет итерацию по обоим спискам, сравнивая соответствующие пары элементов. Если какое-либо сравнение возвращает false, этот метод возвращает false. Если у одного из итераторов заканчиваются элементы раньше, чем у другого, он возвращает false (поскольку списки имеют разную длину); в противном случае он возвращает true, когда итерации завершаются.

Если ваши Car не сравниваются, возможно, сравнение уже дает сбой на ранних этапах сравнения списков? Возможно ли, что списки, которые вы сравниваете, содержат разное количество элементов?

person Carl Smotricz    schedule 08.12.2009
comment
Спасибо, Карл, это то, что я сначала предположил, но когда я сравниваю через foreach для всех элементов, это работает, я фактически добавил println в цикл, и каждая машина присутствовала, и когда я отменил равенство в списке, он работал, как я и ожидал, ArrayList сделать. Я перепроверю это снова, однако спасибо. - person Paul Whelan; 08.12.2009

Если у вас установлены исходные коды Java, вы можете выполнить отладку реализации AbstractList equals и посмотреть, где она дает сбой. Текущая реализация для Java 1.6:

public boolean equals(Object o) {
if (o == this)
    return true;
if (!(o instanceof List))
    return false;

ListIterator<E> e1 = listIterator();
ListIterator e2 = ((List) o).listIterator();
while(e1.hasNext() && e2.hasNext()) {
    E o1 = e1.next();
    Object o2 = e2.next();
    if (!(o1==null ? o2==null : o1.equals(o2)))
    return false;
}
return !(e1.hasNext() || e2.hasNext());
}

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

1- Если вы переопределяете equals, вам нужно переопределить hashCode, я не знаю, удалили ли вы его намеренно или не реализовали. equals() и hashCode() связаны совместным контрактом, который указывает, что если два объекта считаются равными с использованием метода equals(), то они должны иметь одинаковые значения хэш-кода. (Позаимствовано из книги SCJP). В противном случае у вас будут проблемы с этими классами в HashMaps, HashSets и других классах коллекций.

2- В ваших реализациях equals instanceof проверяет как нуль, так и тип класса, вы можете заменить

    if (obj == null)
            return false;
    if (getClass() != obj.getClass())
            return false;

с

if (!(obj instanceof Car)){
            return false;
}
person Iker Jimenez    schedule 08.12.2009

Возможно, вы получаете подклассы для своих объектов (я знаю, что hibernate создает прокси-классы для поддержки ленивой загрузки). в вашем методе класса Car equals вы выполняете сравнение "getClass()", которое будет ложным, если вы сравниваете подкласс прокси с фактическим экземпляром. вы можете попробовать операцию instanceof вместо getClass() (как в вашем методе Garage equals). Вы можете подтвердить все это, включив «getClass()» в свой метод toString().

кроме того, (опять же ленивая загрузка), вы никогда не должны ссылаться на переменные-члены непосредственно в классе сущностей, вы всегда должны использовать геттеры и сеттеры. (поэтому ваши методы equals,toString,... должны использовать getName() и т.д.

person james    schedule 08.12.2009
comment
Спасибо, Джеймс, я попробую. - person Paul Whelan; 09.12.2009

Просто предположение: возможно ли, что когда вы отправляете экземпляр Garage, процесс X получает для него только заглушку? Если это так, когда вы вызываете метод equals, он может фактически выполнять удаленный вызов, фактически вызывая эти методы в исходной JVM (а не в X-процессе).

Вы должны быть в состоянии подтвердить это, добавив точки останова в обе JVM и вызвав equals.

person Chuim    schedule 08.12.2009

Я только что столкнулся с той же проблемой. JUnit assertEquals() для коллекций, возвращаемых Hibernate, завершится ошибкой, когда Hibernate заменит ваш список своим классом PersistentBag, потому что этот класс неправильно реализует equals(). Вот код из класса Hibernate 3.5.1-Final PersistentBag:

/**
 * Bag does not respect the collection API and do an
 * JVM instance comparison to do the equals.
 * The semantic is broken not to have to initialize a
 * collection for a simple equals() operation.
 * @see java.lang.Object#equals(java.lang.Object)
 */
public boolean equals(Object obj) {
    return super.equals(obj);
}

Прочитав их комментарий, кажется, что они делают это из соображений производительности/эффективности. Но это усложняет модульное тестирование, если у вас есть объект, содержащий список. Моим решением будет написать метод areEqualNonNullLists(listA, listB) и поместить его в метод eqauls() моего объекта, содержащего список.

public static boolean areEqualNonNullLists(List thisList, List thatList)
{
    if(thisList.size() != thatList.size()) return false;

    for(int i=0; i<thisList.size(); i++)
    {
        if(!thisList.get(i).equals( thatList.get(i) ) ) return false;
    }
    return true;
}

Интересно, есть ли более элегантное универсальное решение.

person user449065    schedule 18.02.2011