Java中的Comparable接口和Comparator接口

top.jpg

最近Algorithms 4 课上提到了排序。趁着这个机会,梳理一下。

1. 介绍

Comparable<T>接口和Comparator<T>接口都是JDK中提供的和比较相关的接口。使用它们可以对对象进行比较大小,排序等操作。这算是之后排序的先导知识吧。
Comparable, 字面意思是“可以比较的”,所以实现它的类的多个实例应该可以相互比较“大小”或者“高低”等等。
Comparator, 字面意思是“比较仪,比较器”, 它应该是专门用来比较用的“工具”。

2. Comparable

Comparable<T>接口

public interface Comparable<T> {

    public int compareTo(T o);
}

首先看看JDK中怎么说的:

This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class's <i>natural ordering</i>, and the class's <tt>compareTo</tt> method is referred to as its <i>natural comparison method</i>.<p>

大意是: 任何实现这个接口的类,其多个实例能以固定的次序进行排列。次序具体由接口中的方法compareTo方法决定。

Lists (and arrays) of objects that implement this interface can be sorted automatically by {@link Collections#sort(List) Collections.sort} (and {@link Arrays#sort(Object[]) Arrays.sort}).

如果某个类实现了这个接口,则它的List或数组都能使用Collections.sort()Arrays.sort()进行排序。
常见的类如Integer, Double, String都实现了此类。一会儿会结合源码进行分析。

我们先来看Integer中的实现:

public final class Integer extends Number implements Comparable<Integer> {

    private final int value;
    
    public int compareTo(Integer anotherInteger) {
        return compare(this.value, anotherInteger.value);
    }
    public static int compare(int x, int y) {
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }
    public static int compareUnsigned(int x, int y) {
        return compare(x + MIN_VALUE, y + MIN_VALUE);
    }

我们只贴出了和比较相关的方法。
可以看到,compareTo方法其中调用了compare方法,这是JDK1.7增加的方法。在Integer中新增这个方法是为了减少不必要的自动装箱拆箱。传入compare方法的是两个Integer的值xy
如果x < y, 返回-1;如果x = y, 返回0;如果x > y, 返回1
顺便一说,JDK中的实现非常简洁,只有一行代码, 当判断情况有三种时,使用这种嵌套的判断 x ? a : b 可以简洁不少,这是该学习的。

后面的compareUnsigned是JDK1.8新加入的方法, 用来比较无符号数。这里的无符号数意思是默认二进制最高位不再作为符号位,而是计入数的大小。
其实现是

    public static int compareUnsigned(int x, int y) {
        return compare(x + MIN_VALUE, y + MIN_VALUE);
    }

直接为每个值加了Integer的最小值 -231。我们知道Java中int类型为4个字节,共32位。符号位占用一位的话,则其范围为-231 到231 - 1。
使用此方法时,所有正数都比负数小。最大值为 -1,因为 -1的二进制所有位均为 1。
也就是1111 1111 1111 1111 1111 1111 1111 1111 > 其它任何32位数。

具体是什么情况呢?

2.1 计算机编码

首先我们知道,在计算机中,所有数都是以二进制存在,也就是01的组合。
为了使数字在计算机中运算不出错,出现了原码,反码和补码。原码就是一个数的二进制表示,其中最高位为符号位,表示其正负。
正数的原码反码补码都一样,负数的反码是除符号位以外全部取反,补码为反码加1,如图所示为32位bits(也就是4比特bytes)数的原码反码和补码。

原码反码补码.jpg

为什么要使用反码和补码呢?用四位二进制数举例:
1的二进制为0001-1的二进制为1001,如果直接相加,则1 + (-1) = 0,二进制表示为0001 + 1001 = 1010 != 0,所以不能直接使用原码做运算。

后来出现了反码,除符号位之外其他位取反,1001(-1)取反后为1110, 现在做加法 0001 (1) + 1110 (-1) = 1111 。由于1111是负数,所以取反之后才是其真实值,取反后为1000,也就是-0。这能满足条件了,但是美中不足的是,0带了负号。唯一的问题其实就出现在0这个特殊的数值上。 虽然人们理解上+0-0是一样的, 但是0带符号是没有任何意义的。 而且会有0000原和1000原两个编码表示0。怎么办呢?

人们又想出了补码,它是反码加1-1的补码是 1111,以上的运算用补码表示就是0001 (1) + 1111 (-1) = 0000 = 0。神奇的发现,这个式子完美契合了十进制加法!
同时我们留出了1000,可以用它表示-8

(-1) + (-7) = (补码) 1111 + 1001 = 1000 = -8。注意,由于此处的-8使用了之前-0的补码来表示,所以-8没有没有原码和反码表示(针对的四位,如果是八位,则没有原码和反码的是-128,依次类推)。

使用补码, 不仅仅修复了0的符号以及存在两个编码的问题, 而且还能够多表示一个最低数. 这就是为什么4位二进制, 使用原码或反码表示的范围为[-7, +7], 而使用补码表示的范围为[-8, 7].

二进制加法.jpg

这就是简单的要用反码和补码的原因。

2.2 大数溢出问题

int类型在32位系统中占4个字节、32bit,补码表示的的数据范围为:

[10000000 00000000 00000000 00000000] ~ [01111111 11111111 11111111 11111111]

[−231,231−1]
[-2147483648, 2147483647]

在java中表示为:

[Integer.MIN_VALUE, Integer.MAX_VALUE]

byte类型的表示一样,由于负数比正数多表示了一个数字。对下限去相反数后的数值会超过上限值,溢出到下限,因此下限的相反数与下限相等;对上限去相反数的数值为负值,该负值比下限的负值大1,在可以表示的范围内,因此上限的相反数是上限直接取负值。

2.3 String类型的compareTo方法

看完Integer后,我们再来看StringcompareTo的实现方式:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];     // String的值
  
    public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);     // limit, 表示两个String中长度较小的String长度
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;     // 如果char不相同,则取其差值
            }
            k++;    // 如果char值相同,则继续往后比较
        }
        return len1 - len2;     // 如果所有0 ~ (lim - 1)的char均相同,则比较两个String的长短
    }
  // 字面意思是对大小写不敏感的比较器
    public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();
    private static class CaseInsensitiveComparator
            implements Comparator<String>, java.io.Serializable {
        private static final long serialVersionUID = 8575799808933029326L;

        public int compare(String s1, String s2) {  
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);     // 和上面类似,均是取两个String间的最短长度
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1); // 统一换成大写
                    c2 = Character.toUpperCase(c2); // 统一换成大写
                    if (c1 != c2) {     // 大写如果不相等则再换为小写试试
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {     // 到此处则确定不相等
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }

        /** Replaces the de-serialized object. */
        private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
    }
    // String的方法,可以直接使用这个方法和其它String进行比较,
    // 内部实现是调用内部比较器的compare方法
    public int compareToIgnoreCase(String str) {
        return CASE_INSENSITIVE_ORDER.compare(this, str);
    }   
}

