java的两种比较器

比较算法

日常生活中,如果想比较两个数的大小,可采用做差的方式,做差结果的正负可用来判断两个数的大小。假设A - B = C

  • 若整数C > 0,说明 A > B ;
  • 若整数C = 0,说明 A = B;
  • 若整数C < 0,说明 A < B;

java的两种比较器均基于以上判断逻辑,将两个待比较的Object经过某种处理,返回一个整数值,然后根据整个整数值的正负判断大小。类似地,自定义实现比较器时,也是同样道理,经过逻辑处理之后,返回一个整数。

内部比较器(基于Comparable接口)

当某一个业务类A需要具备可比较的特点时,直接实现Comparable接口,重写compareTo(T o)方法,意味着类A支持排序。对于存储类A的列表或数组,就可以使用Collections.sort(List l)或Arrays.sort(E[] es)进行排序。

使用这种方式实现的比较器的比较逻辑代码在业务类内部,因此也被称为内部比较器。java的基本数据类型均采用这种方式。

  • Comparable接口源码

    /**
     * @since 1.2
     */
    public interface Comparable<T> {
        public int compareTo(T o);
    }
    
  • Student类

    业务类直接继承Comparabe接口,实现compareTo方法

    /**
     * @description: 内部比较器
     * @Date: 2021/10/20 20:41
     */
    public class Student implements Comparable<Student> {
        private int age;
        private double height;
        
        ...省略geter setter...
            
        public Student( int age,double height) {
            this.age = age;
            this.height = height;
        }
        /**
         * 通过年龄排序
         * @param o
         * @return
         */
        @Override
        public int compareTo(Student o) {
            //按照年龄升序排序,int不需要转化
            //return this.age - o.getAge();
            
            //按照身高排序, double类型需要转化。
            //用到了基本数据类型的比较器
            return ((Double)(this.height)).compareTo((Double)(o.getHeight())) ;
        }
    
    
        public static void main(String[] args) {
            
            Student[] ss = new Student[5];
            ss[0] = new Student(15,1.9);
            ss[1] = new Student(12,1.8);
            ss[2] = new Student(10,1.7);
            ss[3] = new Student(16,1.6);
            ss[4] = new Student(13,2.1);
            //工具类对数组排序
            Arrays.sort(ss);
    
            for (int i = 0; i < 5; i++) {
                System.out.println(ss[i].getHeight());
            }
        }
    }
    
  • 测试代码

    	//两个学生比身高
    	public static void compare1() {
            
            Student s1 = new Student(16, 1.8);
            Student s2 = new Student(16, 1.67);
            System.out.println(s1.compareTo(s2)); // 1
    	}
    
     	//对学生集合按照身高排序
    	public static void compare1() {
            Student[] ss = new Student[5];
            ss[0] = new Student(15,1.9);
            ss[1] = new Student(12,1.8);
            ss[2] = new Student(10,1.7);
            ss[3] = new Student(16,1.6);
            ss[4] = new Student(13,2.1);
    
            //工具类对数组排序
            Arrays.sort(ss);
    
            for (int i = 0; i < 5; i++) {
                System.out.println(ss[i].getHeight());// 升序 1.6 1.7 1.8 1.9 2.1
            }
    	}
    

外部比较器(基于Comparator接口)

在内部比较器中,比较逻辑的实现代码在业务类的内部。当比较逻辑发生变化时,就必须修改业务类的代码,这不符合设计模式的开闭原则。此时可使用基于策略模式的外部比较器。

其方式是新建一个实现Comparator接口的具体比较器类,并将比较逻辑写入int compare(T o1, T o2)方法。同内部比较器类似,外部比较器同样是对比较算法的实现,因此int compare(T o1, T o2)的返回值同样是整数,这就要求对于业务类的比较项做差不为整数的情况,需要将其差值转换为整数。

  • 业务类Student2

    对比Student的代码,可以发现Student2中只包含了业务自身所需要的代码,没有用于实现比较逻辑的代码

    /**
     * @description: 待比较的业务类
     * @Date: 2021/10/22 10:02
     */
    public class Student2 {
        private double height;
        private int age;
    
      ....省略getter setter....
    
        public Student2( int age,double height) {
            this.age = age;
            this.height = height;
        }
    }
    
  • 外部比较器1
    根据业务比较逻辑的设计,创建一个对应的比较器类,该比较器类独立于业务类,两者不存在耦合关系。
    该比较器按照年龄比较Student2的”大小“,此处年龄是int型,其差值仍未int型,因此不必转换。

    /**
     * @description: 外部比较器1
     * 按照年龄比较
     * @Date: 2021/10/22 10:06
     */
    public class Student2Compartor1 implements Comparator<Student2> {
        @Override
        public int compare(Student2 o1, Student2 o2) {
            return o1.getAge() - o2.getAge();
        }
    }
    
  • 外部比较器2
    该比较器按照身高比较Student2的”大小“,此处身高是double型,其差值是double型,因此需要转换。

    /**
     * @description: 外部比较器2
     * 按照身高比较
     * @Date: 2021/10/22 10:07
     */
    public class Student2Comparator2 implements Comparator<Student2> {
        @Override
        public int compare(Student2 o1, Student2 o2) {
            //不推荐使用compareTo方法,compareTo底层仍是调用了compare方法
            // return ((Double)(o1.getHeight())).compareTo(((Double)(o2.getHeight())));
            
            //将两double的差值转换为int
            return Double.compare(o1.getHeight(),o2.getHeight());
        }
    }
    
  • 测试代码
    创建不同的比较器实例,然后可以进行两个对象的比较,也可进行集合排序

    public static void comparator1Test(){
            //父类引用指向子类对象
            //比较器1 按年龄比较
            Comparator<Student2> sc1 = new Student2Compartor1();
            //比较器2 按身高比较
            Comparator<Student2> sc2 = new Student2Comparator2();
    
        	//比较两个Student2
            Student2 s1 = new Student2(11, 1.7);
            Student2 s2 = new Student2(12, 1.6);
            System.out.println(sc1.compare(s1, s2)); //-1 按年龄 s1 < s2
            System.out.println(sc2.compare(s1, s2));// 1 按身高 s1 > s2
    
        	//对数组排序
            Student2[] ss = new Student2[5];
            ss[0] = new Student2(15,1.9);
            ss[1] = new Student2(12,1.8);
            ss[2] = new Student2(10,1.7);
            ss[3] = new Student2(16,1.6);
            ss[4] = new Student2(13,2.1);
    
            //工具类对数组排序,传入一个集合和对应的比较器
    
            //传入比较器1 按年龄排序
            Arrays.sort(ss,sc1);
            for (int i = 0; i < 5; i++) {
                System.out.println(ss[i].getAge());//升序 10 12 13 15 16
            }
    
            //传入比较器2 按身高排序
            Arrays.sort(ss,sc2);
            for (int i = 0; i < 5; i++) {
                System.out.println(ss[i].getHeight());//升序 1.6 1.7 1.8 1.9 2.1
            }
        }
    

外部比较器与策略模式

在测试代码中可以看到,Student2的集合ss即可以接收按年龄排序的比较器sc1,也可以接收按身高排序的比较器sc2。查看其方法签名public static <T> void sort(T[] a, Comparator<? super T> c),可以发现第二形参是外部比较器的基类。

在实际编码实践中,用户基于不同的比较逻辑,创建不同的外部比较器,这些比较器出现的位置完全可以互换,这实际上就是策略模式的使用。外部比较器基于策略模式组合业务类和比较器类,避免了内部比较器因使用继承造成了耦合,便于修改和扩展。其中,

  • 业务类Student2 对应 策略模式中的环境角色
  • 外部比较器基类Comparator 对应 策略模式中的抽象策略角色
  • 各种自定义的外部比较器(Student2Comparator1、Student2Comparator2)对应 策略模式中的具体策略类

两种外部比较器使用方式

  • 新建外部类

    这种方式是显式创建一个独立的外部比较器,使用时创建对应的比较器实例,如上文的测试代码描述。

  • 匿名内部类

    在外部比较器时,一般只需实现Comparator的核心方法compare,因此在创建比较器时,可以使用匿名内部类。

    //给排序数组传入一个匿名内部类的比较器
    Arrays.sort(ss, new Comparator<Student2>() {
                @Override
                public int compare(Student2 o1, Student2 o2) {
                    return o1.getAge() - o2.getAge();
                }
            };);
    

做差结果与升学或降序的对应关系

不管是jdk自带比较器还是自定义比较器,使对应类具备可比较的特性之后,多用于对类的集合进行排序。其中,排序结果和做差的顺序有关。

  • 在内部比较器中,待比较双方是 1)当前所在类的this实例 ;2)外部传入的同类型实例,即int compareTo(T o)方法的参数o

  • 在外部比较器中,待比较双方是int compare(T o1, T o2)的两个参数o1、o2;

