抱歉,你查看的文章不存在

【源码分析】面试问烂的equals和各种字符串、Integer比较

今天在空闲时间聊天时发现,面试题中的equals问题,以及String、Integer等的判等问题还是讨论的比较激烈而且混乱。。。(滑稽)

其实网上有非常多关于这种面试题的文章或者博客,其实多去看看就可以。

不过。。。看了好多,都是通篇理论,感觉很干。思考之后,决定开一个新的模块,通过源码来解释面试题中、或者常见的一些问题中的原理以及本质,帮助有疑惑的小伙伴更容易、更深入的理解原理,以及相应的源码设计。

说到正题,这篇文章讨论的是关于equals在不同对象、以及特殊类型String、Integer上的实际原理。

1. ==与equals

这一部分属于J2SE最基础的东西了,算是常识性的,也没什么好说的。

  • 基本数据类型,只有==,它是比较两个变量的值是否一致
  • 引用数据类型的比对需要区别对待
    • ==:比较两个对象的内存地址是否相同
    • equals:调用一个对象的equals方法,与另一个对象比对

就因为这些最基本的知识,引发了很多面试题,咱们一一列举


2. String的常见面试题

public class Demo {
   public static void main(String[] args) throws Exception {
       String str1 = "123";
       String str2 = "123";
       String str3 = new String("123");
       String str4 = new String(str1);
       StringBuilder sb = new StringBuilder("123");
       System.out.println(str1 == str2);
       System.out.println(str1 == str3);
       System.out.println(str1 == str4);
       System.out.println(str3 == str4);
       System.out.println(str1.equals(str2));
       System.out.println(str1.equals(str3));
       System.out.println(str1.equals(sb.toString()));
       System.out.println(sb.equals(str1));
   }
}

上面的源码列举了最常见的几种判断String是否相等的情况,小伙伴们可以先不要往下拉,先思考一下输出结果都应该是怎样的。。。

。。。。。。

。。。。。。

。。。。。。

。。。。。。

。。。。。。

。。。。。。

输出结果:1 5 6 7为true,其余为false(你都答对了吗)

下面咱们来一一分析。

2.1 "123" == "123"

        String str1 = "123";
        String str2 = "123";
        System.out.println(str1 == str2); // true
        System.out.println(str1.equals(str2)); // true

第一种情况,要了解Java虚拟机在处理String时的特殊机制:字符串常量池。

简单来说,如果某一个字符串在程序的任意位置被声明出来,则Java虚拟机会将该字符串放入字符串常量池缓存起来,如果后续还有其他字符串变量需要引用该字符串,只需要将该变量引用指向常量池内的字符串常量即可。

当然,这个机制的前提是:String类必须是不可变的。

【源码】String类的声明:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

String被声明为final,也就是不允许有子类,同时一个String就是一个不可变的对象。

所以从上面的解释中,也就明白了,两个对象其实都是指向了字符串常量池中的同一个字符串常量!所以用==比对的结果是true。

那==都是true了,对于equals方法来讲,那肯定也是true。

2.2 “123” == new String("123")

        String str1 = "123";
        String str3 = new String("123");
        String str4 = new String(str1);
        System.out.println(str1 == str3); // false
        System.out.println(str1 == str4); // false
        System.out.println(str3 == str4); // false
        System.out.println(str1.equals(str3)); // true

这几种情况是面试问的最多的。。。

网上大片的理论核心都是说:因为构造方法总会创建新的对象,所以上面的str1, str3, str4都各不相同。。。

可为什么是这样呢?

【源码】String的构造方法

    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
    
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

可以看到,传入String的构造方法,其实是将参数中String的value赋给当前正在实例化的String对象,而这个value是一个char[]。

也就是说,这两个对象,是两个完全不同的对象,但共享同一个char[]

所以==比对的是内存地址,自然也就不同。但为什么equals方法返回的是true呢?

【源码】String的equals方法

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

String类重写了equals方法,所以不再是仅仅调用==而已。。。

上面也提了,如果两个对象的内存地址都相同,那自然是同一个对象,所以在equals方法中也处理了这一步。

关键是下面的if结构:

在这里,针对String进行额外的比对,也就是比对char[]中的内容是否完全一致

而上面的几种情况,都是由"123"这个字符串复制而来,所以char[]自然都是同一个,也就证明这三个String对象,内容相同,但内存地址不同

2.3 String与StringBuilder

        String str1 = "123";
        StringBuilder sb = new StringBuilder("123");
        System.out.println(str1.equals(sb.toString())); // true
        System.out.println(sb.equals(str1)); // false

StringBuilder可以理解是一种字符串拼接中间件吧,它的内部其实也是维护了一个char[]。

