How to correctly implement equals(), hashCode() for Tree in Java?

FreeOnGoo :

I have a tree structure and I need to override the methods equals/hashCode because I use the check of the expected result in the unit tests.

The problem with tree type structures is that they refer to each other recursively. In particular, parents for children and vice versa.

and if all fields are used in the methods equals/hashCode, then there will be a looping. The question is how to correctly override then in order not to violate the contract.

I will give an example of how I implemented it.

public class App {
    public static void main(String[] args) {
        Book book1 = new Book(1L, "The catcher in the rye");
        Book book2 = new Book(2L, "Rich Dad Poor Dad");

        BookTree bookTree1 = new BookTree(book1);
        BookTree bookTreeChild1 = new BookTree(book2);
        bookTree1.addChild(bookTreeChild1);

        BookTree bookTree2 = new BookTree(book1);
        BookTree bookTreeChild2 = new BookTree(book2);
        bookTree2.addChild(bookTreeChild2);

        if (!bookTree1.equals(bookTree2)) {
            throw new RuntimeException("Invalid override equals");
        }
    }
}

class Book {
    private Long id;
    private String name;

    public Book(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) return true;
        if (object == null || getClass() != object.getClass()) return false;
        Book book = (Book) object;
        return Objects.equals(id, book.id) &&
                Objects.equals(name, book.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}

class Tree<T> {
    private List<Tree<T>> children = new ArrayList<>();
    private Tree<T> parent = null;
    private T data;

    public Tree(T data) {
        this.data = data;
    }

    public Tree(T data, Tree<T> parent) {
        this.data = data;
        parent.addChild(this);
    }

    public List<Tree<T>> getChildren() {
        return children;
    }

    public void addChild(Tree<T> child) {
        child.setParent(this);
        this.children.add(child);
    }

    public void addChild(T data) {
        Tree<T> newChild = new Tree<>(data);
        this.addChild(newChild);
    }

    public void removeChildren() {
        this.children = new ArrayList<>();
    }

    public void addChildren(List<Tree<T>> children) {
        for(Tree<T> t : children) {
            t.setParent(this);
        }
        this.children.addAll(children);
    }

    private void setParent(Tree<T> parent) {
        this.parent = parent;
    }

    public Tree<T> getParent() {
        return parent;
    }

    public T getData() {
        return this.data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public boolean isRoot() {
        return (this.parent == null);
    }

    public boolean isLeaf() {
        return this.children.size() == 0;
    }

    public void removeParent() {
        this.parent = null;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) return true;
        if (object == null || getClass() != object.getClass()) return false;
        Tree<?> tree = (Tree<?>) object;
        return Objects.equals(children, tree.children) &&
                Objects.equals(data, tree.data);
    }

    @Override
    public int hashCode() {
        return Objects.hash(children, data);
    }
}

class BookTree extends Tree<Book> {

    public BookTree(Book data) {
        super(data);
    }

    public BookTree(Book data, Tree<Book> parent) {
        super(data, parent);
    }
}

As you can see from my implementation, I use only two fields: "data" and "children". Accordingly, my question is whether I implemented the methods equals/hashCode correctly? If wrong, then please show how.

GhostCat salutes Monica C. :

Accordingly, my question is whether I implemented the methods equals/hashCode correctly?

First of all: "what is correct?" ... one could wonder why a Tree should implement equals() and hashCode() in the first place. Especially hashCode() is tricky: the point of that method is (mainly) so you can store the corresponding object in a HashMap/HashSet. But that raises a big red flag: both these classes do not like it, when hashCode() returns different values over time. And that is exactly what your code will be doing: every time you change your tree (adding/removing a node), hashCode() will give a different result.

So we could have a look at what the standard libs do: and there we find JTree ... which doesn't implement both methods! On the other hand, when we look towards AbstractSet (which is the base class for TreeSet), there we find that both methods are implemented and include the members. So both ways seem to be valid.

Coming back to the question: that really depends how you want these two methods to work. Are two trees equal when they have the exact same content (meaning: does the order of children matter)?

Long story short: assuming that you want to ensure that all data is equal, and that all children are equal, and in the same order, then your implementation seems correct.

And yes, that restriction to only check these two attributes makes a lot of sense: when you include the parent link, you immediately get into a recursion that can't be broken.

Finally: you tagged this question with JUnit. This implies that you consider writing tests for your production code. Then these tests should answer your question. Meaning: one approach would be that you sit down and define the contract for these two methods. And then you create a number of test cases that verify all aspects of these contracts. And then your test cases tell you whether your production code meets your contract.

I think that is the crucial point here: there is no universal rule that tells us if/how to implement equals() and hashCode() for a Tree class. You have to look into your requirements if/how to do that. Then you derive tests from that knowledge, which you then you apply in order to verify if a given implementation meets the requirements/contract.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=132649&siteId=1