从头学Java之"equals"和"=="的故事

    简言:为什么说叫这个标题,因为本人甚是很惭愧,第一是已经很久没有更新博客了,总是各种理由和借口,有一天我看到hyman的一篇博客,那篇博客没有聊技术更多的是聊hyman大神个人写博客的经历,看后甚是感触很大;第二是虽然不更新了博客,但是偶尔还会有一些支持我的朋友路过赞下以前的文章,心里老实说有种莫名的感伤,时间都去哪了。第三为什么叫这个标题,因为发现自己的之前的java学的简直太次了,所以决定在写android的同时,重头开始学习java。这次Java学习之路不同于以前的蜻蜓点水,浪费时间,目的在于加深和巩固。好了,矫情了那么久进入正题,今天来聊下"equals"和“==”的关系。

   “equals”与“==”之间的关系可以说大家都已经非常清楚了,但是对于一个重头学习java的我来说,请大神们不要喷。目的在于学习和巩固。这两者关系是android和java面试和笔试题目中经常考核的问题,也很简单。下面将从这几方面聊下他们之间的故事。

 一、“equals”和“==”的关系。

     大家都很清楚 “equals”比较是两个对象的内容是否相等,而“==”则表示比较的是两个对象的引用是否相等(也即是两个引用是否指向同一个对象)

注意:equals这个方法不适用基本数据类型,而==则适用基本数据类型(故下面讨论的话题,不包含基本数据类型的讨论,只针对引用数据类型)

好就这些了吗?貌似就这些了,没错我第一次学习的时候也就学到这了。一般的笔试题也就考这么简单。但是我觉得下面几个例子也许你会感兴趣的。

第一个非常简单的例子相信你看一眼就知道答案了。

package com.mikyou.test;

public class TestDemo1 {
  public static void main(String[] args) {
	Integer num1 = new Integer(100);//使用包装器类型(属于引用数据类型),可以使用equals
	Integer num2 = new Integer(100);
	System.out.println(num1 == num2);
	System.out.println(num1.equals(num2));
 }
}
输出结果:

输出结果毋庸置疑,证明了我们之前说的equals比较的是两个对象的内容,而"=="则比较的是两个对象的引用是否相等。

好,记住这句话,那么请看第二个例子。

package com.mikyou.test;

public class TestDemo2 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
      Num num1 = new Num();//创建一个自定义Num类的实例对象,属于引用类可以使用equals
      num1.n = 100;
      
      Num num2 = new Num();
      num2.n = 100;
      
      System.out.println(num1.equals(num2));
      
	}
}
输出结果:

大家可能看到这个结果多少会有些疑惑吧,没错呀,引用类型使用equals比较的是内容,我们分别给num1、num2对象中的n成员赋了相同的值,比较的是内容的话应该输出结果是true才对呀。为何会有这样的思维呢?这就是我们平时不好的习惯,平时就像是在喊口号似的,“equals比较的是内容,==比较的是引用”。貌似我们得出以上分析结论是背下来,而没有去真正的分析为什么会是这样的呢。因为这就是我的真实写照,第一次学Java就是背口号似的把这个给硬记下来了,也不知道为啥。

既然是重头学习,那就一起聊下为什么吧?

注意:实际上equals的默认行为才是真正的比较引用而不是比较内容。可是为何第一个例子对的呢?难道第一个例子输出结果有问题?

其实第一个例子没有问题,很正确,为什么会有这样的结果?大家仔细看下第一个例子用的是Integer类包装器类属于Java中的类库,而第二例子属于我们自己定义的类

问题就出现在这。因为equals的默认行为是比较引用,所以第二个例子当然结果当然就是false了,而第一个例子使用的Integer包装器类型为什么是true,因为在大部分的java的类库中都对equals这个方法进行了覆盖,为了比较方便重写了equals方法,使得所有java类库中的equals方法都是比较内容,从而修改了equals比较引用的默认行为。因为在Java类库用的比较多,所以一般就这样把equals说成比较内容相同。如果不信的话,我们来看下Integer包装器类的源码,源码中equals是否被重写了。

可以看到Integer包装器类中确实重写了equals将一个Object类型转化成Integer类型并取出值比较内容是否相等。

为了做比较我们一起来看下最原始的equals方法长得啥样。


