== 和 equles()基于字符串、基本数据类型、包装类应用的不同和原理



前言

对于 == 和 equals() 大家都很熟悉,大多也知道结论,但是运用的时候,有时候根据结论来,完全是摸不着头脑,所以我在这系统的简述一下关于两者在基本数据类型、字符串类型、包装类这三个应用上的区别。

下面我主要是说 == 的运用,因为equals()对于字符串就是比较内容,不重写就是等号操作,所以我主要从堆栈和代码方面说明等号操作在不同情况应用上的不同


一、关于堆栈内存的解释

如果大家对堆栈没有太大的概念,基本类型数据、字符串数据怎么存储的,可以先去看一下下面这篇
浅淡JVM内存结构

二、== 和 equlas() 结论定义

  • == 比较的是内存地址是否一致,是不是同一个对象;
  • equals()被String重写比较的是内容是否一致,如果没有被重写,相当于 ==

String重写equals()源码,发现如果地址一致,就直接返回true,而如果判断是字符串的时候,就会判断长度,长度不一致,返回false,一致才会判断字符串值是否一致。

 public boolean equals(Object anObject) {
        if (this == anObject) {  判断地址,相等返回true
            return true;
        }
        if (anObject instanceof String) {e
            String anotherString = (String)anObject; 强转,使其可以调用String特有方法
            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;
    }

二、基本类型数据

1、示例

  • 基本数据类型:int、long、short、byte、char、float、double、boolean
  • 针对基本数据类型时,只要数据类型,且值相同, 操作符== 输出结果则为 true
     public static  void main(String[] args) {
       int a = 10;
       double b = 10;
       float c =10f;
       System.out.println(a==b);   输出true
       System.out.println(b==c);   输出true
    }

可知,所有的变量都定义在主函数(方法)中,说明主函数(方法)在压栈时,才会处理这些数据,把在这些局部变量存储到栈帧的局部变量表中。
比如 int a = 10 进入栈顶的时候,JVM会查询局部变量表,如果局部变量表中,没有字面值为10的值,则把10存入。当其他局部变量查询到局部变量表中已经有字面值10的地址时,会把引用直接指向该地址,所以输出才都是true.

一句话,只要局部变量表中存入已有的数据类型值,定义其他数据类型时,直接指向该值地址。

在这里插入图片描述


三、字符串类型

1、示例

1、先来个经典示例

    public static  void main(String[] args) {
        String s1 = "ab";
        String s2 = "a" + "b";
        System.out.println(s1 == s2);  输出true
    }

为什么输出true?

1、我们知道String是不可变的,所以它在编译期就已经确定了,所以String s2 = “a” + "b"会被编译器优化,直接赋值,即 s2 = “ab”
2、被加载到JVM时,方法被压栈,引用类型s1、s2随之入栈,但是JVM单独为String数据类型在堆中开辟一个字符串常量池空间,所以字面值 ab 会加载至字符串常量池中
3、s1的字面值存入字符串常量池后,s2在字符串常量词中查询到已存在 ab 的地址,所以直接s2引用该地址

可能有人问,都在方法内,为什么不是存入局部变量表?

1、局部变量表只存储基本数据类型和引用类型数据(不是对象本身),所以只存储了引用类型 s1、s2,没有存储字面值
2、JVM为String类型开辟了常量池技术,所以字符串会指向常量池的地址。


2、如下的代码示例,又为什么是false呢?

    public static  void main(String[] args) {
        String s1 = "ab";
        String s2 = new String("ab");
        System.out.println(s1 == s2);   输出false
    }

1、首先需要知道使用String s2 = new String(“ab”);创建字符串对象时的两大步骤

  • 其一:在编译过程中,会把字符串放入到静态常量池在类加载后,字符串会被加载至字符串常量池中,如果字符串常量池中已经存在该字符串,则忽略此步骤;
  • 其二:主要在于 new 关键字,new 关键字在堆中创建并初始化该对象,静态常量池字符串地址引用堆地址,建立关系。

2、所以s1压栈,ab进入字符串常量池,s2 在入栈的时候,先查询字符串常量池是否有字符串ab,发现已存在,则直接在堆中创建开辟一个新的内存空间,然后字符串常量池中引用s2堆地址
3、== 比较的是两者的地址,s1 的字面值地址在字符串常量池中,s2 的字面值地址则在堆中,所以自然两者不对等

在这里插入图片描述


3、两个字符拼接呢?再看如下代码

    public static  void main(String[] args) {
        String s1 = "ab";
        String s2 = "ab";
        String s3 = s1 + s2;
        String s4 = "abab";
        System.out.println(s3 == s4);  输出false

    }

1、当String的字面值不确定时,会到方法压栈的时候,即运行时才会确定
2、当s1、s2字面值都确定时,则在编译器就会存储至静态常量池中,但s3的字面值不确定,虽然它被s1+s2赋值,但是本身可改变,编译器不知道具体,所以到运行期间,才会 new 创建对象赋值于s3
3、s3的地址创建在堆中和s4的地址并不一致,所以输出为false

换个方式,加个final

    public static  void main(String[] args) {
        final String s1 = "ab";
        final String s2 = "ab";
        String s3 = s1 + s2;
        String s4 = "abab";
        System.out.println(s3 == s4);  输出true

    }

为什么输出 true 呢?

我们知道String的不可变性,String类不可更改,意味着每次定义和修改都是创建新的对象,所以定义的引用类型还是可以修改的,只是变成了一个新的对象,但是如果加上了final关键字,意味着引用类型变量也不可更改了,即s1、s2不可修改了,所以s3也确定了,不可更改,在编译期间就直接赋值于s4=“abab”,所以s3和s4相等

上面是我个人的理解,具体原理如下:
String s3 = s1 + s2;相当于String s3 = new StringBuilder(s1).append(s2).toString();
创建一个对象StringBuilder使用append()和toString()方法拼接两个字符串,然后创建并返回一个字符串

    public String toString() {
        return new String(value, 0, count); 创建并返回一个字符串
    }    

主要是因为String类的不可变性,每次字符串拼接都会创建一个新的字符串对象,存在大量的对象创建,如果没有及时回收,会造成大量的内存资源的浪费,所以JVM为了优化性能,其StringBuffer对象则代表一个字符序列可变的字符串,可动态改变不会产生新的对象,所以拼接使用StringBuilder的append方法,避免了这种对象的大量创建和消耗。


4、 intern() 方法

  • 当调用 intern() 方法时,如果字符串池中已经存在相同内容的字符串,则返回字符串池中的引用;否则,将该字符串添加到字符串池中,并返回对字符串池中的新引用
  • 简而言之,intern()方法就是为了把new出来的字符串加入字符串常量池中

看下面代码:此时字符串常量池中只有Hello World和!,但没有Hello World!,执行s0.intern()时,s0字符串加入字符常量池中,s1引用s0地址,所以s0、s1的地址都一致

        String s0=  new StringBuilder().append(new String("Hello World")).append(new String("!")).toString();; //创建String并返回
        String s1=s0.intern();  //此时字符串常量池中没有Hello World!
        System.out.println(s1 == s0);  输出true

在这里插入图片描述


如果字符串在字符串常量池中存在,又会是怎么的呢?

        String s0=  new StringBuilder().append(new String("Hello World")).append(new String("!")).toString();; //创建String并返回
        String s1="Hello World!";
        String s2=s0.intern();  //此时字符串常量池中存在Hello World!
        System.out.println(s0 == s1);  输出false
        System.out.println(s1 == s2);  输出true

因为字符串常量池中已存在字符串Hello World!,所以s0.intern()方法直接返回s1在字符串常量池中的地址,并赋予s2
在这里插入图片描述

再来个对比,如下

        String s4 = new String("abcd");
        String s5 = s4.intern();
        System.out.println(s4 == s5);  输出false

还记得new String()吧,String s4 = new String(“abcd”);会先查询字符串常量池,没有则创建一个字符串在常量池中,然后再在堆中,创建实例,s4.intern()方法调用时,发现字符串常量池中已经有abcd字符串,知道把字符串常量池中的地址赋予s5,所以s4的指向地址在堆中,而s5指向地址在常量池中,自然不对等。

当然也还有其他情况,但是记住原理,都能做出来


2、总结

  1. 普通定义字符串时,在编译期已经确定并优化加号,该字符串会加入常量池,如果字符串一致,则 == 输出true
  2. new字符串时,会在常量池中创建该字符串对象,并在堆中创建该字符串对象实例,即有两个对象。分为两个情况,字符串常量池没有该字符串,则创建,已有则常量池返回堆中地址,建立关系。
  3. intern() 方法主要是把字符串插入字符串常量池中,如果已有,则直接引用常量池地址,如果没有,则插入字符串,并使字符串地址引用堆中地址

四、包装类类型示例

1、装箱和拆箱

基本数据类型都有自己的包装类:Integer、Long、Character、Byte、Short、Boolean、Float、Double
Float、Double并没有实现自己的缓存机制
在这里插入图片描述
简单理解就是:装箱就是基本类型转换成封装类型, 拆箱就是封装类型转换成基本类型
用int、Integer举例

装箱:基本数据类型——>包装类

Integer a = 2;    等于 Integer a =Integer.valueOf(2);     

部分源代码

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)   判断是否在范围区间内【-128 -127return IntegerCache.cache[i + (-IntegerCache.low)];  是,则返回IntegerCache缓存中的数据
        return new Integer(i);  不是,新建一个对象
    }

