Java中自然排序和比较器排序详解

前言

当指执行插入排序、希尔排序、归并排序等算法时,比较两个对象“大小”的比较操作。我们很容易理解整型的 i>j 这样的比较方式,但当我们对多个对象进行排序时,如何比较两个对象的“大小”呢?这样的比较 stu1 > stu2 显然是不可能通过编译的。为了解决如何比较两个对象大小的问题,JDK提供了两个接口 java.lang.Comparable java.util.Comparator

一、自然排序:java.lang.Comparable

Comparable 接口中只提供了一个方法: compareTo(Object obj) ,该方法的返回值是 int 。如果返回值为正数,则表示当前对象(调用该方法的对象)比 obj 对象“大”;反之“小”;如果为零的话,则表示两对象相等。

下面是一个实现了 Comparable 接口的 Student 类:

public class Student implements Comparable { 

 private int id; 

 private String name; 

 public Student() {
  super();
 } 

 @Override
 public int compareTo(Object obj) {
  if (obj instanceof Student) {
   Student stu = (Student) obj;
   return id - stu.id;
  }
  return 0;
 } 

 @Override
 public String toString() {
  return "<" + id + ", " + name + ">";
 }
} 

Student 实现了自然排序接口 Comparable ,那么我们是怎么利用这个接口对一组 Student 对象进行排序的呢?我们在学习数组的时候,使用了一个类来给整型数组排序: java.util.Arrays 。我们使用 Arrays 的 sort 方法来给整型数组排序。翻翻 API 文档就会发现, Arrays 里给出了 sort 方法很多重载形式,其中就包括 sort(Object[] obj) ,也就是说 Arryas 也能对对象数组进行排序,排序过程中比较两个对象“大小”时使用的就是 Comparable 接口的 compareTo 方法。

public class CompareTest { 

 public static void main(String[] args) {
  Student stu1 = new Student(1, "Little");
  Student stu2 = new Student(2, "Cyntin");
  Student stu3 = new Student(3, "Tony");
  Student stu4 = new Student(4, "Gemini"); 

  Student[] stus = new Student[4];
  stus[0] = stu1;
  stus[1] = stu4;
  stus[2] = stu3;
  stus[3] = stu2;
  System.out.println(“Array: ” + Arrays.toString(stus));
  Arrays.sort(stus);
  System.out.println(“Sort: ” + Arrays.toString(stus));
 }
} 

Student 数组里添加元素的顺序并不是按学号 id 来添加的。调用了 Arrays.sort(stus) 之后,对 Student 数组进行排序,不管 sort 是使用哪种排序算法来实现的,比较两个对象“大小”这个操作,它是肯定要做的。那么如何比较两个对象的“大小”? Student 实现的 Comparable 接口就发挥作用了。 sort 方法会将待比较的那个对象强制类型转换成 Comparable ,并调用 compareTo 方法,根据其返回值来判断这两个对象的“大小”。所以,在这个例子中排序后的原 Student 乱序数组就变成了按学号排序的 Student 数组。

但是我们注意到,排序算法和 Student 类绑定了, Student 只有一种排序算法。但现实社会不是这样的,如果我们不想按学号排序怎么办?假如,我们想按姓名来给学生排序怎么办?我们只能修改 Student 类的 Comparable 接口的 compareTo 方法,改成按姓名排序。如果在同一个系统里有两个操作,一个是按学号排序,另外一个是按姓名排序,这怎么办?不可能在 Student 类体中写两个 compareTo 方法的实现。这么看来Comparable就有局限性了。为了弥补这个不足,JDK 还为我们提供了另外一个排序方式,也就是下面要说的比较器排序。

二、比较器排序:java.util.Comparator

上面我提到了,之所以提供比较器排序接口,是因为有时需要对同一对象进行多种不同方式的排序,这点自然排序 Comparable 不能实现。另外, Comparator 接口的一个好处是将比较排序算法和具体的实体类分离了。

