认识Object中的几个经常需要覆盖的方法——考虑实现Comparable接口

学习Java少不了对Object的认知,所有类都会继承它的属性,真正的超类。这一个系列,我会对Object中的几个方法,也就是我们自定义类的时候需要重写的几个方法做一个介绍。下面是这一个系列的主要内容:

本系列内容源于对《Effective Java》中文第二版第8条到第12条的学习记录。所有内容的准确性均以原书为准。

 

 

1,引言

我们可能会想,这个Comparable接口Object类没有实现,我们自定义类的时候为什么要实现了。那么第一件事就是展示一个例子给大家看看我们实现了这个接口之后对于我们自定义的类有什么作用。

测试代码如下:

String str1 = "ab";
        String str2 = "bc";
        String str3 = "cd";

        String[] array1 = new String[3];
        array1[1] = str1;
        array1[2] = str2;
        array1[0] = str3;

        int i = 0;
        System.out.println("排序前:");
        while (i < 3) {
            System.out.print(array1[i] + ",");
            i++;
        }

        i = 0;
        Arrays.sort(array1);
        System.out.println("\n排序后:");
        while (i < 3) {
            System.out.print(array1[i] + ",");
            i++;
        }

测试结果如下:

排序前:
cd,ab,bc,
排序后:
ab,bc,cd,

认识Object中的几个经常需要覆盖的方法——考虑实现Comparable接口

 

上面使用数组工具类Arrays里面的sort方法对String元素进行了排序,由此我们想,我们怎么样才能让自己的类也能配合Arrays以及Collections工具类实现对我们的类的对象组成的数组以及集合进行排序了。这就是我们接下来要讨论的,自定义类时考虑实现Comparable接口。

 

2,分析

 

刚刚说到我们自己定义的类想和String一样,也可以配合数组工具类和集合工具类完成排序就需要实现Comparable接口,那我们就来看看String究竟有没有这样做:

java.lang
类 String

java.lang.Object
  java.lang.String

所有已实现的接口:

Serializable, CharSequence, Comparable<String>

 

可见。String类的确实现了该接口,那我们再转去看看Comparable接口究竟有什么:

 

接口 Comparable<T>

类型参数:

T - 可以与此对象进行比较的那些对象的类型

所有已知子接口:

Delayed, Name, RunnableScheduledFuture<V>, ScheduledFuture<V>

所有已知实现类:

Authenticator.RequestorType, BigDecimal, BigInteger, Boolean, Byte, ByteBuffer, Calendar, Character, CharBuffer, Charset, ClientInfoStatus, CollationKey, Component.BaselineResizeBehavior, CompositeName, CompoundName, Date, Date, Desktop.Action, Diagnostic.Kind, Dialog.ModalExclusionType, Dialog.ModalityType, Double, DoubleBuffer, DropMode, ElementKind, ElementType, Enum, File, Float, FloatBuffer, Formatter.BigDecimalLayoutForm, FormSubmitEvent.MethodType, GregorianCalendar, GroupLayout.Alignment, IntBuffer, Integer, JavaFileObject.Kind, JTable.PrintMode, KeyRep.Type, LayoutStyle.ComponentPlacement, LdapName, Long, LongBuffer, MappedByteBuffer, MemoryType, MessageContext.Scope, Modifier, MultipleGradientPaint.ColorSpaceType, MultipleGradientPaint.CycleMethod, NestingKind, Normalizer.Form, ObjectName, ObjectStreamField, Proxy.Type, Rdn, Resource.AuthenticationType, RetentionPolicy, RoundingMode, RowFilter.ComparisonType, RowIdLifetime, RowSorterEvent.Type, Service.Mode, Short, ShortBuffer, SOAPBinding.ParameterStyle, SOAPBinding.Style, SOAPBinding.Use, SortOrder, SourceVersion, SSLEngineResult.HandshakeStatus, SSLEngineResult.Status, StandardLocation, String, SwingWorker.StateValue, Thread.State, Time, Timestamp, TimeUnit, TrayIcon.MessageType, TypeKind, URI, UUID, WebParam.Mode, XmlAccessOrder, XmlAccessType, XmlNsForm


public interface Comparable<T>

此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法

实现此接口的对象列表(和数组)可以通过 Collections.sort(和 Arrays.sort)进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器

对于类 C 的每一个 e1 和 e2 来说,当且仅当 e1.compareTo(e2) == 0 与 e1.equals(e2) 具有相同的 boolean 值时,类 C 的自然排序才叫做与 equals 一致。注意,null 不是任何类的实例,即使 e.equals(null) 返回 false,e.compareTo(null) 也将抛出 NullPointerException。

