Comparable 和 Comparator
Java中两个对象相比较的方法通常在元素排序中,常用的两个接口分别是 Comparable
和 Comparator
,前者是自己和自己比较,可以看作是自营性质的比较器;后者是第三方比较器,可以看作是平台性质的比较器。
我们熟知的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 要求如下:
- 如果两个对象的 equals 的结果是相等的,则两个对象的hashCode 的返回结果也必须是相同的。
- 任何时候覆写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();
}
总结
- 只要重写 equals ,就必须重写 hashCode
- 因为Set存储的是不重复的对象,依据hashCode 和 equals 进行判断,所以Set存储的对象必须重写这两个方法。
- 如果自定义对象做为Map的键,那么必须重写 hashCode 和 equals 。
说明: String 重写了hashCode 和 equals 方法,所以我们可以非常愉快地使用String对象作为key来使用。