翻翻 API 会发现, Arrays.sort 还有种重载形式:sort(T[] a, Comparator<? super T> c) ,这个方法参数的写法用到了泛型,我们还没讲到。我们可以把它理解成这样的形式: sort(Object[] a, Comparator c) ,这个方法的意思是按照比较器 c 给出的比较排序算法,对 Object 数组进行排序。Comparator 接口中定义了两个方法: compare(Object o1, Object o2) equals 方法,由于 equals 方法所有对象都有的方法,因此当我们实现 Comparator 接口时,我们只需重写 compare 方法,而不需重写 equals 方法。Comparator 接口中对重写 equals 方法的描述是:“注意,不重写 Object.equals(Object) 方法总是安全的。然而,在某些情况下,重写此方法可以允许程序确定两个不同的 Comparator 是否强行实施了相同的排序,从而提高性能。”。我们只需知道第一句话就OK了,也就是说,可以不用去想应该怎么实现 equals 方法,因为即使我们不显示实现 equals 方法,而是使用Object类的 equals 方法,代码依然是安全的。

那么我们来写个代码,来用一用比较器排序。还是用 Student 类来做,只是没有实现 Comparable 接口。由于比较器的实现类只用显示实现一个方法,因此,我们可以不用专门写一个类来实现它,当我们需要用到比较器时,可以写个匿名内部类来实现 Comparator 。

下面是我们的按姓名排序的方法:

public void sortByName () {
 Student stu1 = new Student(1, "Little");
 Student stu2 = new Student(2, "Cyntin");
 Student stu3 = new Student(3, "Tony");
 Student stu4 = new Student(4, "Gemini"); 

 Student[] stus = new Student[4];
 stus[0] = stu1;
 stus[1] = stu4;
 stus[2] = stu3;
 stus[3] = stu2;
 System.out.println("Array: " + Arrays.toString(stus)); 

 Arrays.sort(stus, new Comparator() { 

  @Override
  public int compare(Object o1, Object o2) {
   if (o1 instanceof Student && o2 instanceof Student) {
    Student s1 = (Student) o1;
    Student s2 = (Student) o2;
    //return s1.getId() - s2.getId(); // 按Id排
    return s1.getName().compareTo(s2.getName()); // 按姓名排
   }
   return 0;
  } 

 }); 

 System.out.println("Sorted: " + Arrays.toString(stus));
} 

当我们需要对Student按学号排序时,只需修改我们的排序方法中实现Comparator的内部类中的代码,而不用修改 Student 类。

注意: 当然,你也可以用 Student 类实现 Comparator 接口,这样Student就是(is a)比较器了(Comparator)。当需要使用这种排序的时候,将 Student 看作 Comparator 来使用就可以了,可以将 Student 作为参数传入 sort 方法,因为 Student is a Comparator 。但这样的代码不是个优秀的代码,因为我们之所以使用比较器(Comparator),其中有个重要的原因就是,这样可以把比较算法和具体类分离,降低类之间的耦合。

TreeSet对这两种比较方式都提供了支持,分别对应着TreeSet的两个构造方法:

     1、TreeSet():根据TreeSet中元素实现的 Comparable 接口的 compareTo 方法比较排序

   2、TreeSet(Comparator comparator):根据给定的 comparator 比较器,对 TreeSet 中的元素比较排序

当向 TreeSet 中添加元素时,TreeSet 就会对元素进行排序。至于是用自然排序还是用比较器排序,就看你的 TreeSet 构造是怎么写的了。当然,添加第一个元素时不会进行任何比较, TreeSet 中都没有元素,和谁比去啊?

下面,分别给出使用两种排序比较方式的 TreeSet 测试代码:

/**
 * 使用自然排序
 * Student必须实现Comparable接口,否则会抛出ClassCastException
 */
