`

未完 Java各种比较 : == | equals | compareTo | compare | instanceof

阅读更多
    
Equality Operator == :
http://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.21
一 基本数字类型之间、基本数字类型和其包装类对象之间使用 “==”,比较的是它们的数字值。
引用
称为 Numerical Equality Operator。具体点说:
如果参与==的两个操作数都是基本数字类型,或者一个是基本数字类型一个是基本数字类型的包装类时,做的是两个操作数的数字值的比较:有包装类,则Unboxing;Unboxing完后若存在基本类型不匹配,则Binary Numeric Promotion,直到两个操作数类型一致后,再做数字值的比较。
		int i = 2;
		double d = 2;
		System.out.println(i == d); //true
		System.out.println(i == new Float(2)); //true
		System.out.println(d == new Byte((byte)2)); //true
二 boolean与boolean、boolean与Boolean之间使用 “==”,比较的是它们的布尔值。
三 两个引用类型之间使用 “==”,比较的是他们是否指向同一个对象。
或者表述为:两个对象之间使用 “==”,比较的是这两个对象的内存地址值。
注意:
1 当通过Autoboxing,而不是new的方式创建包装类 Byte / Short / Integer / Long / Character / Boolean 的实例时,由于Java对整数包装类常用区间上包装类实例的缓存和对Boolean取值的常量化,将会导致出现一些意想不到的结果:
       http://wuaner.iteye.com/blog/1668172





equals(Object obj):
== 做对象比较,得出“相等”的结论所表达的是“左右两边就是完完全全的同一个对象”。但在很多场景下,这种严格意义上的“对象相等”并不是我们想要的,我们更加期望通过对象的状态来区别它们是否相等,如用户注册与登录时“邮箱地址相等且登录名相同的用户,就是同一个用户”。这样的场景在实际中比比皆是,于是Java为Object类提供了equals()方法,让你可以通过重写它,来定制自己认可的“相等”。
equals()方法在Object类中的默认实现,是完全等价于 == 的。
Java也给出了重写equals方法的五条约定;这些约定确实是很有必要的,为了满足这些约定,你应该通过测试来检验自己的equals方法,以使它足够健壮。其中的自反性和非空性(For any non-null reference value x, x.equals(null) should return false)还好说,另外三个对称性、传递性和一致性需要在重写时着重检验。
equals() 和 hashcode()的关系 - 为什么重写equals()必须重写hashcode()? (Bloch,Effective Java 2nd)
引用
JDK src中对对象hashcode()方法产生的hash code,有以下约定:
1 只要对象的equals()方法做比较所用的状态(字段)没有被修改,那么对该对象调用多次hashcode()方法都应该始终返回同一个hash code;
2 equals()方法比较结果为相等的两个对象,他们的hashcode()方法返回的hash code必须相同;
3 equals()方法比较结果为不相等的两个对象,不要求你必须为他们产生不相同的hash code;但程序员应该明白为不相等的对象产生不相同的hash code可以提高基于散列表的容器类(HashMap/Hashtable/HashSet)的性能。

首先hashcode()方法返回的int类型hash code是什么,干什么用的?
看名便知,对象的hash code是与散列表(Hash Table, http://wuaner.iteye.com/blog/553007)有关的。散列表插入删除快,以及常数级的快速查找等优点,使java多个容器类都是用了散列表作为其内部的数据结构实现方式,如HashMap/Hashtable/HashSet etc。Java里对象的hash code,正是散列表相关概念里的“关键字”(key)。记录在散列表中的存储地址address,正是通过散列函数H(key)产生的。

约定 1 告诉我们:
equals()方法和hashcode()方法应该是基于对象相同的状态字段;对不可变类(immutable class,实例不能被修改的类),如Java中的Integer、String、BigDecimal等,他们的实例对象的状态永远不会变,重写equals和hashcode相对容易;对可变类(mutable class,状态字段会在使用中被修改),我们应该记住:不要让equals和hashcode方法依赖于那些变化的状态(字段)。

为什么重写equals()必须重写hashcode()?
原因源自JDK的要求: equals()相等的对象其hash code必须相同。
未重写hashcode()的话,Object类的hashcode()方法返回的hash code,是对象的内存地址通过一定方式转换成的integer,你可以通过System.identityHashCode(obj)取得这个默认的hash code;这种机制保证了不互相 == 的对象,其hash code一定不相同(As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects。但这句话也不完全正确,默认hash code也是存在相同的可能的,参见:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6321873 )。那么,状态上符合equals()相等的两个不同对象,其hash code也将是不同的。所以,我们应该在重写equals时,也必须重写hashcode()。

为什么equals()相等的对象其hash code必须相同?
1 举一个反例:equals相等的对象,其hash code不等,会出现什么后果那?我们知道,对象在散列桶(hash buckets)中的位置(address或说index)是通过hashcode经散列函数计算出来的;equals相等,hash code不相同的对象,则他们会被散列到不同的bucket中。
2 Set接口元素不能重复、Map接口作为key的对象不能重复,这个"重复"的判断依据是通过调用equals()方法来判断的。但同时,这些以散列表为底层实现的容器类,为了性能的需要,缓存了对象的hash code值(确切说是hash code经过second hash后的值),并将这个缓存值和equals一起,也作为对象重复的判断依据。那么,如果两个对象equals相等、hash code却不同,则会出现两个对象被重复装入容器的问题;且你想通过一个对象获取容器中一个equals相等(但hash code不同)的对象时,是无法获取到的。

为什么equals()不相等的对象其hash code应该尽可能地不相同?
对象的hash code对应散列表概念里的“关键字”。既然是“关键字”,关键字的选取策略,遵循的就是唯一性原则:不同记录,其关键字不应该相同。不同的关键字尚且有冲突的可能(参见散列表冲突的定义),关键字本身都存在相同的话冲突更是严重,最终导致散列表性能的大幅下降。
举一个极端的例子:假设某类的所有对象,其hash code都是一个固定的、完全相同的整数,则经过散列函数H(hash code)得到的存储地址address则也将自始至终都是同一个,导致散列表退化为一个彻头彻尾的链表,丧失其快速查找定位的优点。

通过以上分析看的出来,如果你重写了类的equals方法,并试图将该类的对象放入基于散列表的容器类(作为Set元素,或作为Map的key)的话,则你也必须重写它的hashcode方法!!其他时候,hashcode方法重不重写其实关系不大。但作为一个好的习惯,你应该在重写equals方法的同时去重写hashcode方法,保证equals相等的对象其hash code也相等 (万一这个类的对象被别人放入基于散列表的容器类,也就不会出问题了)。


重写了equals&hashcode方法的JKD类有:
String:串一样的String对象都equals相等、有相同的hash code;
所有的基本类型包装类:数值基本类型包装类,数字值一样的本类对象都equals相等、hash code相同;Boolean布尔值一样就equals相等、hash code相同;
Math包下的BigDecimal等。
        String s1= new String("abc");  
        String s2 = new String("abc");  
        String s3 = "abc";  
        System.out.println(s1.equals(s2));
        System.out.println(s1.equals(s3));
        System.out.println(s2.equals(s3));
        System.out.println(s1.hashCode() + " " + s2.hashCode() + " " + s3.hashCode());  
输出结果:
true
true
true
96354 96354 96354

Srcs:
Integer Hash Function:
http://www.concentric.net/~ttwang/tech/inthash.htm
参考 : Java Collections  |  容器:
http://wuaner.iteye.com/blog/1672580



Comparable<T> 's compareTo(T o):
http://download.java.net/jdk7/archive/b123/docs/api/java/lang/Comparable.html
你可能已经注意到,Java里基本类型的包装类、String、BigDecimal等类都实现了Comparable接口。这个接口是干什么的那?
正如其名,实现Comparable接口,是为了告诉外界该类的对象之间是“可以比较的”。“可以比较的”这里比较的意义在于,当你在Array及有序的容器类如ArrayList、LinkedList、Vector、TreeMap、TreeSet中放入的是实现了Comparable接口的类的对象时,你可以基于该类中对compareTo(T o)方法的实现,对容器类中的元素对象做排序等操作。Comparable接口为它的实现类所提供的排序方式,我们习惯上称其为自然排序(natural ordering),类所重写的compareTo(T o)方法我们称为它的自然比较方法(natural comparison method)。JDK里几个重要的类的自然排序如下:

关于Comparable接口的int compareTo(T o)方法:
引用
以x.compareTo(y)为例,其int类型返回值与比较结果的关系是:
若返回值小于0,表示 x < y;
若返回值等于0,表示 x = y;
若返回值大于0,表示 x > y。
Comparable接口的实现类必须保证其重写的compareTo方法满足以下四个重要的限制条件:
1. sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
2. (x.compareTo(y)>0 && y.compareTo(z)>0) implies x.compareTo(z)>0.
3. x.compareTo(y)==0 implies sgn(x.compareTo(z)) == sgn(y.compareTo(z))
4. (x.compareTo(y)==0) == (x.equals(y))(IOW, Natural ordering should be consistent with equals. - Not required,But strongly recommended)

Comparable<T>接口是参数化的(泛型),这在一定程度上意味着,实现该接口的类,可以去做跨类的比较(即Class A implements Cmparable<B>);但跨类比较这种特性仔细想想既没必要,又使compareTo方法的四个限制条件难以被保证。在Java默认提供的类的自然排序中没有跨类的比较。

JDK中依赖于元素对象所在类的自然排序的类有:
工具类Arrays和Collections;
有序容器类TreeSet和TreeMap;
用来做自然比较的两个元素对象,必须是“可以互相比较的”(mutually comparable)。“可以互相比较”从两个方面理解:
1 容器内元素对象都来自一个类的实例的情况下:如果作为元素的对象其类没有实现Comparable接口,则元素间肯定是无法相互比较的,所以会出现:对于Arrays和Collections,会在调用它们的sort()方法时报ClassCastException; 对于TreeSet和TreeMap,如果你使用的是默认的无参构造方法构建这两个容器类的实例,它们元素对象的“有序”正是通过元素或key的自然排序来做的,在试图添加第二个元素时,这种自然排序的比较就会被用到,从而报ClassCastException。所以,元素所在的类必须实现Comparable接口,否则报ClassCastException;
2 如果放入一个有序容器类里的元素对象来自不同的类,此时哪怕这几个类都实现了Comparable接口定义了(针对本类对象的)自己的自然排序,跨类的比较仍然是无法做的,还是会报ClassCastException。
总之,一句话:使用自然排序时只能向集合中加入同类型的对象,并且这些对象的类必须实现Comparable接口

Srcs:
http://www.coderanch.com/t/557926/java-programmer-SCJP/certification/Collections-sort-throws-ClassCastException




Comparator<T> 's compare(T o1, T o2):
http://docs.oracle.com/javase/6/docs/api/java/util/Comparator.html
Comparator接口是个比较器,它同样适用于工具类Arrays和Collections、有序容器类TreeSet和TreeMap。当你需要一个不是基于类的自然排序来做的排序时(比如,你想单独基于Person的name,id,age分别做排序),可以用它来做。
和Comparable实现的自然排序相比,基于比较器的排序更加的灵活。
int  compare(T o1, T o2)方法:
引用
和Comparable接口的compareTo方法很像:
若o1 < o2, 则返回值 < 0;
若o1 = o2, 则返回值 = 0;
若o1 > o2, 则返回值 > 0;
注意:
引用
不论是 Comparable<T> 's compareTo(T o) 还是 Comparator<T> 's compare(T o1, T o2),当使用 java 排序 API(如 Collections.sort) 做排序时,都是按照 asc 正序,即 from smaller (o1) to greater (o2)。参见:
http://stackoverflow.com/a/17641949/1635855




Keyword - instanceof:
http://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.20.2
用来比较一个对象是否是指定类型(类或接口)的实例。格式:
          ref instanceof ReferenceType
ref 必须是引用类型变量,ReferenceType必须是引用类型;当且仅当ref不为null并且可以强制转换为ReferenceType类型时,返回true。
需要注意的是,不要在代码中使用instanceof做一些诸如条件判断之类的事情,因为那样做违反里氏替换原则,使代码变的不易维护,难以扩展。事实上,instanceof唯一正确的使用场景就是在重写equals方法时,其他时候都应该尽量避免使用它。



Sources:
Java Tutorials - Collections - Object Ordering:
http://docs.oracle.com/javase/tutorial/collections/interfaces/order.html
==, .equals(), compareTo(), and compare()
http://leepoint.net/notes-java/data/expressions/22compareobjects.html
  • 大小: 15.1 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics