java中的==、equals()、hashCode()

本文摘抄至 : chenssy旭东deepinmind

摘要 : 介绍==的比较分析。equals的比较原理和在重写equals时使用instanceof得一个误初,建议使用getClass。hashcode()的介绍。后一个hashcode优化内容先记录,有再战

一、双等号==


java中的==是比较两个对象在JVM中的地址。比较好理解。看下面的代码:

public class Sign {
    public static void main(String[] args) {

        String s1 = "nihao" ;
        String s2 = "nihao" ;
        String s3 = new String("nihao") ;

        System.out.println(s1 == s2);
        System.out.println(s1 == s3);
    }
}
---------   output  -----------
true
false

上述代码中:

(1)、s1 == s2为true,是因为s1和s2都是字符串字面值”nihao”的引用,指向同一块地址,所以相等。

(2)、s1 == s3为false,是因为通过new产生的对象在堆中,s3是堆中变量的引用,而是s1是指向字符串字面值”nihao”的引用,地址不同所以不相等。

二、equals()


超类Object中有这个equals()方法,该方法主要用于比较两个对象是否相等。该方法的源码如下:

 public boolean equals(Object obj) {
        return (this == obj);
    }

我们知道所有的对象都拥有标识(内存地址)和状态(数据),同时“==”比较两个对象的的内存地址。
所以说使用Object的equals()方法是比较两个对象的内存地址是否相等。
虽然有时候Object的equals()方法可以满足我们一些基本的要求,但是我们必须要清楚我们很大部分时间都是进行两个对象的比较,这个时候Object的equals()方法就不可以了,实际上JDK中,String、Math等封装类都对equals()方法进行了重写。下面是String的equals()方法:

public boolean equals(Object anObject) {
    if (this == anObject) {
    // (1).String类中的equals首先比较地址,如果是同一个对象的引用,可知对象相等,返回true。
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = count;
        if (n == anotherString.count) {
        char v1[] = value;
        char v2[] = anotherString.value;
        int i = offset;
        int j = anotherString.offset;
        while (n-- != 0) {
        //(2).若果不是同一个对象,equals方法挨个比较两个字符串对象内的字符,只有完全相等才返回true,否则返回false。
            if (v1[i++] != v2[j++])
            return false;
        }
        return true;
        }
    }
    return false;
    }

对于这个代码段:if (v1[i++] != v2[j++])return false;我们可以非常清晰的看到String的equals()方法是进行内容比较,而不是引用比较。至于其他的封装类都差不多。

在Java规范中,它对equals()方法的使用必须要遵循如下几个规则:

equals 方法在非空对象引用上实现相等关系:

public class Sign {
    public static void main(String[] args) {

        String s1 = "nihao" ;
        String s2 = "nihao" ;
        String s3 = new String("nihao") ;

        //自反性 
        System.out.println("自反性 :"+s1.equals(s1));

        //对称性
        if(s1.equals(s2))
            System.out.println("对称性 : "+s2.equals(s1));
        if(s2.equals(s1))
            System.out.println("对称性 : "+s1.equals(s2));

        //传递性
        if(s1.equals(s2) && s2.equals(s3))
            System.out.println("传递性 :"+s1.equals(s3));

        //一致性
    for(int i=0;i<5;i++)
        System.out.println("一致性第 "+i+"次比较"+s1.equals(s2));

    //非空与 null比较
    System.out.println("非空与 null比较 ;"+s1.equals(null));
    System.out.println("非空与 null比较 : "+s3.equals(null));    
    }
}
------  output  -----------
自反性 :true
对称性 : true
对称性 : true
传递性 :true
一致性第 0次比较true
一致性第 1次比较true
一致性第 2次比较true
一致性第 3次比较true
一致性第 4次比较true
非空与 null比较 ;false
非空与 null比较 : false


1、自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。

2、对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。

3、传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。

4、一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。

5、对于任何非空引用值 x,x.equals(null) 都应返回 false。



对于上面几个规则,我们在使用的过程中最好遵守,否则会出现意想不到的错误。

在java中进行比较,我们需要根据比较的类型来选择合适的比较方式:


1) 对象域,使用equals方法 。

2) 类型安全的枚举,使用equals或== 。

3) 可能为null的对象域 : 使用 == 和 equals

4) 数组域 : 使用 Arrays.equals 。

5) 除float和double外的原始数据类型 : 使用 == 。

6) float类型: 使用Float.foatToIntBits转换成int类型,然后使用==。

7) double类型: 使用Double.doubleToLongBit转换成long类型,然后使用==。


至于6)、7)为什么需要进行转换,我们可以参考他们相应封装类的equals()方法,下面的是Float类的:

 public boolean equals(Object obj) {
        return (obj instanceof Float)
               && (floatToIntBits(((Float)obj).value) == floatToIntBits(value));
    }

原因嘛,里面提到了两点:

However, there are two exceptions:
If f1 and f2 both represent
Float.NaN, then the equals method returns
true, even though Float.NaN==Float.NaN
has the value false.
If <code>f1 represents +0.0f while
f2 represents -0.0f, or vice
versa, the equal test has the value
false, even though 0.0f==-0.0f
has the value true.

三、 在equals()中使用getClass进行类型判断


我们在覆写equals()方法时,一般都是推荐使用getClass来进行类型判断,不是使用instanceof。我们都清楚instanceof的作用是判断其左边对象是否为其右边类的实例,返回boolean类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。注意后面这句话:可以用来判断继承中的子类的实例是否为父类的实现,正是这句话在作怪。我们先看如下实例(摘自《高质量代码 改善java程序的151个建议》)。

父类:Person
public class Person {

    protected String name;

    public String getName() {
        return name;
    }

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

    public Person(String name) {
        this.name = name;
    }

    public boolean equals(Object object){
        if(object instanceof Person){
            Person p = (Person) object;
            if(p.getName() == null || name == null){
                return false;
            }
            else{
                return name.equalsIgnoreCase(p.getName());
            }
        }
        return false;
    }
}
 子类:Employee
public class Employee extends Person {

    private int id;

    public int getId() {
        return id;
    }

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

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

      /**
     * 重写equals()方法
     */
    public boolean equals(Object object){
        if(object instanceof Employee){
            Employee e = (Employee) object;
            return super.equals(object) && e.getId() == id;
        }
        return false;
    }
}

上面父类Person和子类Employee都重写了equals(),不过Employee比父类多了一个id属性。测试程序如下:

public class Test {

    public static void main(String[] args) {

        Employee e1 = new Employee("chenssy", 23);
        Employee e2 = new Employee("chenssy", 24);
        Person p1 = new Person("chenssy");

        System.out.println(p1.equals(e1));
        System.out.println(p1.equals(e2));
        System.out.println(e1.equals(e2));

    }

}
--------    output  --------
true
true
false

上面定义了两个员工和一个普通人,虽然他们同名,但是他们肯定不是同一人,所以按理来说输出结果应该全部都是false,但是事与愿违,结果是:true、true、false。

对于那e1!=e2我们非常容易理解,因为他们不仅需要比较name,还需要比较id。
但是p1即等于e1也等于e2,这是非常奇怪的,因为e1、e2明明是两个不同的类,但为什么会出现这个情况?
首先p1.equals(e1),是调用p1的equals方法,该方法使用instanceof关键字来检查e1是否为Person类,这里我们再看看instanceof:判断其左边对象是否为其右边类的实例,也可以用来判断继承中的子类的实例是否为父类的实现。他们两者存在继承关系,肯定会返回true了,而两者name又相同,所以结果肯定是true。

所以出现上面的情况就是使用了关键字instanceof,这是非常容易“专空子”的。故在覆写equals时推荐使用getClass进行类型判断。而不是使用instanceof。

四、hashcode()


hashCode是根类Obeject中的方法。
默认情况下,Object中的hashCode() 返回对象的32位jvm内存地址。也就是说如果对象不重写该方法,则返回相应对象的32为JVM内存地址。

