集合元素的比较需要注意的事项

Comparable 和 Comparator

Java中两个对象相比较的方法通常在元素排序中,常用的两个接口分别是 ComparableComparator ,前者是自己和自己比较,可以看作是自营性质的比较器;后者是第三方比较器,可以看作是平台性质的比较器。

我们熟知的Integer 和 String 实现的就是Comparaable的自然排序。而我们在使用某个自定义对象时,可能需要按照自己定义的方式排序,比如在搜素对象 SearchResult 中进行比较时,先根据相关度排序,然后再根据浏览数排序,实现这样的自定义 Comparable 的示例代码如下:

public class SearchResult implements Comparable<SearchResult>{
    int relativeRatio;
    long count;
    int recentOrders;

    public SearchResult(int relativeRatio, long count) {
        this.relativeRatio = relativeRatio;
        this.count = count;
    }
    @Override
    public int compareTo(SearchResult o) {
        //先比较相关度
        if(this.relativeRatio != o.relativeRatio){
            return this.relativeRatio > o.relativeRatio ? 1 : -1;
        }
        //相关度相等时再比较浏览数
        if(this.count != o.count)
        {
            return this.count > o.count ? 1 : -1;
        }
        return 0;
    }
}

实现 Comparable 时,可以加上泛型限定,在编译阶段即可发现传入的参数非 SearchResult 对象,不需要在运行期进行类型检查和强制转换。如果这个排序的规则不符合业务方的要求,那么就需要修改这个类的比较方法 compareTo,然而我们都知道开闭原则,即最好不要对自己已经交付的类进行修改。

另外,如果另一个业务方也在使用这个比较方法呢? 甚至这个SearchResult 是他人提供的类,我们可能连源码都没有。所以,我们其实需要在外部定义比较器,即 Comparator

Comparator 的出现,业务方可以根据需要修改排序规则。如在上面的示例代码中,如果业务方需要在搜索时将最近订单数(recentOrders) 的权重调整到相关度和浏览数之前,则使用
Comparator 实现的比较器如下所示:

public class SeatchResultComparator implements Comparator<SearchResult> {
    @Override
    public int compare(SearchResult o1, SearchResult o2) {
        //先比较相关度
        if(o1.relativeRatio != o2.relativeRatio){
            return o1.relativeRatio > o.relativeRatio ? 1 : -1;
        }
        //如果相关度一样,则最近订单数多着排前
        if(o1.recentOrders != o2.recentOrders){
            return o1.recentOrders > o2.recentOrders ? 1: -1;
        }
        //相关度相等时再比较浏览数
        if(o1.count != o2.count)
        {
            return o1.count > o2.count ? 1 : -1;
        }
        return 0;
    }
}

在JDK中,Comparator 最典型的应用是在Arrays.sort中作为比较器参数进行排序

    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }

约定俗成,不管是 Comparable 还是 Comparator , 小于的情况返回 -1 ,等于的情况返回0,大于的情况返回1。当然,很多代码里只是判断是否大于或小于0,如下集合中使用比较器进行排序时,直接使用正负来判断比较的结果:

result = comparator.compare( key,t.key);
if(result < 0){
  t = t.left;
}
else if(result > 0)
{
  t = t.right;
}
else
return t;

hashCode 和 equals

hashCode 和 equals 用来标识对象,两个方法协同工作用来判断两个对象是否相等。

对象通过调用 Object.hashCode() 生成哈希值;由于不可避免地会存在哈希值冲突的情况,因此当hashCode相同时,还需要再次调用equals进行一次值的比较;但是,若 hashCode不同,将直接判定Object不同,跳过equals ,这加快了冲突处理的效率。

Object 类定义中对hashCode 和 equals 要求如下:

  1. 如果两个对象的 equals 的结果是相等的,则两个对象的hashCode 的返回结果也必须是相同的。
  2. 任何时候覆写equals,都必须同时覆写hashCode。

在Map 和 Set 类集合中,用到这两个方法时,首先 判断hashCode的值,如果hash相等,再判断 equals 的结果,HashMap的get判断如下:

 if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))

if 条件表达式中的 e.hash == hash 是先决小件,只有相等才会执行比较后面部分。如果不想等,后面的表达式根本不会执行。equals 不相等时并不强制要求hashCode 也不相等,但是一个优秀的哈希算法应尽可能地让元素均匀分布,降低冲突概率,即在equals 不相等时尽量使hashCode 也不相等,这样 &&或||短路操作一旦生效,会极大地提高程序的执行效率。

如果是自定义对象作为Map的键,那么必须覆写 hashCode 和 equals .此外,因为set存储的是不重复的对象,依据hashCode 和 equals 进行判断,所以Set存储的自定义对象也必须覆写这两个方法。此时如果覆写了equals,而没有覆写 hashCode ,具体会有什么影响?

public class EqualsObject {
    private int id;
    private String name;

    public EqualsObject(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        //如果为null,或者并非同类,则直接返回false
        if(obj==null || this.getClass()!=obj.getClass())
        {
            return false;
        }
        //如果指向同一对象 ,则返回true
        if(obj == this){return true;}

        //需要强制转换来获取 EqualsObject 的方法
        EqualsObject temp = (EqualsObject) obj;
        if(temp.getId()==this.getId() && temp.equals(this.getName())){
            return true;
        }
        return false;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

测试类:

public class EqualsObjectTest {
    public static void main(String[] args) {
        Set<EqualsObject> sets = new HashSet<>();
        EqualsObject o1 = new EqualsObject(1,"1");
        EqualsObject o2 = new EqualsObject(1,"1");
        EqualsObject o3 = new EqualsObject(1,"1");
        sets.add(o1);
        sets.add(o2);
        sets.add(o3);
        System.out.println(sets.size());
    }
}

结果: size = 3

为什么结果时3呢 ?

因为如果不覆写hashCode(), 即使equals 相等也毫无意义,Object.hashCode() 的实现是默认为每一个对象生成不同的 int 数值,它本身是 native 方法,一般与对象内存地址有关,
HashSet 底层也是通过 HashMap 来实现的,所以判断代码如下:

if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))

因为 hashCode 不一样,所以是三个对象,如果想存储不重复的元素,那么需要在EqualsObject 类中覆写 hashCode();

   @Override
    public int hashCode() {
        return id + name.hashCode();
    }

总结

  1. 只要重写 equals ,就必须重写 hashCode
  2. 因为Set存储的是不重复的对象,依据hashCode 和 equals 进行判断,所以Set存储的对象必须重写这两个方法。
  3. 如果自定义对象做为Map的键,那么必须重写 hashCode 和 equals 。

说明: String 重写了hashCode 和 equals 方法,所以我们可以非常愉快地使用String对象作为key来使用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半夏_2021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值