Comparison of objects in java (three ways)

Keywords: Java data structure

Question raised

Last class, we talked about priority queue. When inserting elements into priority queue, there is a requirement: the inserted elements cannot be null or the elements must be able to be compared. For simplicity, we just inserted Integer type. Can we insert custom type objects in priority queue? Let's start with a code:

class Card {
    public int rank; // numerical value
    public String suit; // Decor

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public String toString() {
        return "Card{" +
                "rank=" + rank +
                ", suit='" + suit + '\'' +
                '}';
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Card card1 = new Card(1, "♦");
        Card card2 = new Card(1, "♦");
        Card card3 = new Card(3, "♦");
        //Create a priority queue
        Queue<Card> qu = new PriorityQueue<>();
        //Insert an element into the priority queue first
        qu.offer(card1);
        //The printed result is [Card{rank=1, suit = ' ♦'}]
        System.out.println(qu);
    }
}

You can see that the print result is good. Next, let's continue to see: when we add a second custom object to this heap, let's see what happens:

class Card {
    public int rank; // numerical value
    public String suit; // Decor

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public String toString() {
        return "Card{" +
                "rank=" + rank +
                ", suit='" + suit + '\'' +
                '}';
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Card card1 = new Card(1, "♦");
        Card card2 = new Card(1, "♦");
        Card card3 = new Card(3, "♦");
        //Create a priority queue
        Queue<Card> qu = new PriorityQueue<>();
        //Insert an element into the priority queue first
        qu.offer(card1);
        qu.offer(card2);
        System.out.println(qu);
    }
}

At this point, we will find that there is a type conversion exception (ClassCastException): why?

At this time, we click the error code:

Then the corresponding error reporting statement is found:

Here, we find that it forcibly converts x into a Comparable type. In the above code, it is equivalent to that x is our card. If this type of card wants to realize type conversion, it should at least inherit our Comparable interface, and there is another problem. We mentioned earlier when talking about priority queue that each element will be adjusted when we insert it again The process of forming a small root heap and adjusting it into a small root heap is actually the process of comparing the size. Previously, all our codes inserted objects as integers, which are very easy to compare the size. But this time we inserted custom objects. Take our card as an example. When we inserted the second custom object, did we compare the color or the size of the number of cards , this does not tell us how to compare. Therefore, when the second inserted element is viewed in our priority queue, we need to build a small root heap, but it does not tell us the way to compare the inserted objects, which eventually leads to high type conversion. Therefore, at this time, we should let the card class implement our Comparable interface and tell us to compare What's the way
And you should pay attention to: Why did people automatically compare the size and build a large / small heap when we inserted an integer before? Let's look at:

We press Ctrl to enter the source code of Integer:

At this point, we will find that Integer has already implemented the Comparable interface

analysis

Here we provide two methods to tell us how to compare:
1: Comparable interface
2: Comparator interface

Comparable interface

Compare according to the value of rank

The bottom layer is small root pile

Here is the correct comparison method to prevent type conversion exceptions when inserting a custom object into the priority queue:
Let's start with our code:

class Card implements Comparable<Card> {
    public int rank; // numerical value
    public String suit; // Decor

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public String toString() {
        return "Card{" +
                "rank=" + rank +
                ", suit='" + suit + '\'' +
                '}';
    }

    @Override
    public int compareTo(Card o) {
        //Here is to tell us to compare according to the number of cards. At this time, it defaults to small root heap
        return this.rank - o.rank;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Card card1 = new Card(1, "♦");
        Card card2 = new Card(2, "♦");
        Card card3 = new Card(3, "♦");
        //Create a priority queue
        Queue<Card> qu = new PriorityQueue<>();
        qu.offer(card2);
        qu.offer(card1);
        qu.offer(card3);
        //The output result is [Card{rank=1, suit = ' ♦'},  Card{rank=2, suit=' ♦'},  Card{rank=3, suit=' ♦'}]
        System.out.println(qu);
    }

matters needing attention:
1: First of all, we insert our custom object card into our priority queue, which has two attributes, one is the card value and the other is the color of the card. In the above code, we first compare according to the card value
2: When our card class implements the comparable interface, we need to rewrite our compareTo method. When return this.rank - o.rank, it means that every time we insert a card object into the heap, our heap is adjusted in the form of small root heap. Therefore, the comparison method is also in the form of small root heap, and the compared value is the value of rank in our card object

The bottom layer is a large root pile
class Card implements Comparable<Card> {
    public int rank; // numerical value
    public String suit; // Decor

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public String toString() {
        return "Card{" +
                "rank=" + rank +
                ", suit='" + suit + '\'' +
                '}';
    }