String类源码中重写的hashCode方法如下 :
 1 public int hashCode() {
 2     int h = hash;    //Default to 0 ### String类中的私有变量,
 3     if (h == 0 && value.length > 0) {    //private final char value[]; ### Sting类中保存的字符串内容的的数组
 4         char val[] = value;
 5 
 6         for (int i = 0; i < value.length; i++) {
 7             h = 31 * h + val[i];
 8         }
 9         hash = h;
10     }
11     return h;
12 }

String源码中使用private final char value[];保存字符串内容,因此String是不可变的。

看下面的例子,没有重写hashCode方法的类,直接返回32位对象在JVM中的地址;Long类重写了hashCode方法,返回计算出的hashCode数值:

public class Hu_hashCode {

    public static void main(String[] args) {
        Hu_hashCode a = new Hu_hashCode();
        Hu_hashCode b = new Hu_hashCode();
        System.out.println(a.hashCode());    
        System.out.println(b.hashCode());    


        String s1 = "nihao";
        String s2 = "nihao";
        System.out.println(s1.hashCode());    
        System.out.println(s2.hashCode());   

        Long num1 = new Long(8);
        Long num2 = new Long(8);
        System.out.println(num1.hashCode());   
        System.out.println(num2.hashCode());   

    }
}
--------    output  --------
1284693
31168322
104818427
104818427
8
8

根据以上可得 :

(1)绑定。当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

(2)绑定原因。Hashtable实现一个哈希表,为了成功地在哈希表中存储和检索对象,用作键的对象必须实现 hashCode 方法和 equals 方法。同(1),必须保证equals相等的对象,hashCode 也相等。因为哈希表通过hashCode检索对象。

(3)默认。

==默认比较对象在JVM中的地址。

hashCode 默认返回对象在JVM中的存储地址。

equal比较对象,默认也是比较对象在JVM中的地址,同==

五、hashCode的性能优化


hashCode()方法的一个主要作用就是使得对象能够成为哈希表的key或者散列集的成员。但同时这个对象还得实现equals(Object)方法,它和hashCode()的实现必须是一致的:

  • 如果a.equals(b)那么a.hashCode == b.hashCode()
  • 如果hashCode()在同一个对象上被调用两次,它应该返回的是同一个值,这表明这个对象没有被修改过。

hashCode()的性能


从性能的角度来看的话,hashCode()方法的主要目标就是尽量使得不同的对象拥有不同的哈希值。JDK中所有基于哈希的集合都是将值存储在数组中的。查找元素的时候,会使用哈希值来计算出在数组中的初始查找位置;然后再调用equals()方法将给定的值和数组中存储对象的值进行比较。如果所有元素的哈希值都不一样,这会减少哈希的碰撞概率。换句话说,如果所有的值的哈希码都一样的话,hashmap(或者hashset)会蜕化成一个列表,操作的时间复杂度会变成O(n2)。

更多细节可以看下hash map碰撞的解决方案。JDK用了一个叫开放寻址的方法,不过还有一种方法叫拉链法。所有哈希码一样的值都存储在一个链表里(说反了吧)。

我们来看下不同质量的哈希值有什么区别。我们将一个正常的String和它的包装类进行比较,这个包装类重写了hashCode()方法,所有对象都返回同一个哈希值。

private static class SlowString
{
    public final String m_str;

    public SlowString( final String str ) {
        this.m_str = str;
    }

    @Override
    public int hashCode() {
        return 37;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final SlowString that = ( SlowString ) o;
        return !(m_str != null ? !m_str.equals(that.m_str) : that.m_str != null);
    }
}

下面是一个测试方法。后面我们还会再用到它,所以这里还是简单介绍一下 。它接收一个对象列表,然后对列表中的每个元素依次调用Map.put(), Map.containsKey()方法。

private static void testMapSpeed( final List lst, final String name )
{
    final Map<Object, Object> map = new HashMap<Object, Object>( lst.size() );
    int cnt = 0;
    final long start = System.currentTimeMillis();
    for ( final Object obj : lst )
    {
        map.put( obj, obj );
        if ( map.containsKey( obj ) )
            ++cnt;
    }
    final long time = System.currentTimeMillis() - start;
    System.out.println( "Time for "  + name + " is " + time / 1000.0 + " sec, cnt = " + cnt );
}

