java比较器

2021/02/17 Java初级进阶 共 10958 字,约 32 分钟
闷骚的程序员

1. 浅谈需要比较器的背景

1.1 为啥需要比较器?

目前我们已经学习完了数组、集合等。
我们知道数组和集合中可以保存一堆数据,而且Arrays.sort()方法、Collections.sort()方法、TreeSet等都可以对其中保存的对象元素进行排序。
如果保存的是简单数据类型,则默认会按照数据字典从小到大进行排序。
那么现在问题来了,如果保存的是我们自定义的javabean对象的话,怎么排序呢?

正如我们提出的问题那样,需要比较器的背景应该出于如下因素:
java中的对象,一般情况下,我们只能对两个java对象进行相等比较,比较他们是否相等(相同):== 或者 !=。
而不能比较两个java对象的大小,即不能使用 > 或者 < 去比较(当然我们这里谈论的是java对象,对于类似int类型的数字java编译器自然是允许直接使用大于小于进行大小比较的)。
但实际开发中,我们经常会碰到排序操作:将一堆数据按照从小到大或者从大到小的顺序进行排列。
很明显,要进行排序,就得比较java对象的大小;而不能说是通过比较两个java对象是否相同来进行排序。
只有知道谁大谁小,才能按照从小到大或者从大到小的顺序进行排序啊!

如何实现java对象大小的比较呢?
java中提供了两个接口:

Comparable;
Comparator;

tips: 首先我们必须清楚: 在java中比较两个java对象是否相同,直接使用 == 比较二者引用地址即可。 比较两个java对象内容是否相同,则使用对象覆盖后的equals()方法进行比较即可。 同理,要比较两个java对象谁大谁小,那对象本身必须提供大小比较的规则啊;即什么情况下表示当前对象大,什么情况下表示当前对象小。 在java中,只要是想比较两个java对象的大小,那么该java对象就一定提供了规则,换言之,就一定实现了Comparable接口或者Comparator接口。 那为啥我们能方便的比较两个包装类型的数据大小呢? 那是因为jdk将常见的类都实现了Comparable接口,所以我们才能方便的比较两个整数或者字符串等。

1.2 深入理解顺序和大小的关系

比较的含义? 比较的目的是啥?
我比你高、我比你胖、这个苹果比那个苹果大…
生活中的各种比较,是为了啥?
我们就拿最基本的数字来说:为啥2比1大?2比1大能干啥?
在我看来,比较两个东西的目的实际上是为了排序
分出个胜负,争出个高低,目的是为了优先级,即顺序。

compareTo方法:
比较器提供的比较方法:

int compareTo(T o);

官方含义:比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
仔细斟酌下官方文档的解释,其返回结果表示的是顺序,那么顺序怎么表示呢?我们只能通过大小来表示顺序,所以compareTo方法实际上返回的是两个对象的大小规则,它此时和顺序还没有关系。
其实单独来看compareTo比较器是没有意义的,因为它只是一个比较器,用于返回两个对象的大小比较规则,我们实现了compareTo方法后只能得到两个对象谁大谁小的结果,而没法排序。
要理解compareTo,必须结合指定的排序算法去看。
结合起来看的话,这里面就有两个规则:一个是大小的规则;一个是排序的规则。而排序的规则依赖于大小的规则,即我们只有分出大小了,才能指定排序啊。
所谓比较规则:就是什么情况下表示我比你大?什么情况下表示我比你小?
所谓排序规则:就是,如果我比你大,我是在你前面还是在你后面?如果我比你小,你是在我前面还是后面?
我们再来看看compareTo()方法,它实际上只提供了一个返回值结果,这个结果表示的是大小,也就是说,compareTo()方法只是提供了大小的规则。
即:

如果compareTo返回负整数,则认为当前对象比目标对象小; 
如果compareTo返回正整数,则认为当前对象比目标对象大; 
如果compareTo返回0,则认为当前对象和目标对象大小相同。

那么,当我们通过compareTo()方法知道了大小的结果后,我们要对多个对象进行排序,还需要指定排序的规则。即:是从大到小还是从小到大?
实际上这个规则,jdk内部已经为我们指定好了,jdk有三个地方的排序依赖compareTo方法的大小结果:

Arrays.sort()方法;
Collections.sort()方法;
TreeSet或者TreeMap等集合结构。

而这些方法内部默认使用的排序规则是:从小到大。
实际上底层使用的就是二叉树的排序算法,即从小到大排序。
现在我们总结一下:

compareTo方法提供了比较两个对象大小的规则;  
sort方法或者Tree结构中提供了根据大小进行排序的规则,即从小到大。  