【源码】StringBuilder与其父类的部分源码

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence{
    
    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }
    
    //......
}

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;
    
    //......
}

说点题外话,原题中这种情况下,这个StringBuilder对象sb,在实例化时将一个String传入,其实是将这个String塞入内部的char[],并额外扩容16个空间

也就是:[123 ](16个空格)

回到正题上。那在比较一个String对象和一个StringBuilder对象时,就需要看StringBuilder有没有重写equals方法。

查看源码,使用IDE的方法搜索,并没有找到StringBuilder有重写equals方法,父类亦如此。

也就是说,最终调用的还是Object的equals方法,而。。。

【源码】Object的equals方法

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

Object的equals方法,也就是==

显而易见,一个是String,一个是StringBuilder,类型都不相同,肯定就不同了。。。

那为什么str1.equals(sb.toString())会返回true呢?

【源码】StringBuilder的toString方法

    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

在这里,它重新创建了一个String对象,这个对象是截取了StringBuilder内部维护的char[]的一部分,这个count是最后一个有字符的数组索引+1,那自然就是有内容的那一部分。

那原来的str1,内容就是"123",上面构造的StringBuilder,内容也是"123",那调用toString方法时,返回的自然也就是"123",最后两个字符串进行比对,自然返回true。


3. Integer的常见面试题

public class Demo {
    public static void main(String[] args) throws Exception {
        Integer num1 = 100;
        Integer num2 = 100;
        Integer num3 = 200;
        Integer num4 = 200;
        Integer num5 = new Integer(100);
        Integer num6 = new Integer("100");
        Integer num7 = Integer.valueOf(100);
        Integer num8 = Integer.valueOf("100");
        Integer num9 = Integer.parseInt("100");
        Long l1 = 100L;
        Integer num10 = l1.intValue();
        
        System.out.println(num1 == num2); 
        System.out.println(num3 == num4); 
        System.out.println(num1 == num5); 
        System.out.println(num1 == num6); 
        System.out.println(num1 == num7); 
        System.out.println(num1 == num8); 
        System.out.println(num1 == num9); 
        System.out.println(num1 == num10);
        System.out.println(num5 == num6); 
        System.out.println(num5 == num7); 
    }
}

上面的源码列举了最常见的几种判断Integer对象是否相等的情况,小伙伴们可以先不要往下拉,先思考一下输出结果都应该是怎样的。。。

。。。。。。

。。。。。。

。。。。。。

。。。。。。

。。。。。。

。。。。。。

输出结果:1 5 6 7 8为true,其余为false(你都答对了吗)

下面咱们来一一分析。

3.1 自动装箱

        Integer num1 = 100;
        Integer num2 = 100;
        Integer num3 = 200;
        Integer num4 = 200;
        System.out.println(num1 == num2); // true
        System.out.println(num3 == num4); // false

我们都知道,jdk1.5后引入了自动装箱机制,基本数据类型可以在编码时直接隐式转换为引用数据类型(包装类)。

实际上,我们编写的这段源码,经过编译再反编译,拿到的反编译源码 ↓

【反编译】上述源码的反编译后的结果:

        Integer num1 = Integer.valueOf(100);
        Integer num2 = Integer.valueOf(100);
        Integer num3 = Integer.valueOf(200);
        Integer num4 = Integer.valueOf(200);

那既然在进行==对比时还打印了true,唯一的可能性是Integer.valueOf方法,两次返回了同一个对象!

那我们就需要追到valueOf方法中一探究竟。

【源码】Integer的部分源码:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

    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() {}
    }

可以看到,在valueOf方法中,实际上是先判断传入的数是否在一个范围内:如果在,会从IntegerCache中取,否则直接创建新的Integer对象

这个IntegerCache是一个高速缓冲区,它缓冲了从-128到127之间的所有整数的包装对象(源码中low=-128, high=127)

传入的数为100,在缓冲区范围内,故两次都取的缓冲区的对象,自然也就是同一个对象了。

传入的数为200,不在缓冲区范围内,故两次都调用了构造方法,自然就是两个不同的对象。

3.2 构造方法

        Integer num1 = 100;
        Integer num5 = new Integer(100);
        Integer num6 = new Integer("100");
        System.out.println(num1 == num5); // false
        System.out.println(num1 == num6); // false
        System.out.println(num5 == num6); // false

构造方法,必定是创建新对象,所以上述实际是一个高速缓冲区的对象和两个独立创建的对象

【源码】Integer的两个构造方法:

    public Integer(int value) {
        this.value = value;
    }

    public Integer(String s) throws NumberFormatException {
        this.value = parseInt(s, 10);
    }