——– output(自己测试是这样) ——–
Time for String is 0.02 sec, cnt = 100000
Time for SlowString is 0.03 sec, cnt = 100000

String和SlowString对象都是按照”ABCD”+i的格式生成的。处理100000个String对象需要0.041秒,而处理SlowString对象则需要82.5秒。

结果表明,String类的hashCode()方法明显胜出。我们再做另一个测试。先创建一个字符串列表,前半部分的格式是”ABCdef&”+i,后半部分的是”ABCdef&”+i+”ghi”(确保字符串的中间部分变化而结尾不变,不会影响哈希值的质量)。我们会创建1百万,5百万,1千万,2千万个字符串,来看下有多少字符串是共享哈希值的,同一个哈希值又会被多少个字符串共享。下面是测试的结果:

Number of duplicate hashCodes for 1000000 strings = 0

Number of duplicate hashCodes for 5000000 strings = 196
Number of hashCode duplicates = 2 count = 196

Number of duplicate hashCodes for 10000000 strings = 1914
Number of hashCode duplicates = 2 count = 1914

Number of duplicate hashCodes for 20000000 strings = 17103
Number of hashCode duplicates = 2 count = 17103

可以看到,共用同一个哈希值的字符串很少,而一个哈希值被两个以上的字符串共享的概率则非常小。当然了,你的测试数据可能不太一样——如果用这个测试程序测试你给定的字符串的话。

自动生成long字段的hashCode方法


许多IDE生成long类型的hashcode()的方式非常值得一提。下面是一个生成的hashCode()方法,这个类有两个long类型的字段。

public int hashCode() {
    int result = (int) (val1 ^ (val1 >>> 32));
    result = 31 * result + (int) (val2 ^ (val2 >>> 32));
    return result;
}

下面给只有两个int类型的类生成的方法:

public int hashCode() {
    int result = val1;
    result = 31 * result + val2;
    return result;
}

可以看到,long类型的处理是不一样的。java.util.Arrays.hashCode(long a[])用的也是同样的方法。事实上,如果你将long类型的高32位和低32位拆开当成int处理的话,生成的哈希值的分布会好很多。下面是两个long字段的类的改进后的hasCode方法(注意,这个方法运行起来比原来的方法要慢,不过新的hashCode的质量会高很多,这样的话hash集合的执行效率会得到提高,虽然hashCode本身变慢了)。

public int hashCode() {
    int result = (int) val1;
    result = 31 * result + (int) (val1 >>> 32);
    result = 31 * result + (int) val2;
    return 31 * result + (int) (val2 >>> 32);
}

下面是testMapSpeed 方法分别测试10M个这三种对象的结果。它们都是用同样的值进行初始化的。

Two longs with original hashCodeTwo longs with modified hashCodeTwo longs with modified hashCode
2.596 sec1.435 sec1.435 sec

可以看出,更新后的hashCode方法的效果是不太一样的。虽然不是很明显,但是对性能要求很高的地方可以考虑一下它。

高质量的String.hashCode()能做些什么

假设我们有一个map,它是由String标识符来指向某些值。map的key(String标识符)不会在内存的别的地方存储(某一时间可能有一小部分值是存储在别的地方)。假设我们已经收集到了map的所有记录,比如说在某个两阶段算法中的第一个阶段。下一步我们要通过key来查找map中的值。我们只会用map里存在的key进行查找。

怎样才能提升map的性能?前面你已经看到了,String.hashCode()返回的几乎都是不同的值,我们可以扫描所有的key,计算出它们的哈希值,找出那些不唯一的哈希值 :

Map<Integer, Integer> cnt = new HashMap<Integer, Integer>( max );
for ( final String s : dict.keySet() )
{
    final int hash = s.hashCode();
    final Integer count = cnt.get( hash );
    if ( count != null )
        cnt.put( hash, count + 1 );
    else
        cnt.put( hash, 1 );
}