因此将两者结合在一起来看compareTo方法即得出如下结论:
如果是负整数,说明当前对象小于目标对象,而此处“小于”的含义是指当前对象在目标对象前面(参考 1小于2,1在2前面);
如果是正整数,说明当前对象大于目标对象,“大于”实际上是表示当前对象在目标对象后面(参考2大于1,2在1前面);
如果是0,说明当前对象和目标对象相同,“相同”实际上是表示二者顺序完全相同。

1.3 很多类jdk默认实现了比较器

jdk为基本类型的包装类和String都实现了Comparable接口,覆盖了compareTo()方法,指定了比较两个对象大小的规则:自然大小规则。
而sort排序的默认规则是自然规则。
所谓自然规则,即如果是数组,就默认从小到大,如果是字母,就按照字母顺序排序等等,即符合自然顺序特征。

测试代码:

package a.compare;
import java.util.Arrays;

// java比较器
public class CompareMain {
    public static void main(String[] args) {
        String s1 = "A";
        String s2 = "B";
        String s3 = "C";
        String s4 = "D";
        String[] strings = {s4, s3, s2, s1};
        // 按照自然规则从小到大排序
        Arrays.sort(strings);
        System.out.println(Arrays.toString(strings));
    }
}

测试结果:

[A, B, C, D]

2. 比较器应用

2.1 直接调用compareTo方法

既然包装类和String内部实现了Comparable接口,我们就可以直接调用其compareTo方法来观察。

package a.compare;
import java.util.Arrays;

// java比较器
public class CompareMain {
    public static void main(String[] args) {
        Integer a = 100;
        Integer b = 19;
        // 因为排序规则就是从小到大,所以,100肯定要排在19后面,即100肯定要比19大。因此此处返回的结果肯定是正整数1
        System.out.println(a.compareTo(b));
    }
}

2.2 自定义大小规则

自定义比较规则:

package a.compare;
import java.util.Arrays;

// java比较器
public class CompareMain {
    public static void main(String[] args) {
        Student s1 = new Student("zhangsan", 66d);
        Student s2 = new Student("zhangsan", 89d);
        Student s3 = new Student("zhangsan", 98d);
        Student s4 = new Student("zhangsan", 103d);
        Student[] students = {s1, s2, s3, s4};
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
    }
}
class Student implements Comparable<Student> {
    private String name;
    private double score;
    public Student(String name, double score) {
        this.setName(name);
        this.setScore(score);
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getScore() {
        return score;
    }
    public void setScore(double score) {
        this.score = score;
    }
    public String toString() {
        return "name = " + this.getName() + ";score = " + this.getScore();
    }
    // 指定两个student 对象大小比较的规则。
    // 记住,排序规则是死的,即从小到大。
    @Override
    public int compareTo(Student o) {
        // 如果我想让成绩高的学生排序在前面的话,那么意味着当前学生成绩高的话,则当前学生对象应该比目标学生对象小(排序规则固定是从小到大)。
        // 所以,条件就好写了,即当前学生成绩如果大于目标学生成绩,则大小规则结果应该返回-1,表示当前学生对象比目标学生对象小。
        // 这样在排序时候才能将当前学生排序到目标学生前面,而当前学生的成绩高于目标学生,因此就实现了将成绩高的学生排列在前面,即按照成绩从大到小排序。
        if (this.getScore() > o.getScore()) {
            // -1表示当前学生排序在目标学生前面
            return -1;
        } else if (this.getScore() < o.getScore()) {
            // 同理,如果当前学生成绩小于目标学生,则当前学生应该排列在后面,意味着当前学生应该比目标学生对象大。
            // 因此,此处只管返回正整数1即可。
            return 1;
        } else {
            return 0;
        }
    }
}

测试结果:

[name = zhangsan;score = 103.0, name = zhangsan;score = 98.0, name = zhangsan;score = 89.0, name = zhangsan;score = 66.0]

2.3 比较器的使用场景

前面已经反复说过,比较器的使用场景只有三个地方:

  1. Arrays.sort()方法;
  2. Collections.sort()方法;
  3. TreeSet或者TreeMap系列。

下面通过案例来展示下:
比较器使用场景总结:

package a.compare;
import java.util.*;

// java比较器
public class CompareMain {
    public static void main(String[] args) {
        // 搞几个自定义比较器的javabean
        Student s1 = new Student("zhangsan", 66d);
        Student s2 = new Student("zhangsan", 89d);
        Student s3 = new Student("zhangsan", 98d);
        Student s4 = new Student("zhangsan", 103d);
        // 场景1:Arrays.sort()使用场景
        Student[] students = {s1, s2, s3, s4};
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
        // 场景2:Collections.sort()使用场景
        List<Student> studentList = new ArrayList<>();
        studentList.add(s1);
        studentList.add(s2);
        studentList.add(s3);
        studentList.add(s4);
        Collections.sort(studentList);
        System.out.println(studentList);
        // 场景3:tree系列使用场景
        Set<Student> treeSet = new TreeSet<>();
        treeSet.add(s1);
        treeSet.add(s2);
        treeSet.add(s3);
        treeSet.add(s4);
        System.out.println(treeSet);
    }
}
class Student implements Comparable<Student> {
    private String name;
    private double score;
    public Student(String name, double score) {
        this.setName(name);
        this.setScore(score);
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getScore() {
        return score;
    }
    public void setScore(double score) {
        this.score = score;
    }
    public String toString() {
        return "name = " + this.getName() + ";score = " + this.getScore();
    }
    @Override
    public int compareTo(Student o) {
        if (this.getScore() > o.getScore()) {
            return -1;
        } else if (this.getScore() < o.getScore()) {
            return 1;
        } else {
            return 0;
        }
    }
}

测试结果:

[name = zhangsan;score = 103.0, name = zhangsan;score = 98.0, name = zhangsan;score = 89.0, name = zhangsan;score = 66.0]
[name = zhangsan;score = 103.0, name = zhangsan;score = 98.0, name = zhangsan;score = 89.0, name = zhangsan;score = 66.0]
[name = zhangsan;score = 103.0, name = zhangsan;score = 98.0, name = zhangsan;score = 89.0, name = zhangsan;score = 66.0]

3. 深入比较器原理

3.1 Comparable接口指定大小规则

Comparable接口和Comparator接口都是比较器接口,二者原理完全相同。
内容提供了一个方法,compareTo()方法。
该方法需要开发者去实现,指定两个java对象进行大小比较的规则,即:
什么场景下,返回负整数,表示当前对象小于目标对象;
什么场景下,返回正整数,表示当前对象大于目标对象;
什么场景下,返回0,表示当前对象等于目标对象。

3.2 排序规则的底层算法

已经讲过,单纯实现Comparable接口的compareTo方法没有任何意义。需要结合其使用处。
即结合sort()方法和tree系列数据结构。
Arrays.sort()方法、Collections.sort()方法和treeSet等底层的排序原理是二叉树排序算法和中序遍历。

下面实现一个二叉树中序遍历排序算法,来模拟sort()方法的底层原理!

package javase.demo18.classlibray.comparable;

// 学习比较器-基于Comparable 接口
// 分析Comparable接口实现比较器的排序原理
// 基于二叉树排序方法,通过二叉树进行排序,然后利用中序遍历的方式将内容一次读取出来。
// 二叉树遍历分为三种:前序、中序、后序,其中序遍历最为重要。为啥叫这个名字?是根据根节点的顺序命名的。
// 根节点遍历时放在中间就是中序遍历,放在前面就是前序遍历,放在后边就是后序遍历。
// 所以,前序、中序、后序指的是遍历二叉树的顺序方式。
// 注意对于叶子节点都是先左后右进行遍历。
// 二叉树排序的基本原理就是:
// 将集合中的第一个元素作为根节点保存;
// 如果后面的值比根节点的小(此处不容易理解,应该是后面的值采用了自己定义的比较规则返回结果为-1的就放在左子树;
// 返回为0或者1的就放在右子树),则放在根节点的左子树;
// 如果后面的值比根节点的值大, 则放在根节点的右子树。
// 然后按照中序遍历原理,即从左子树->根节点->右子树的方式,即可将元素排序读取出来。
// 下面使用Integer类来实现一个简单的二叉树排序算法,之所以使用Integer类,是因为Integer类已经实现了Comparable接口。
// 本案例模拟的其实就是Arrays.sort()和Collections.sort()的操作;
// 让开发者明白sort()的底层就是通过二叉树进行排序的且为什么需要实现Comparable接口
public class StudyComparableYuanLi {
    public static void main(String args[]) {
        // 验证Integer类实现了Comparable接口
        
        // 声明一个Comparable接口对象
        Comparable<Integer> com = null;
        // 通过Integer类直接为Comparable接口对象实例化,说明了Integer类是Comparable接口的实现类,可以为父类实例化
        com = 30;
        // 输出Comparable接口的实例化对象,实际上调用的是Integer类的toString()方法
        System.out.println("内容为:" + com);
       
        // 调用自己实现了二叉树算法
        BinaryTree bt = new BinaryTree();
        bt.add(8);
        bt.add(3);
        // 加入重复元素
        bt.add(3);
        bt.add(10);
        bt.add(9);
        bt.add(1);
        bt.add(5);
        // 加入重复元素
        bt.add(5);
        System.out.println("排序之后的结果:");
        bt.print();
    }
}

// 定义二叉树算法类
class BinaryTree {
    