可见两种构造方法都是将值赋给了即将实例化的对象的value成员中,那自然就是不同对象了。

3.3 valueOf与parseInt

        Integer num1 = 100;
        Integer num8 = Integer.valueOf("100");
        Integer num9 = Integer.parseInt("100");
        System.out.println(num1 == num8); // true
        System.out.println(num1 == num9); // true

valueOf不说了,上面提到了,是从高速缓冲区取对象,但是parseInt方法呢?

注意:不要被这种陷阱迷惑!parseInt的返回值是int!

    public static int parseInt(String s) throws NumberFormatException {
        return parseInt(s,10);
    }

也就是说,返回值为int,还不能直接将值赋给num9,还需要多做一步,也就是valueOf。。。

说白了还是从高速缓冲区拿。。。

【反编译】上述源码反编译的结果:

    Integer num1 = Integer.valueOf(100);
    Integer num8 = Integer.valueOf("100");
    Integer num9 = Integer.valueOf(Integer.parseInt("100"));

分明就是执行了三次valueOf!

3.4 Integer与Long

        Integer num1 = 100;
        Long l1 = 100L;
        Integer num10 = l1.intValue();
        System.out.println(num1 == num10); // true

调用Long对象的intValue操作,其实看方法名也能看出来,返回的是int。。。又是int。。。

好了,不用我说了,都明白这种骚套路了吧,又是valueOf。。。

也甭看源码了,直接看反编译的结果吧。。。

【反编译】上述源码反编译的结果:

    Integer num1 = Integer.valueOf(100);
    Long l1 = Long.valueOf(100L);
    Integer num10 = Integer.valueOf(l1.intValue());

好吧,原来到最后都是valueOf方法,这波骚套路我算是记下来了。。。

(完)

(adsbygoogle = window.adsbygoogle || []).push({}); function googleAdJSAtOnload() { var element = document.createElement("script"); element.src = "//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"; element.async = true; document.body.appendChild(element); } if (window.addEventListener) { window.addEventListener("load", googleAdJSAtOnload, false); } else if (window.attachEvent) { window.attachEvent("onload", googleAdJSAtOnload); } else { window.onload = googleAdJSAtOnload; }

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

编辑于

LinkedBear的个人空间

0 篇文章14 人订阅

相关文章

来自专栏阿凯的Excel

Python读书笔记16(循环大法好!while少不了)

今天和大家分享一个新的循环语句while! 之前学过for循环语句用于遍历列表、元组、字典内的值,我们重温一下! ? 这种for循环语句是根据列表元素值的数量来...

38350
来自专栏java一日一条

一个以前没有注意的问题:java构造函数的执行顺序

昨天在改一处代码时发现执行的过程和预想的不一样,仔细探究才发现是构造器执行顺序问题.(汗自己一下,基础不够扎实) 特地做了一些尝试然后把java构造器的执行顺...

9620
来自专栏Java帮帮-微信公众号-技术文章全总结

第二十天 IO-异常file类【悟空教程】

在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。

14050
来自专栏专注 Java 基础分享

关于类的对象创建与初始化

今天,我们就来解决一个问题,一个类实例究竟要经过多少个步骤才能被创建出来,也就是下面这行代码的背后,JVM 做了哪些事情?

42360
来自专栏小樱的经验随笔

【Java学习笔记之三十二】浅谈Java中throw与throws的用法及异常抛出处理机制剖析

异常处理机制 异常处理是对可能出现的异常进行处理,以防止程序遇到异常时被卡死,处于一直等待,或死循环。 异常有两个过程,一个是抛出异常;一个是捕捉异常。 抛出异...

35860
来自专栏java一日一条

一个以前没有注意的问题:java构造函数的执行顺序

昨天在改一处代码时发现执行的过程和预想的不一样,仔细探究才发现是构造器执行顺序问题.(汗自己一下,基础不够扎实) 特地做了一些尝试然后把java构造器的执行顺...

7120
来自专栏Java学习网

Java面试题系列之基础部分(二)——每天学5个问题

Java基础部分学习的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io的语法,虚拟机方面的语法,这些都是最基...

27150
来自专栏菜鸟前端工程师

JavaScript学习笔记022-原型链0原型继承0对象的深浅拷贝extends

7210
来自专栏企鹅号快讯

《常见排序算法》

1.概述 常见的排序算法,虽然很基础,但是很见功力,如果能思路清晰,很快写出来各个算法的代码实现,还是需要花一点功夫的,今天,就跟大家盘点下常用的一些算法。 冒...

22270
来自专栏十月梦想

js对象创建

15030

扫码关注云+社区

领取腾讯云代金券