//keep only not unique hash codes
final Map<Integer, Integer> mult = new HashMap<Integer, Integer>( 100 );
for ( final Map.Entry<Integer, Integer> entry : cnt.entrySet() )
{
    if ( entry.getValue() > 1 )
        mult.put( entry.getKey(), entry.getValue() );
}

现在我们可以创建两个新的map。为了简单点,假设map里存的值就是Object。在这里,我们创建了Map 和Map(生产环境推荐使用TIntObjectHashMap)两个map。第一个map存的是那些唯一的hashcode以及对应的值,而第二个,存的是那些哈希值不唯一的字符串以及它们相应的值。

final Map<Integer, Object> unique = new HashMap<Integer, Object>( 1000 );
final Map<String, Object> not_unique = new HashMap<String, Object>( 1000 );

//dict - original map
for ( final Map.Entry<String, Object> entry : dict.entrySet() )
{
    final int hashCode = entry.getKey().hashCode();
    if ( mult.containsKey( hashCode ) )
        not_unique.put( entry.getKey(), entry.getValue() );
    else
        unique.put( hashCode, entry.getValue() );
}

//keep only not unique hash codes
final Map<Integer, Integer> mult = new HashMap<Integer, Integer>( 100 );
for ( final Map.Entry<Integer, Integer> entry : cnt.entrySet() )
{
    if ( entry.getValue() > 1 )
        mult.put( entry.getKey(), entry.getValue() );
}

现在,为了查找某个值,我们得先查找第一个哈希值唯一的map,如果没找到,再查找第二个不唯一的map:

public Object get( final String key )
{
    final int hashCode = key.hashCode();
    Object value = m_unique.get( hashCode );
    if ( value == null )
        value = m_not_unique.get( key );
    return value;
}

在一些不太常见的情况下,你的这个不唯一的map里的对象可能会很多。碰到这种情况的话,先尝试用java.util.zip.CRC32或者是java.util.zip.Adler32来替换掉hashCode()的实现(Adler32比CRC32要快,不过它的分布较差些)。如果实在不行,再尝试用两个不同的函数来计算哈希值:低32位和高32位分别用不同的函数生成。hash函数就用Object.hashCode, java.util.zip.CRC32或者java.util.zip.Adler32。

(译注:这么做的好处就是压缩了map的存储空间,比如你有一个map,它的KEY存100万个字符串的话,压缩了之后就只剩下long类型以及很少的字符串了)

set的压缩效果更明显

前面那个例子中,我们讨论了如何去除map中的key值。事实上,优化set的话效果会更加明显。set大概会有这么两个使用场景:一个是将原始的set拆分成多个子set,然后依次查询标识符是否属于某个子set;还有一个是写一个拼写检查器(spellchecker )——有些要查询的值是预想不到的值(比如拼写错误了),而就算出了些错误的话影响也不是很大(如果碰巧另一个单词也有同样的hashCode,你会认为这个单词是拼写正确的)。这两种场景set都非常适用。

如果我们延用前面的方法的话,我们会得到一个唯一的hashcode组成的Set,不唯一的hashCode得到的是一个Set。这里至少能优化掉不少字符串的空间。

如果我们可以把哈希值的取值限制在一定的区间内(比如说2^20),那么我们可以用一个BitSet来代替Set,这个在BitSet一文中已经提到了。一般来说如果我们提前知道原始set的大小的话,哈希值的范围是有足够的优化空间的。

下一步就是确定有多少标识符是共享相同的哈希值的。如果哈希值碰撞比较多的话,改进下你的hashCode()方法,或者扩大哈希值的取值范围。最完美的情况就是你的标记符全都有唯一的hashcode( 这其实不难实现)。优化完的好处就是,你只需要一个BitSet就够了,而不需要存储一个大的字符串集合。

总结

改进你的hashCode算法的分布。优化它比优化这个方法的执行速度要重要多了。千万不要写一个返回常量的hashCode方法。

String.hashCode的实现已经相当完美了,因此很多时候你可以用String的hashCode来代替字符串本身了。如果你使用的是字符串的set,试着把它优化成BitSet。这将大大提升你程序的性能。

参考链接
JDK1.7_API
英文原文链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值