    // 声明一个节点类,内部类
    class Node {
        @SuppressWarnings("rawtypes")
        // 保存具体的内容
        private Comparable data;
        // 保存左子树
        private Node left;
        // 保存右子树
        private Node right;
       
        // 增加新节点,递归调用
        @SuppressWarnings("unchecked")
        public void addNode(Node newNode) {
            // 要确定是放在左子树还是右子树
            if (newNode.data.compareTo(this.data) < 0) {
                // 放在左子树
                if (this.left == null) {
                    this.left = newNode;
                } else {
                    this.left.addNode(newNode);
                }
            }
            if (newNode.data.compareTo(this.data) >= 0) {
                // 放在右子树
                if (this.right == null) {
                    this.right = newNode;
                } else {
                    this.right.addNode(newNode);
                }
            }
        }
        
        // 采用中序遍历输出,递归调用
        public void printNode() {
            if (this.left != null) {
                // 先输出左子树
                this.left.printNode();
            }
            // 再输出根节点
            System.out.print(this.data + "\t");
            if (this.right != null) {
                // 最后输出右子树
                this.right.printNode();
            }
        }
    }
    
    // 根节点
    private Node root;
    @SuppressWarnings("rawtypes")
    public void add(Comparable data) {
        // 每次传入一个新的内容就声明一个根节点
        Node newNode = new Node();
        newNode.data = data;
        if (root == null) {
            // 如果是第一个元素,就设置为根元素
            root = newNode;
        } else {
            // 确定节点是放在左子树还是右子树
            root.addNode(newNode);
        }
    }
    
    // 输出节点
    public void print() {
        this.root.printNode();
    }
}

4. 补救式比较器-Comparator接口

4.1 Comparator比较器使用方式

Comparator接口和Comparable接口的底层原理一模一样,唯一不同的就是使用方式。
Comparable接口一般是在设计类之初,就使类实现该接口,指定大小比较规则;这样在后续遇到Arrays.sort()、Collections.sort()或者treeSet系列时,就可以按照自定义的大小规则,根据二叉树排序原理从小到大排序。
那,试想一下,如果一个List中的javabean已经都上线了,现在不想改动这个javabean,但是又想对该list进行sort处理,这时候怎么办呢?
jdk不仅提供了Comparable这个一劳永逸的比较器接口,也提供了临时性的补救式的比较器接口,即Comparator接口。
这个接口不要求javabean实现其,只需要在使用Arrays.sort()、Collections.sort()或者treeSet系列时传入一个该接口的实现对象即可。
最常见的方式实际上是直接通过匿名内部类的方式去传入该接口的对象,或者采用lambda表达式的方式传入即可。

4.2 Comparator比较器案例

Comparator比较器:

package zeh.test.demo.com.compare;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

public class CompareMain {
    public static void main(String[] args) {
        // 实例化javaBean
        Person person1 = new Person("zhangsan", 22);
        Person person2 = new Person("lisi", 25);
        Person person3 = new Person("wangwu", 46);
        // 下面使用Comparator比较器进行大小比较的补救
        // 方式1:Arrays.sort()
        Person[] people = new Person[]{person1, person2, person3};
        Arrays.sort(people, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                if (o1.getAge() > o2.getAge()) {
                    return -1;
                } else if (o1.getAge() < o2.getAge()) {
                    return 1;
                } else {
                    return 0;
                }
            }
        });
        System.out.println(Arrays.toString(people));
        // 方式2:Collections.sort()
        List<Person> personList = new ArrayList<>();
        personList.add(person1);
        personList.add(person2);
        personList.add(person3);
        Collections.sort(personList, new <Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                if (o1.getAge() > o2.getAge()) {
                    return -1;
                } else if (o1.getAge() < o2.getAge()) {
                    return 1;
                } else {
                    return 0;
                }
            }
        });
        System.out.println(personList);
        // 方式3:TreeSet
        Set<Person> personSet = new TreeSet<>(new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                if (o1.getAge() > o2.getAge()) {
                    return -1;
                } else if (o1.getAge() < o2.getAge()) {
                    return 1;
                } else {
                    return 0;
                }
            }
        });
        personSet.add(person1);
        personSet.add(person2);
        personSet.add(person3);
        System.out.println(personSet);
    }
}
class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.setName(name);
        this.setAge(age);
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String toString() {
        return "name = " + this.getName() + ";age = " + this.getAge();
    }
}

测试结果:

[name = wangwu;age = 46, name = lisi;age = 25, name = zhangsan;age = 22]
[name = wangwu;age = 46, name = lisi;age = 25, name = zhangsan;age = 22]
[name = wangwu;age = 46, name = lisi;age = 25, name = zhangsan;age = 22]

文档信息

Search

    Table of Contents