为方便记忆,将内部比较器的this 和外部比较器的o1作为基准,如果他们作为被减数,即位于减号左边,那排序结果为升序排序,否则为降序排序。

  • 升学排序

    //内部比较器 基准作为被减数
    public int compareTo(Test o) {
        return this.i - o.i;
    }
    
    //外部比较器 o1作为被减数
     public int compare(int o1, int o2) {
         return o1 - o2;
    }
    
  • 降序排序

    //内部比较器 this作为减数
    public int compareTo(Test o) {
        return  o.i - this.i ;
    }
    
    //外部比较器 o1作为减数
     public int compare(int o1, int o2) {
         return o2 - o1;
    }
    

基本数据类型的比较器

上文描述的两种比较器都是基于类实现,因此对于基本数据类型,需要先将其转换为对应的包装类,然后使用对应的比较器。JDK中默认会将基本数据类型装箱,然后使用升序逻辑的比较算法。

  • int类型

    由于两个int类型数值的差值已经是一个整数,不必再进行转换,因此其比较大小的实现不必使用上文的两种比较器,而是直接做差即可.

    public int compareTest(int a, int b){
        return a - b;
    }
    
  • String类型

    字符串不是数字,不能直接做差返回一个整数,因此需要借助比较器实现字符串的比较。jdk默认使用内部比较器,即实现Comparable接口。其比较逻辑基于字符串的字符的Unicode值,按照顺序逐个对比字符串的字符unicode值大小。以下为String类的比较逻辑实现

    //    anotherString为待比较的字符串
    public int compareTo(String anotherString) {
        	//value为当前字符串对应的字符数组
            int len1 = value.length;
        	//获取另一个字符串的字符数组
            int len2 = anotherString.value.length;
        	//获取较短字符串的长度
            int lim = Math.min(len1, len2);
        	
        	//另存字符数组引用
            char v1[] = value;
        	//另存字符数组引用
            char v2[] = anotherString.value;
    		
        	//遍历两个字符数组
        	//由于字符和int可以互换,因此将两个字符做差等同于int型做差,
        	//返回差值
            int k = 0;
            while (k < lim) {
                char c1 = v1[k];
                char c2 = v2[k];
                if (c1 != c2) {
                    //使用this作为被减数,因此排序是升序
                    return c1 - c2;
                }
                k++;
            }
        	//若已遍历完较短的字符串后,仍无法判断大小,则根据长度判断,较长的更大。
        	//使用this作为被减数,因此排序是升序
            return len1 - len2;
        }
    
    
    	/**
         * String内部比较器 
         *this作为被减数,若排序,则结果为升序
         */
     static void stringTest(){
          	String a = "aaaa";
            String b = "bbbb";
            String d = "dddd";
         
         	//两个字符串比较
            System.out.println(a.compareTo(b)); //-1. 底层执行'a' - 'b',等价于97 - 98
            System.out.println(a.compareTo(d));//-3. 底层执行‘a’ - 'd',等价于97 - 100
         	
         	//字符串集合排序
            String[] ss = new String[5];
            ss[0] = "abc";
            ss[1] = "xxxxx";
            ss[2] = "bcdeeee";
            ss[3] = "bc";
            ss[4] = "dfff";
            Arrays.sort(ss);
            for(String s : ss){
                System.out.println(s); // abc  bc bcdeeee  dfff xxxxx 
            }
        }
    
  • double类型

    两double数据做差,其结果仍是double类型。若将结果强制类型转换为int型,会丢失小数部分,其结果不再适用于判断算法。此时可以借助其包装类型Double。

    在排序时。jdk默认会为基本数据类型装箱,并使用升序排序。特别地,包装类提供了两个比较方法1)静态方法static int compare(double d1, double d2) ;2)重写comparable接口方法Double.compareTo(Double d)该方法底层仍是调用静态方法compare,在使用时建议使用compare()方法

     /**
         * 基本数据类型使用其对应的包装类,
         * jdk默认提供內部比较器,并自动装箱
         */
        static void doubleTest(){
            double a = 1.2;
            double b = 1.8;
            int c = (int)(a - b);
            //0.按照算法逻辑可得 a 等于 b,实际上是 a < b
            System.out.println(c);
            
            //两个doule比较
            //Double的内部比较器
            System.out.println(((Double) a).compareTo((Double) b));//-1
            //Double的静态方法
            System.out.println(Double.compare((Double) a, (Double) b));//-1
    
            //double集合排序
            double[] ds = new double[5];
            ds[0] = 1.7;
            ds[1] = 1.3;
            ds[2] = 1.9;
            ds[3] = 1.5;
            ds[4] = 1.6;
            Arrays.sort(ds);
            for(double d : ds){
                System.out.println(d);// 1.3 1.5 1.6 1.7 1.9
            }
        }
    
  • 其他基本数据类型同double类似

延申

  • 将double类型强转为Double类型,然后调用Double的方法,需要三个括号。((Double)(this.height))

参考资料

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值