1、Comparable和Comparator
Java中两个对象相比较的方法通常用在元素排序里,常用的两个接口分别是Comparable和Comparator。
- 前者Comparable是自己和自己比,可以看作是自营性质的比较器。从词根上分析,Comparable以-able结尾,表示它有自身具备某种能力的性质,表明Comparable对象本身是可以与同类型进行比较的,它的比较方法是compareTo。
- 后者Comparator是第三方比较器,可以看作是平台性质的比较器。从词根上分析,Comparator以-or结尾,表明自身是比较器的实践者,它的比较方法是compare。
我对上面这两点的理解是:
- Comparable:当一个类实现Comparable接口时,重写比较方法compareTo(参数:这个类的一个实例对象),在进行比较的时候格式是这样的:这个类的一个实例对象.compareTo(这个类的另一个实例对象)。在需要进行比较的类内部重写了比较方法,使用比较方法时是通过这个类的实例对象去调用的,所以作者说他是自营性质的比较器。
- Comparator:由一个跟“需要比较业务的类1”毫不相关的类2去实现Comparator接口,重写比较方法compare(参数1:类1的实例对象,参数2:类2的实例对象)。因为比较功能由毫不相关的第三方类去实现,所以作者说它是一个平台性质的比较器。
我们经常说的自然排序其实是以人类对常识认知的升序排序,比如数字的1、2、3,字母的a、b、c等。我们熟知的Integer和String实现的就是Comparable的自然排序。
我们在使用某个自定义对象时,可能需要按照自己定义的方法排序,比如在搜索列表对象SearchResult中进行大小比较时,先根据相关度排序,然后再根据浏览数排序,实现这种自定义Comparable的示例代码如下:
package Test;
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;
}
public void setRecentOrders(int recentOrders){
this.recentOrders = recentOrders;
}
}
实现Comparable时,可以加上泛型限定,在编译阶段即可发现传入的参数非SearchResult对象,不需要再运行期进行类型检查和强制转换。如果这个排序规则不符合业务方的要求的话,那么只需要修改ComparaTo这个比较方法。
但是如果SearchResult这个类是他人提供的类,我们可能连源码都没有。所以这个时候我们需要在外部定义比较器,即Comparator。正因为Comparator的出现,业务方可以根据需要修改排序规则,如在上面的示例代码中,如果业务方需要在搜索时将最近订单数(recentOrders)的权重调整到相关度与浏览数之间,则使用Comparator实现的比较器,代码如下所示:
package Test;
import java.util.Comparator;
public class SearchResultComparator implements Comparator<SearchResult> {
@Override
public int compare(SearchResult o1, SearchResult o2) {
//相关度是第一排序准则,更高者排前
if (o1.relativeRatio != o2.relativeRatio){
return o1.relativeRatio > o2.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);
}
}
<? super T>为下限通配符(请参考我的另一篇文章集合与泛型),如果本例中不加限定,嘉定sort对象是Integer,那么传入String时就会编译报错,充分利用多态的向下转型功能。
约定俗成,不管是Comparable还是Comparator,小于的情况返回-1,等于的情况返回0,大于的情况返回1。
2、hashCode和equals
hashCode和equals用来标识对象,两个方法协同工作可用来判断两个对象是否相等。
众所周知,根据生成的哈希将数据离散开来,可以使存取元素更快(通过哈希值来确定元素存放的位置)。对象通过调用Object.hashCode()生成哈希值;由于不可避免地存在哈希值冲突的情况(什么是哈希冲突?由于哈希算法被计算的数据是无限的,但是计算结果的范围是有限的,因此总会存在不同数据经过计算后得到的值一样,这就是哈希冲突。最常见的哈希算法是取模法,假设有一个长度为5的数组,这时有一个数据是6,那怎么把这个数据放到数组中去呢?采用取模法,计算6%5=1,于是6存放在数组中下标为1的位置。依此类推,7放在下标为2的位置,直到出现11这个数据,11%5=1,哈希算法计算出来的存放位置跟6相同,这时就出现了哈希冲突。),因此当hashCode相同时,还需要再调用equals进行一次值的比较;但是,若hashCode不同,将直接判定Object不同,跳过equals,这加快了冲突处理的效率。Object类定义中对hashCode和equals要求如下:
- 如果两个对象的equals的结果是相等的,则两个对象的hashCode的返回结果也必须是相同的。
- 任何时候覆写equals,都必须同时覆写hashCode。
在Map和Set类集合中,用到这两个方法时,首先判断hashCode的值,如果hash相等,则再判断equals的结果,HashMap的get判断代码如下:
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return (e = getNode(hash(key), key)) == null ? null : e.value;
if表达式中的e.hash == hash是先决条件,只有相等才会执行&&后面的语句。如果不相等,则&&后面的语句根本不会被执行。equals不相等时并不强制要求hashCode也不相等,但是一个优秀的哈希算法应尽可能的让元素均匀分布,降低冲突概率,即在equals不相等时尽量使hashCode也不相等,这样&&或||短路操作一旦生效,会极大地提高程序的执行效率。
如果自定义Map和Set的对象的话,必须要覆写hashCode和equals两个方法,如果只是覆写了equals,而没有覆写hashCode会出现什么影响,从下面这段代码中来体会:
package Test;
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,getClass()用来比较较两个对象是否是同一个类的实例(第一处)
if (obj == null || this.getClass() != obj.getClass()){
return false;
}
//如果引用指向同一个对象,则返回true,==判断的是引用指向的地址是否相同
if (this == obj){
return true;
}
//需要强制转换来获取EqualsObject的方法
EqualsObject temp = (EqualsObject)obj;
//本示例判断标准是两个属性值相等,逻辑随业务场景不同而不同
if (temp.getId() == this.id && name.equals(temp.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;
}
}
第一处说明:首先判断两个对象的类型是否相同,如果不匹配,则直接返回false。此处使用getClass()方法,就是严格限制了只有EqualsObject对象本身才可以执行equals操作(为什么使用getClass()方法而不是用instanceof方法,是因为子类对象 instanceof 父类对象的返回值为true,而子类.getClass() == 父类.getClass()的返回值为false,防止子类被判断为父类的情况)。
这里没有覆写hashCode,那么把这个对象放置到Set(不允许出现重复元素)集合中去:
Set<EqualsObject> hashSet = new HashSet<>();
EqualsObject a = new EqualsObject(1, "one");
EqualsObject b = new EqualsObject(1, "one");
EqualsObject c = new EqualsObject(1, "one");
hashSet.add(a);
hashSet.add(b);
hashSet.add(c);
System.out.println(hashSet.size());
输出的结果是3。对然这3个对象显而易见是相同的,但在HashSet操作中,应该只剩下一个,为什么结果是3呢?因为如果不覆写hashCode(),即使equals()相等也毫无意义,Object.hashCode()的实现是默认每一个对象生成不同的int数值,它本身是native方法,一般与对象内存地址有关。(hashCode其实就是根据对象的地址进行相关计算得到int类型数值的)
因为EqualsObject没有覆写hashCode,所以得到的是一个与对象地址相关的唯一值,回到刚才HashSet集合上,如果想存储不重复的元素,那么需要在EqualsObject类中覆写hashCode():
@Override
public int hashCode(){
return id + name.hashCode();
}
EqualsObject的name属性是String类型,String覆写了hashCode(),所以可以直接调用。equals()的实现方式与类的具体处理逻辑有关,但又各不相同,因而应尽量分析源码来确定其判断结果,如下例代码所示:
package Test;
import java.util.LinkedList;
public class ListEquals {
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<Integer>();
linkedList.add(1);
ArrayList<Integer> arraylist = new ArrayList<Integer>();
arraylist.add(1);
if (arraylist.equals(linkedList)){
System.out.println("equals is true");
}else {
System.out.println("equals is false");
}
}
}
两个不同的集合类,输出结果是equals is true。因为ArrayList的equals()只进行了是否为List子类的判断,接着调用了equalsRange()方法:
boolean equalsRange(List<?> other, int from, int to){
final Object[] es = elementData;
//用var变量接收linkedList的遍历器(第一处)
var oit = other.iterator();
for (; from < to; from++){
//如果linkedList没有元素,则equals结果直接为false;
//如果linkedList有元素,则对应下标进行值的比较(第二处)
if (!oit.hasNext() || !Object.equals(es[from], oit.next())){
return false;
}
}
//如果arrayList已经遍历完,而linkedList还有元素,则equals结果为false
return !oit.hasNext();
}
第一处说明:局部变量类型推断是JDK10引入的变量命名机制,一改Java是强类型语言的传统形象,这是Java致力于未来体积更小,面向生产效率的新语言特性,减少累赘的语法规则,当然这仅仅是一个语法糖,Java仍然是一种静态语言。在初始化阶段,在处理var变量的时候,编译器会检测右侧代码的返回值类型,并将其类型用于左侧。
第二处说明:尽量避免通过实例对象引用来调用equals方法,否则容易抛出空指针异常。推荐使用JDK7引入的Object的equals方法,源码如下,可以有效地防止在equals调用时产生NPE问题:
public static boolean equals(Object a, Object b){
return (a == b) || (a != null && a.equals(b));
}
- hashCode()为不同的对象产生不同的int值,根据equals定义:如果两个对象是相等(equals)的,那么它们的hashCode()返回结果也必须是相等的;如果两个对象是不相等的,那么它们的hashCode()返回结果也必须不相等。所以必须同时重写equals()和hashCode()以保证同步性。
请看下面一段代码,这段代码是我写的一个简单的例子,目的是撇去一切干扰,能更清楚明了的明白为什么重写equals的同时也要重写hashCode():
package Test;
import java.util.HashMap;
import java.util.Map;
public class hashCodeTest {
public static void main(String[] args) {
Worker worker1 = new Worker("wang");
Worker worker2 = new Worker("wang");
System.out.println( "worker1.equals(worker2)的结果为"+worker1.equals(worker2));
System.out.println("worker1.hashCode()="+worker1.hashCode()+" | worker2.hashCode()="+worker2.hashCode());
Map<Worker,Integer> map1 = new HashMap<Worker,Integer>();
map1.put(worker1, new Integer(10));
System.out.println(map1.get(worker2));
}
}
class Worker{
private String name;
Worker(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//重写equals方法,让其名字相等则判断对象相等
@Override
public boolean equals(Object obj){
return (this.name.equals(((Worker)obj).name));
}
}
先给出执行结果:
上面这段代码中,我在Worker类中覆写了equals()方法:当对象的name属性相同时则判定两个对象相等。由上面的执行结果可以看到worker1.equals(worker2)的结果为true,但是由hashCode可以看出这其实是两个完全不同的对象。当程序执行map.get(worker2)时,会计算worker2的hashcode,而由于Object.hashCode()返回的worker1和worker2的hash值并不相同,因此,map中未找到worker2对应的key值,所以返回的value便为null。
所以可以得到以下结论:
- 如果自定义了equals()方法,且返回true的充要条件不是(this == obj),那么就必须覆写hashCode,且必须保证equals为true的时候两者的hashCode必须相等,否则当该对象作为key值存在hash表中的时候,就无法用逻辑上相等的对象取出该key所对应的value