    @Override
    public int compareTo(Card o) {
        //Here is to tell us to compare according to the number of cards. At this time, it defaults to large root heap
        return o.rank - this.rank;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Card card1 = new Card(1, "♦");
        Card card2 = new Card(2, "♦");
        Card card3 = new Card(3, "♦");
        //Create a priority queue
        Queue<Card> qu = new PriorityQueue<>();
        qu.offer(card2);
        qu.offer(card1);
        qu.offer(card3);
        //The output result is [Card{rank=3, suit = ' ♦'},  Card{rank=1, suit=' ♦'},  Card{rank=2, suit=' ♦'}]
        System.out.println(qu);
    }
}

matters needing attention:
Note that we specify here that the user-defined object we insert into the priority queue is compared according to the value of rank. At the same time, we also specify that the heap is adjusted according to the form of large root heap every time we insert it

Comparator interface

Compare according to the value of rank

The bottom layer is small root pile
class Card {
    public int rank; // numerical value
    public String suit; // Decor

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public String toString() {
        return "Card{" +
                "rank=" + rank +
                ", suit='" + suit + '\'' +
                '}';
    }

}

public class TestDemo {
    public static void main(String[] args) {
        Card card1 = new Card(1, "♦");
        Card card2 = new Card(2, "♦");
        Card card3 = new Card(3, "♦");
        //Create a priority queue
        Queue<Card> qu = new PriorityQueue<>(new Comparator<Card>() {
            @Override
            public int compare(Card o1, Card o2) {
                //According to the rank value, the default is small root heap
               return o1.rank - o2.rank;
            }
        });
        qu.offer(card2);
        qu.offer(card1);
        qu.offer(card3);
        //The output result is [Card{rank=1, suit = ' ♦'},  Card{rank=2, suit=' ♦'},  Card{rank=3, suit=' ♦'}]
        System.out.println(qu);
    }
}

matters needing attention
Here we use the Comparator interface to implement it. Note that it is different from the previous one. Then o1.rank - o2.rank represents that the default heap building form is small root heap

The bottom layer is a large root pile
class Card {
    public int rank; // numerical value
    public String suit; // Decor

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public String toString() {
        return "Card{" +
                "rank=" + rank +
                ", suit='" + suit + '\'' +
                '}';
    }

}

public class TestDemo {
    public static void main(String[] args) {
        Card card1 = new Card(1, "♦");
        Card card2 = new Card(2, "♦");
        Card card3 = new Card(3, "♦");
        //Create a priority queue
        Queue<Card> qu = new PriorityQueue<>(new Comparator<Card>() {
            @Override
            public int compare(Card o1, Card o2) {
                //According to the rank value, the default is small root heap
                return o2.rank - o1.rank;
            }
        });
        qu.offer(card2);
        qu.offer(card1);
        qu.offer(card3);
        //The output result is [Card{rank=3, suit = ' ♦'},  Card{rank=1, suit=' ♦'},  Card{rank=2, suit=' ♦'}]
        System.out.println(qu);
    }
}

matters needing attention
Here we use the Comparator interface to implement it. Note that it is different from the previous one. Then o2.rank - o1.rank represents that the default heap building form is large root heap

Comparison of elements

Object comparison (three ways)

class Card implements Comparable<Card> {
    public int rank; // numerical value
    public String suit; // Decor

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }

    @Override
    public String toString() {
        return "Card{" +
                "rank=" + rank +
                ", suit='" + suit + '\'' +
                '}';
    }

    @Override
    public int compareTo(Card o) {
        return o.rank - this.rank;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Card card1 = new Card(1, "♦");
        Card card2 = new Card(2, "♦");
        Card card3 = new Card(3, "♦");
        //Direct compilation error
        System.out.println(card1 > card2);
        //false because the addresses in memory are different
        System.out.println(card1 == card2);
        //Compilation error
        System.out.println(card1 < card2);
        //The result is 1
        System.out.println(card1.compareTo(card2));
        //The result is false
        //Because our Card class does not override the equals method in our Object class, the equals method here calls the equals method in the Object class
        System.out.println(card1.equals(card2));
    }
}

