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