很明显我们可以看到最原始的equals方法比较的是引用而不是内容,比较传入对象引用是否与当前的对象引用相等,而且很明显一个是equals应该是直接属于Object这个类的一个原始方法。所以到这里我想应该对于equals是比较内容还是引用有个更清楚认识吧。

说完上面的“equals”再来聊下“==”中一个有趣的问题。这个问题也是我在逛csdn的时候看到某大神写了的,因为看着与我这篇博客符合,所以放在一起,至于哪个大神不好意思忘了,写进来也就给我这个重头学Java的人一个学习。

不管太多,先来看段很简单很简单的代码:

package com.mikyou.test;

public class TestDemo3 {
	public static void main(String[] args) {
		Integer num1 = 100;
		Integer num2 = 100;
		Integer num3 = 200;
		Integer num4 = 200;
        System.out.println(num1 == num2);
        System.out.println(num3 == num4);
	}
}

输出的结果:


看到上面的例子你是不是又感到疑惑了,不瞒你说,我刚开始的时候看到也懵逼了,因为第一次Java学的很浅显。没有深入学习。

这个例子按照我们前面例子来说,Integer属于包装器类型,而且==比较的是引用,那么按道理应该输出的是false,false才对呀。如果我们这样分析的话,还是思维定式了,思维太死了。得出这个结论只因为我们对Integer这个包装器类不熟悉。

注意:实际上在Integer包装器类中还有一个IntegerCache内部类,这是Integer包装类中的一个缓存类,用于缓存-128到127大小的整数。首先说下为什么会有这个缓存的类的存在,因为我们在编程过程-128到127范围这些数使用的频繁度更高,为了提高效率和优化性能,所以设计一个缓存的类来帮助实现。

为了更好理解我们来看下Integer类中的IntegerCache源码

/**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }
从上面的源码我们缓存的范围是-128~127
例子中的Integer num1 = 100;实际上就是Integer num1 = Integer.valueOf(100);

我们可以一起再来看下,Integer中的valueOf方法的具体实现。

  /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

我们可以很明显的看到,首先会判断传入进去的数据是否在缓存中(-128-127)中,如果在的话就直接取缓存中cache数组中的数,这个cache数组中我们清楚看到它是事先就在静态代码块中预先初始化好了,也就是说cache数组中的包含-128~127范围的对象引用。例子中第一次100,因为在缓存中,就去找到cache数组中的100所对应的Integer对象引用,第二次100也还是将这个cache为100的引用赋值。所以两次的100引用所指的是同一个对象,所以输出为true.而超过缓存范围的我们可以看到会重新new 一个Interger对象,所以第一次200不在范围内,所以会new一个新的对象,第二次也会new一个新的对象,很明显不是同一个对象所以输出false。

说到这是不是觉得很神奇。那么既然很神奇的话,不妨做个更加神奇的事。

通过源码我们可以看到缓存的实现主要是通过IntegerCache类中的cache数组,而这个数组的大小就是保存从-128到127的总共为256长度的元素对象。我们可以通过Java中的反射机制,来修改这个缓存数组,从而使得通过Integer创建的对象的数且在该缓存范围的数大小由我们而定。来看这段代码。


package com.mikyou.test;

import java.lang.reflect.Field;

public class TestDemo4 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		try {
			Class cache = Integer.class.getDeclaredClasses()[0]; //得到Integer类中的内部类的Class类型
			  Field myCache = cache.getDeclaredField("cache"); //我们从源码可看到cache这个数组是static所以直接通过反射出的类即可得到这个缓存数组
			  myCache.setAccessible(true);//设置个数组是可访问的
			  Integer[] newCache = (Integer[]) myCache.get(cache);
			  newCache[127] = newCache[129]; 
			  //我们知道cache是-128~127的数组对应0~256数组下标,下标为127的为-1,下标为129的为1;此操作也就是将缓存的中的1覆盖-1
		} catch (SecurityException | NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
			e.printStackTrace();
		}
		  Integer a = -1; 
	      Integer b = a * 3; 
	      System.out.println(b);//-1 * 3 = -3,可最后b=3,因为通过反射将缓存中的数据-1修改为1,然后1 * 3 =3 
	}

}
输出结果:

说到这,今天的有关equals和==内容就说到这了,欢迎大家留言,一起分享有关equals和==的故事。好了已经很晚了,该睡了,想想明天的还得早起上班,不过很开心,已经很久都没有这种熬夜写博客的感觉了。




猜你喜欢

转载自blog.csdn.net/u013064109/article/details/52878345