matters needing attention
1: It can be seen from the compilation resu lt s that variables of reference type in Java cannot be directly compared in the way of > or < why = = can be compared?
Because: for user-defined types, they inherit from the Object class by default, and the equals method is provided in the Object class, and = = calls the equals method by default, but the comparison rule of this method is: instead of comparing the contents of the reference Object of the reference variable, they directly compare the address of the reference variable, but in some cases, this comparison does not meet the meaning of the problem.

Under what circumstances does it not meet the meaning of the question

class Card {
    public int rank; // numerical value
    public String suit; // Decor

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }

}

public class TestDemo {
    public static void main(String[] args) {
        Card card1 = new Card(1, "♦");
        Card card2 = new Card(1, "♦");
        Card card3 = new Card(3, "♦");
        //The result is false
        System.out.println(card1.equals(card2));
    }
}

At this time, we will find that the objects pointed to by card1 and card2 are actually a Card, but although the two references are logically the same, two memories are opened in the heap to store the objects pointed to by the two references, and the equals method here calls the equals method in our Object class, The comparison method is reference comparison. The code corresponding to the above code is card1 == card2, so it is false. But we clearly are a Card. Why do we return false? We want it to be true. We compare its contents like the equals method of String without comparing addresses. Then we need to rewrite the equals method in our Object class in the Card class, (the reason why the equals method can be used to compare content in a String is that it overrides the equals method in the String class.) let's look at the code below

class Card {
    public int rank; // numerical value
    public String suit; // Decor

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }


    //Note that the equals and hashcode methods appear in pairs and are generated directly without our own modification
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Card card = (Card) o;
        return rank == card.rank &&
                Objects.equals(suit, card.suit);
    }

    @Override
    public int hashCode() {
        return Objects.hash(rank, suit);
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Card card1 = new Card(1, "♦");
        Card card2 = new Card(1, "♦");
        Card card3 = new Card(3, "♦");
        //The result is true
        System.out.println(card1.equals(card2));
    }
}

After rewriting, when we call the equals method with the card1 reference, we will directly compare whether the contents are equal
And note that when we rewrite the equals method, we will find that the equals method here appears together with our hashcode method. In fact, these two methods often appear together. Moreover, we will find that the equals method has been written for us, and we don't need to write logic ourselves at all
2: Object comparison can also use the compareTo method. Note that the premise of using the compareTo method is that our class should inherit the Comparable interface and override the compareTo method
At the same time, we can also use the Comparator interface to complete the comparison: look at the code:

  • The first step is to define our comparison method, which is a separate class
    This class is named CardComparator class
public class CardComparator implements Comparator<Card> {
    @Override
    public int compare(Card o1, Card o2) {
        if (o1 == o2) {
            return 0;
        }

        if (o1 == null) {
            return -1;
        }

        if (o2 == null) {
            return 1;
        }
        return o1.rank - o2.rank;
    }
}
  • The second step is to directly compare the codes:
class Card {
    public int rank; // numerical value
    public String suit; // Decor

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }

}

public class TestDemo {
    public static void main(String[] args) {
        Card card1 = new Card(1, "♦");
        Card card2 = new Card(1, "♦");
        Card card3 = new Card(2, "♦");
        CardComparator cardComparator = new CardComparator();
        //The result is 0
        System.out.println(cardComparator.compare(card1, card2));
        //The result is - 1
        System.out.println(cardComparator.compare(card2, card3));
        //The result is 1
        System.out.println(cardComparator.compare(card3, card2));

    }
}

summary

There are actually three ways to compare objects. The first is to override the equals method of the base class, the second is to compare based on the Comparble interface class, and the third is based on the Comparator comparison, that is, our Comparator interface. For the comparison of Comparator interfaces, you can either declare a class outside the class to define the comparison method, or define the comparison party inside the class as in this article We have also written a blog about Comparable interface and Comparator interface before. You can click the following link to review:
Click me to enter the blog

Posted by jenniferG on Fri, 29 Oct 2021 21:18:00 -0700