public void testSortedSet3() {
 Student stu1 = new Student(1, "Little");
 Student stu2 = new Student(2, "Cyntin");
 Student stu3 = new Student(3, "Tony");
 Student stu4 = new Student(4, "Gemini"); 

 SortedSet set = new TreeSet();
 set.add(stu1);
 set.add(stu3); // 若Student没有实现Comparable接口,抛出ClassCastException
 set.add(stu4);
 set.add(stu2);
 set.add(stu4);
 set.add(new Student(12, "Little")); 

 System.out.println(set);
} 
/**
 * 使用比较器排序
 * Student可以只是个简单的Java类,不用实现Comparable接口
 */
public void testSortedSet3() {
 Student stu1 = new Student(1, "Little");
 Student stu2 = new Student(2, "Cyntin");
 Student stu3 = new Student(3, "Tony");
 Student stu4 = new Student(4, "Gemini"); 

 SortedSet set = new TreeSet(new Comparator() { 

  @Override
  public int compare(Object o1, Object o2) {
   if (o1 instanceof Student
     && o2 instanceof Student) {
    Student s1 = (Student) o1;
    Student s2 = (Student) o2;
    return s1.getName().compareTo(s2.getName());
   }
   return 0;
  } 

 }); 

 set.add(stu1);
 set.add(stu3);
 set.add(stu4);
 set.add(stu2);
 set.add(stu4);
 set.add(new Student(12, "Little")); 

 System.out.println(set);
} 

另外,介绍个工具类,java.util.Collections。注意,这不是Collection接口。Collections很像Arrays类。Arrays提供了一系列用于对数组操作的静态方法,查找排序等等。Collections也提供了一系列这样的方法,只是它是用于处理集合的,虽然Collections类和Collection接口很像,但是不要被Collections的名字给欺骗了,它不是只能处理Collection接口以及子接口的实现类,同样也可以处理Map接口的实现类。

总结

Java中自然排序和比较器排序的介绍就到这里了,文章介绍的还是相对详细的,希望能对大家的学习或者工作带来一定的帮助,如果有疑问大家可以留言交流。

(0)