建议(虽然不是必需的)最好使自然排序与 equals 一致。这是因为在使用自然排序与 equals 不一致的元素(或键)时,没有显式比较器的有序集合(和有序映射表)行为表现“怪异”。尤其是,这样的有序集合(或有序映射表)违背了根据 equals 方法定义的集合(或映射表)的常规协定。

例如,如果将两个键 a 和 b 添加到没有使用显式比较器的有序集合中,使 (!a.equals(b) && a.compareTo(b) == 0),那么第二个 add 操作将返回 false(有序集合的大小没有增加),因为从有序集合的角度来看,a 和 b 是相等的。

实际上,所有实现 Comparable 的 Java 核心类都具有与 equals 一致的自然排序。java.math.BigDecimal 是个例外,它的自然排序将值相等但精确度不同的 BigDecimal 对象(比如 4.0 和 4.00)视为相等。

从数学上讲,定义给定类 C 上自然排序的关系式 如下:

      {(x, y)|x.compareTo(y) <= 0}。

整体排序的 是:

      {(x, y)|x.compareTo(y) == 0}。

它直接遵循 compareTo 的协定,商是 C 的等价关系,自然排序是 C 的整体排序。当说到类的自然排序与 equals 一致 时,是指自然排序的商是由类的 equals(Object) 方法定义的等价关系。

    {(x, y)|x.equals(y)}。

此接口是 Java Collections Framework 的成员。

从以下版本开始:

1.2

另请参见:

Comparator


方法摘要
 int compareTo(T o)
          比较此对象与指定对象的顺序。

 

方法详细信息

compareTo

int compareTo(T o)

比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

实现类必须确保对于所有的 x 和 y 都存在 sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) 的关系。(这意味着如果 y.compareTo(x) 抛出一个异常,则 x.compareTo(y) 也要抛出一个异常。)

实现类还必须确保关系是可传递的:(x.compareTo(y)>0 && y.compareTo(z)>0) 意味着 x.compareTo(z)>0。

最后,实现者必须确保 x.compareTo(y)==0 意味着对于所有的 z,都存在 sgn(x.compareTo(z)) == sgn(y.compareTo(z))。 强烈推荐 (x.compareTo(y)==0) == (x.equals(y)) 这种做法,但并不是 严格要求这样做。一般来说,任何实现 Comparable 接口和违背此条件的类都应该清楚地指出这一事实。推荐如此阐述:“注意:此类具有与 equals 不一致的自然排序。”

在前面的描述中,符号 sgn(expression) 指定 signum 数学函数,该函数根据 expression 的值是负数、零还是正数,分别返回 -1、0 或 1 中的一个值。

参数:

o - 要比较的对象。

返回:

负整数、零或正整数,根据此对象是小于、等于还是大于指定对象。

抛出:

ClassCastException - 如果指定对象的类型不允许它与此对象进行比较。

 

 

ok,到这里,我们先简单分析一下这个接口:

  • 它是一个泛型接口
  • 它只有一个接口方法compareTo(),返回int类型值,通常可以是-1,0,1
  • 它有很多实现类,大部分我们知道的都是值类型的类,比如Integer,Float等等
  • 它和重写Object中的equals方法一样,也有几条约束并且和equals约束极其类似
  • 实现了该接口的类的对象列表(或数组)无需比较器就可以配合Collections.sort(或Arrays.sort)实现排序;反之,我们通过比较器也可以实现对没有实现该接口的类的对象列表(或数组)配合Collections.sort(或Arrays.sort)实现排序。

总结下来其实就是这么几条。

 

那下面就来结合示例看看如何编写Comparable接口的compareTo()方法,还是拿前几篇使用的Person类来说

 

package hfut.edu;

/**
 * Date:2018年10月1日 上午11:05:45 Author:why
 */

public class Person implements Comparable<Person> {
    //public class Person{
    //private int hash = 1;
    int age;
    String name;
    String sex;