拆箱:包装类——>基本数据类型

        Integer a = 2;
        int i = a.intValue();  

总之其他基本数据类型也差不多如此,这里不多说,原理挺简单的,大家可以看一下源码即可

结论:

  1. Byte、Integer、Short、Long缓存范围都是【-128 -127】,在这个范围内的数据,直接从缓存中取出该数据实例即可,如果不是这个范围内,则需要new,在堆中新建实例对象
  2. Float、Double并没有实现自己的缓存机制
  3. Character取值范围在【0-127】之间

2、示例

直接写结论:

  • Integer 在范围内时,直接从缓存区拿数据,数字相同即==输出true
  • 包装类数值不在范围内时,需要在堆中创建一个对象
  • 基本数据类型和包装类对比时,只要不是new,数字相同即==输出true
    public static  void main(String[] args) {
        int a = 128;
        Integer s1 = 127;   //相当于   Integer s1 = Integer.valueOf(127);
        Integer s2 = 127;   //相当于   Integer s2 = Integer.valueOf(127);
        Integer s3 = 128;   //相当于   Integer s3 = new Integer(128);
        Integer s4 = 128;   //相当于   Integer s4 = new Integer(128);
        Integer s5 = new Integer(127);


        System.out.println(s1==s2);  输出true  都在范围,直接从缓冲区取
        System.out.println(s2==s3);  输出false s3不在范围内,在堆中new对象,地址不一样
        System.out.println(s2==s4);  输出false s5在堆中new对象,地址不一样
        System.out.println(a==s4);   输出true  可以理解为a在局部变量表中,s4直接获取
    }

总结

以上内容,不懂可以评论区询问,我看到,就回你
包装类有一部分知识大家可以去了解一下,比如类型转换,笔试题通常有(嘿嘿,虽然不多)

  • 56
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值