相关推荐

  • 浅谈Java中几种常见的比较器的实现方法

    在Java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题. 通常对象之间的比较可以从两个方面去看: 第一个方面:对象的地址是否一样,也就是是否引用自同一个对象.这种方式可以直接使用"=="来完成. 第二个方面:以对象的某一个属性的角度去比较. 从最新的JDK8而言,有三种实现对象比较的方法: 一.覆写Object类的equals()方法: 二.继承Comparable接口,并实现compareTo()方法: 三.定义一个单独的对象比较器,继承自Comparator接口

  • 对比Java中的Comparable排序接口和Comparator比较器接口

    Comparable Comparable 是排序接口. 若一个类实现了Comparable接口,就意味着"该类支持排序". 即然实现Comparable接口的类支持排序,假设现在存在"实现Comparable接口的类的对象的List列表(或数组)",则该List列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序. 此外,"实现Comparable接口的类的对象"可以用作"有序映射(如Tree

  • java比较器comparator使用示例分享

    复制代码 代码如下: import java.util.ArrayList;import java.util.Collections;import java.util.Comparator;import java.util.List; public class ComparatorTest implements Comparator<stuEntity> { /**     * @param args     */    public static void main(String[] arg

  • java比较器Comparable接口与Comaprator接口的深入分析

    java的比较器有两类,分别是Comparable接口和Comparator接口.在为对象数组进行排序时,比较器的作用非常明显,首先来讲解Comparable接口.让需要进行排序的对象实现Comparable接口,重写其中的compareTo(T o)方法,在其中定义排序规则,那么就可以直接调用java.util.Arrays.sort()来排序对象数组,实例如下: 复制代码 代码如下: class Student implements Comparable<Student>{    priv

  • java 中maven pom.xml文件教程详解

    maven pom.xml文件教程详解,具体内容如下所示: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.x

  • Java中内存异常StackOverflowError与OutOfMemoryError详解

     Java中内存异常StackOverflowError与OutOfMemoryError详解 使用Java开发,经常回遇到内存异常的情况,而StackOverflowError和OutOfMemoryError便是最常遇见的错误. 首先,看看这两种错误的解释: 如果当前线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常. 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常. 这里把异常分为两种情况,但是存在一些相互重

  • java中Executor,ExecutorService,ThreadPoolExecutor详解

    java中Executor,ExecutorService,ThreadPoolExecutor详解 1.Excutor 源码非常简单,只有一个execute(Runnable command)回调接口 public interface Executor { /** * Executes the given command at some time in the future. The command * may execute in a new thread, in a pooled thre

  • java 中HttpClient传输xml字符串实例详解

    java 中HttpClient传输xml字符串实例详解 介绍:我现在有一个对象page,需要将page对象转换为xml格式并以binary方式传输到服务端 其中涉及到的技术点有: 1.对象转xml流 2.输出流转输入流 3.httpClient发送二进制流数据 POM文件依赖配置 <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifact

  • java 中JDBC连接数据库代码和步骤详解及实例代码

    java 中JDBC连接数据库代码和步骤详解 JDBC连接数据库 •创建一个以JDBC连接数据库的程序,包含7个步骤:  1.加载JDBC驱动程序:  在连接数据库之前,首先要加载想要连接的数据库的驱动到JVM(Java虚拟机),这通过java.lang.Class类的静态方法forName(String  className)实现. 例如: try{ //加载MySql的驱动类 Class.forName("com.mysql.jdbc.Driver") ; }catch(Class

  • java 中模式匹配算法-KMP算法实例详解

    java 中模式匹配算法-KMP算法实例详解 朴素模式匹配算法的最大问题就是太低效了.于是三位前辈发表了一种KMP算法,其中三个字母分别是这三个人名的首字母大写. 简单的说,KMP算法的对于主串的当前位置不回溯.也就是说,如果主串某次比较时,当前下标为i,i之前的字符和子串对应的字符匹配,那么不要再像朴素算法那样将主串的下标回溯,比如主串为"abcababcabcabcabcabc",子串为"abcabx".第一次匹配的时候,主串1,2,3,4,5字符都和子串相应的

  • java中的static{}块的实例详解

    java中的static{}块的实例详解 一直以来对static块不是很熟系,今天特意写了两个程序来搞清楚一下: 第一个小程序: package com.babyDuncan.Sohu; public class testStatic { static { int x = 5; } static int x, y; public static void main(String[] args) { x--; myMethod(); System.out.println(x + y + ++x);

  • java中注解机制及其原理的详解

    java中注解机制及其原理的详解 什么是注解 注解也叫元数据,例如我们常见的@Override和@Deprecated,注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包.类.接口.字段.方法参数.局部变量等进行注解.它主要的作用有以下四方面: 生成文档,通过代码里标识的元数据生成javadoc文档. 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证. 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码. 运行时动态处理,运行时通过代码里标识

  • 基于Java中进制的转换函数详解

    十进制转成十六进制: Integer.toHexString(int i) 十进制转成八进制 Integer.toOctalString(int i) 十进制转成二进制 Integer.toBinaryString(int i) 十六进制转成十进制 Integer.valueOf("FFFF",16).toString() 八进制转成十进制 Integer.valueOf("876",8).toString() 二进制转十进制 Integer.valueOf(&qu

  • Java中Properties类的操作实例详解

    Java中Properties类的操作实例详解 知识学而不用,就等于没用,到真正用到的时候还得重新再学.最近在看几款开源模拟器的源码,里面涉及到了很多关于Properties类的引用,由于Java已经好久没用了,而这些模拟器大多用Java来写,外加一些脚本语言Python,Perl之类的,不得已,又得重新拾起.本文通过看<Java编程思想>和一些网友的博客总结而来,只为简单介绍Properties类的相关操作.  一.Java Properties类 Java中有个比较重要的类Properti

随机推荐