    public Person(int age, String name, String sex) {
        super();
        this.age = age;
        this.name = name;
        this.sex = sex;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    @Override
    public boolean equals(Object obj) {
        // TODO Auto-generated method stub

        if (!(obj instanceof Person))
            return false;

        Person p = (Person) obj;
        return this.age == p.age && this.name.equals(p.name) && this.sex.equals(p.sex);

    }

    @Override
    public int hashCode() {
        // TODO Auto-generated method stub
        int hash=1;
        hash = hash * 3 + this.age;
        hash = hash * 3 + this.name.hashCode();
        hash = hash * 3 + this.sex.hashCode();
        return hash;
    }

    @Override
    public String toString() {
        return "Person [age=" + age + ", name=" + name + ", sex=" + sex + "]";
    }


    @Override
    public int compareTo(Person o) {
        // TODO Auto-generated method stub
        return this.age-o.age;
    }
    
}
 

这里面,Person类实现了Comparable接口,非常简单的重写了compareTo()方法,就是对年龄进行了排序,来看看有没有效果:

认识Object中的几个经常需要覆盖的方法——考虑实现Comparable接口

嘿,还真有效果,那么如果年龄相同的有多个该肿么办了,我们再接着看:

 

Person p1 = new Person(23, "why", "hfut");
        Person p2 = new Person(22, "jr", "hfut");
        Person p3 = new Person(26, "love", "hfut");
        Person p4 = new Person(23, "tom", "hfut");
        Person p5 = new Person(23, "jerry", "hfut");
        Person p6 = new Person(26, "bob", "hfut");

        List<Person> list = new ArrayList();
        list.add(p1);
        list.add(p2);
        list.add(p3);
        list.add(p4);
        list.add(p5);
        list.add(p6);
        System.out.println("\n排序前:"+list.toString());
        
        Collections.sort(list);
        System.out.println("\n排序后:"+list.toString());
        
        Collections.sort(list);
        System.out.println("\n排序后:"+list.toString());

输出结果如下:

排序前:[Person [age=23, name=why, sex=hfut], Person [age=22, name=jr, sex=hfut], Person [age=26, name=love, sex=hfut], Person [age=23, name=tom, sex=hfut], Person [age=23, name=jerry, sex=hfut], Person [age=26, name=bob, sex=hfut]]

排序后:[Person [age=22, name=jr, sex=hfut], Person [age=23, name=why, sex=hfut], Person [age=23, name=tom, sex=hfut], Person [age=23, name=jerry, sex=hfut], Person [age=26, name=love, sex=hfut], Person [age=26, name=bob, sex=hfut]]

排序后:[Person [age=22, name=jr, sex=hfut], Person [age=23, name=why, sex=hfut], Person [age=23, name=tom, sex=hfut], Person [age=23, name=jerry, sex=hfut], Person [age=26, name=love, sex=hfut], Person [age=26, name=bob, sex=hfut]]

这里我特想重新排序了两次,结果完全一样,可见除了年龄,其他的排序时按照添加的顺序输出的,假如我们想按照字母顺序把姓名排序,则需重写compareTo方法如下:

@Override
    public int compareTo(Person o) {
        // TODO Auto-generated method stub
        int difAge=this.age-o.age;
        if(difAge!=0) {
            return difAge;
        }
       if(!this.name.equals(o.name)) {
           return this.name.compareTo(o.name);
       }
       return 0;
    }

我们再运行测试代码:

排序前:[Person [age=23, name=why, sex=hfut], Person [age=22, name=jr, sex=hfut], Person [age=26, name=love, sex=hfut], Person [age=23, name=tom, sex=hfut], Person [age=23, name=jerry, sex=hfut], Person [age=26, name=bob, sex=hfut]]

排序后:[Person [age=22, name=jr, sex=hfut], Person [age=23, name=jerry, sex=hfut], Person [age=23, name=tom, sex=hfut], Person [age=23, name=why, sex=hfut], Person [age=26, name=bob, sex=hfut], Person [age=26, name=love, sex=hfut]]

可见,结果是和我们想要的一样了。结果先按照年龄排序,在按照姓名排序,如果想把Person类中的其他域加进来比较也是同样的道理,但是需要注意一下几点:

  • 按照类的域的重要性依次排序,比如对于Person类想主要以年龄排序,那么age就放在最先比较
  • 我上面的compareTo()方法在比较年龄时用了一点简化的手段,但是这样做并不是对所有int类型的域都有效,上面那种写法可能会出现溢出现象,正确的写法应该是:

if(this.age>o.age){

return 1;

}

if(this.age<o.age){

return -1;

}

...............

  • 在复杂的域类型都可以通过递归调用转化为我们可以处理的基本类型来比较

 

到这里,关于为什么自定义类要考虑实现Comparable接口就介绍完了,关于对象列表或者数组的排序通过Comparator的方式这里就不介绍了,其实也没有什么东西,就是自定义一个比较器,把比较的工作单独提取出来放在比较器里面去做,我们在调用Arrays或者Collections的sort方法时多传入一个比较器实例作为参数即可。