3

I have to objects Client and Order and these objects are living in bidirectional relation and I try to write them to file, but I get StackOverflowError. I got this error because my equals methods are looped.

My classes which I try to serialize:

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Client implements Serializable {

    private Long id;

    private String name;

    private List<Order> orders = new ArrayList<>();

    public void addOrder(Order order) {
        order.setClient(this);
        orders.add(order);
    }

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

        Client client = (Client) o;

        if (id != null ? !id.equals(client.id) : client.id != null) return false;
        if (name != null ? !name.equals(client.name) : client.name != null) return false;
        return orders != null ? orders.equals(client.orders) : client.orders == null;
    }

    @Override
    public int hashCode() {
        int result = id != null ? id.hashCode() : 0;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + (orders != null ? orders.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Client{" +
                "id=" + id +
                ", name='" + name + '\'' +
//                ", orders=" + orders.size() +
                '}';
    }
}

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Order implements Serializable {

    private Long id;

    private String name;

    private Client client;

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

        Order order = (Order) o;

        if (id != null ? !id.equals(order.id) : order.id != null) return false;
        if (name != null ? !name.equals(order.name) : order.name != null) return false;
        return client != null ? client.equals(order.client) : order.client == null;
    }

    @Override
    public int hashCode() {
        int result = id != null ? id.hashCode() : 0;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + (client != null ? client.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Order{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

@Data
@AllArgsConstructor
public class MapDataSource implements Serializable {

    private final Map<Date, List<Client>> clients = new HashMap<>();
    private final Map<Date, List<Order>> orders = new HashMap<>();
}

@Slf4j
public class ObjectWriter {
    private final String fileName = "data.obj";

    public void write(String fileName, MapDataSource mapDataSource) {
        try (
                FileOutputStream fs = new FileOutputStream(fileName);
                ObjectOutputStream oos = new ObjectOutputStream(fs)
        ) {
            oos.writeObject(mapDataSource);
            log.info("Object has been written.");
        } catch (IOException ioe) {}
    }
}

@Slf4j
public class ObjectReader {
    private static final String fileName = "data.obj";

    public MapDataSource readObj(String fileName) {
        MapDataSource mapDataSource = null;
        try (
                FileInputStream fis = new FileInputStream(fileName);
                ObjectInputStream ois = new ObjectInputStream(fis)
        ) {
            mapDataSource = ((MapDataSource) ois.readObject());
//            log.info("Read object: {}", mapDataSource);
        } catch (IOException ioe) {

        } catch (ClassNotFoundException classEx) {
            System.out.println();
        }
        return mapDataSource;
    }
}

And when I try to run code below I get StackOVerflowError:

String testFile = "testFile.obj";
        final DateTime time = new DateTime(2017, 12, 1, 10, 0);
        final Client client1 = new Client(1L, "Client1", new ArrayList<>());
        final Order order1 = new Order(1L, "Order1", null);
        final MapDataSource mapDataSource = new MapDataSource();
        mapDataSource.getClients().put(time.toDate(), new ArrayList<>());
        mapDataSource.getClients().get(time.toDate()).add(client1);
        mapDataSource.getOrders().put(time.toDate(), new ArrayList<>());
        mapDataSource.getOrders().get(time.toDate()).add(order1);

        new ObjectWriter().write(testFile, mapDataSource);
        final MapDataSource found = new ObjectReader().readObj(testFile);
        System.out.println(found);

Solution: MapDataSource needs to have implemented equals() and hashcode() methods.

user
  • 4,410
  • 16
  • 57
  • 83
  • Totally unrelated to your question: style wise you don't need those one line if statements, you can just return the Boolean expressions. – Meepo Dec 03 '17 at 02:44
  • See https://stackoverflow.com/questions/7602089/examining-associated-objects-in-equals – Raedwald Dec 04 '17 at 10:05

1 Answers1

2

It seems like you need to sit down and seriously consider what it should even mean for two clients or orders to be equal in the first place. The Long id; makes me wonder whether you should really be comparing the object graphs in the first place. If e.g. clients have unique IDs, then it could make sense to just ensure that clients are unique object instances and then you do away with the problem entirely.

If you really do need to compare object graphs, you could use something like the following. We use an IdentityHashMap to record all of the objects we've seen, then if we detect a cycle, we just compare the previously-stored counter values which tells us if the two graphs have the same cycle.

Client and Order need to share code (so the maps can be passed around), so you just override equals in both to return ClientOrderEquality.equals(this, that).

import java.util.*;

public final class ClientOrderEquality {
    private ClientOrderEquality() {}

    private static final class Counter { long value; }

    public static boolean equals(Client lhs, Client rhs) {
        return equals(lhs, new IdentityHashMap<>(),
                      rhs, new IdentityHashMap<>(),
                      new Counter());
    }

    public static boolean equals(Order lhs, Order rhs) {
        return equals(lhs, new IdentityHashMap<>(),
                      rhs, new IdentityHashMap<>(),
                      new Counter());
    }

    private static boolean equals(Client            lhs,
                                  Map<Object, Long> seenL,
                                  Client            rhs,
                                  Map<Object, Long> seenR,
                                  Counter           counter) {
        if (lhs == null || rhs == null)
            return lhs == rhs;
        Long countL = seenL.putIfAbsent(lhs, counter.value);
        Long countR = seenR.putIfAbsent(rhs, counter.value);
        if (countL != null || countR != null)
            return Objects.equals(countL, countR);
        counter.value++;
        if (lhs == rhs)
            return true;
        if (!Objects.equals(lhs.id, rhs.id))
            return false;
        if (!Objects.equals(lhs.name, rhs.name))
            return false;
        if (lhs.orders.size() != rhs.orders.size())
            return false;
        Iterator<Order> itL = lhs.orders.iterator();
        Iterator<Order> itR = rhs.orders.iterator();
        while (itL.hasNext() && itR.hasNext())
            if (!equals(itL.next(), seenL, itR.next(), seenR, counter))
                return false;
        return true;
    }

    private static boolean equals(Order             lhs,
                                  Map<Object, Long> seenL,
                                  Order             rhs,
                                  Map<Object, Long> seenR,
                                  Counter           counter) {
        if (lhs == null || rhs == null)
            return lhs == rhs;
        Long countL = seenL.putIfAbsent(lhs, counter.value);
        Long countR = seenR.putIfAbsent(rhs, counter.value);
        if (countL != null || countR != null)
            return Objects.equals(countL, countR);
        counter.value++;
        if (lhs == rhs)
            return true;
        if (!Objects.equals(lhs.id, rhs.id))
            return false;
        if (!Objects.equals(lhs.name, rhs.name))
            return false;
        return equals(lhs.client, seenL, rhs.client, seenR, counter);
    }
}

I assume if you want to actually use that code, you'll need to alter it to use whatever getter naming format you're using and write a hashCode implementation. You'll also need to consider subtypes correctly if you're extending Client and Order.

Radiodef
  • 37,180
  • 14
  • 90
  • 125