String中的关于compare的方法相对复杂一点,但还是比较简单。我们先不看其他的代码,只重点关注compareTo方法。

    public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);     // limit, 表示两个String中长度较小的String长度
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;     // 如果char不相同,则取其差值
            }
            k++;    // 如果char值相同,则继续往后比较
        }
        return len1 - len2;     // 如果所有0 ~ (lim - 1)的char均相同,则比较两个String的长短
    }

内容很简洁,就是取两个String的长度中较小的,作为限定值(lim)。之后对数组下标为从0lim - 1char变量进行遍历比较,如果遇到不相同的值,返回其差值。一般我们只用其正负性,如果返回负数则说明第一个对象比第二个对象“小”。
例如比较 "abc""bcd",当对各自第一个字符'a''b'进行比较时,发现 'a' != 'b',则返回 'a' - 'b' ,这个值是负数, char类型的-1,Java会自动将其类型强转为int型。最后得出结论"abc""bcd"小。

3. Comparator

Comparator<T>接口

public interface Comparator<T> {
    int compare(T o1, T o2);
}

这是一个外部排序接口,它的功能是规定“比较大小”的方式。实现它的类可以作为参数传入Collections.sort()Arrays.sort(),使用它的比较方式进行排序。
它可以为没有实现Comparable接口的类提供排序方式。
String类中以及Array类等都有实现此接口的内部类。

在上面String的源码中就有一个内部的自定义ComparatorCaseInsensitiveComparator, 我们看看它的源码。

    public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();
    private static class CaseInsensitiveComparator
            implements Comparator<String>, java.io.Serializable {
        private static final long serialVersionUID = 8575799808933029326L;

        public int compare(String s1, String s2) {  
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);     // 和上面类似,均是取两个String间的最短长度
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1); // 统一换成大写
                    c2 = Character.toUpperCase(c2); // 统一换成大写
                    if (c1 != c2) {     // 大写如果不相等则再换为小写试试
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {     // 到此处则确定不相等
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }

        /** Replaces the de-serialized object. */
        private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
    }
    // String的方法,可以直接使用这个方法和其它String进行比较,
    // 内部实现是调用内部比较器的compare方法
    public int compareToIgnoreCase(String str) {
        return CASE_INSENSITIVE_ORDER.compare(this, str);
    }   
}

CaseInsensitiveComparator, 字面意思是对大小写不敏感的比较器。
我们观察它的compare方法,可以发现,它和上面的compareTo方法实现类似,都是取两个String中长度较小的,作为限定值min,之后对数组下标为从0min - 1char变量进行遍历比较。和上面稍有不同的是,此处先将char字符统一换成大写(upper case), 如果仍然不相等,再将其换为小写(lower case)比较。一个字母只有大写或者小写两种情形,如果这两种情况都不想等则确定不相等,返回其差值。如果限定值内所有的char都相等的话,再去比较两个String类型的长度。

例如比较 "abC""ABc"compareTo会直接返回 'a' - 'A',而compareToIgnoreCase方法由于使用了CaseInsensitiveComparator,比较结果最终会返回true

参考文章

[1] Java源码学习之Integer类(二)——1.8新增的几个函数和变量
[2] 计算机原码、反码、补码详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,783评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,360评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,942评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,507评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,324评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,299评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,685评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,358评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,652评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,704评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,465评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,318评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,711评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,991评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,265评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,661评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,864评论 2 335

推荐阅读更多精彩内容

  • 网站乱码问题我们会经常碰到,大多见于非英文的中文字符或其他字符乱码,而且,这类问题常常是因为编码方式问题,主要原因...
    波段顶底阅读 2,784评论 1 9
  • Java源码 Integer Integer的签名如下,继承了Number类并实现Comparable接口 Com...
    wngn123阅读 1,242评论 0 2
  • 书中关于原码、反码、补码和移码的定义如下(n是机器字长):原码: 反码: 补码: 移码: 原码, 反码, 补码的基...
    困卡阅读 15,658评论 2 8
  • As i was sitting there, i suddenly noticed a little girl....
    三木之墓阅读 202评论 0 0
  • 一 “成亮,还不走吗?”刚刚出门...
    游迈阅读 296评论 0 1