字符串比较--小问题大智慧

String相等之谜

引言:在最近的Java学习中,遇到一些关于字符串的小问题,现在此做一些总结

Java中的“相等”

等号大比拼

  • ==

    众所周知,在 Java 中如果用 == 比较两个对象,那就是比较两个对象是否在内存的同一个位置(地址是否相同)。

  • equals

    在 Java 中,所有类的父类 Object 存在一个 equals 方法,String类复写了这个方法,它实现了真正的字符串比较,代码如下:

    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;
        }
    
  • hashCode

    通常情况下,当我们重写 equals 方法时,我们需要重写 hashCode方法 以保持一致性。默认的 hashCode 方法与地址有关。

热身--JVM内存结构

概览

看到上图,我们简单介绍一下 Java StackMethod AreaHeap 。这几个我们经常提到的内存区。

  • Java堆(Heap)

    对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。(我们 new 出来的东西都放在这里)

  • 方法区(Method Area)

    方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

  • JVM栈(JVM Stacks)

    与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

    局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。

    其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

    在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

    讲完了 内存的概念, 我们就可以引入下一个问题了,码来!

    public static void main(String[] args) {
            String t1 = "abc";
            String t2 = new String("abc");
            System.out.println(t1==t2);
    
        }
    

    我们看到 t1t2 的值都是 "abc",直觉上来看结果 应该返回 true,但是运行以后 返回的是 false ,这是为什么呢?(因为他们的地址不同)。那么他们分别在哪呢?这里我们就要引入常量池的概念了。

常量池

Java中的常量池,通常指的是运行时常量池,它是方法区的一部分,一个JVM实例只有一个运行常量池,各线程间共享该运行常量池。

Java常量池简介:Java常量池中保存了一份在编译期间就已确定的数据。它里面包括final常量的值(包括成员常量、局部常量和引用常量)、以及对象字面量的值。

在编译期间,每当给常量赋值它就会去检测常量池中是否存在该值,若存在直接返回该值的地址给常量,若不存在则先在常量池中创建该值,再返回该值的地址给常量。因此常量池中不可能出现相等的数据。

  • final常量

一切经final关键字修饰的变量均为常量,final常量必须在定义时就赋初值,否则编译不通过。

  • 对象字面量

对象字面量是指直接以一常量给对象赋值,而不是在堆空间new出一个对象实例。

常见的两种对象字面量:基本类型的包装类对象字面量、String对象字面量。

String Pool

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。

当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);     // false,s1和s2的地址不同所以返回了false
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4);     // true, s3和s4都指向了StringPool中的"aaa"是同一个对象所以返回了 true

PS: 在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

常量折叠

接下来让我们再看一个问题:

public static void main(String[] args) {
        String a = "hello2";
        final String b = "hello";
        String d = "hello";
        String c = b + 2;
        String e = d + 2;
        System.out.println((a == c));
        System.out.println((a == e));

    }

结果返回了:

true
false

为什么呢?这就要引入常量折叠的概念。

常量折叠的概念

  • 常量折叠是一种编译器优化技术。
  • 常量折叠主要指的是编译期常量加减乘除的运算过程会被折叠

对于 String s1 = "1" + "2";

编译器会给你优化成 String s1 = "12";

在生成的字节码中,根本看不到 "1" "2" 这两个东西。

我们通过idea进行验证下

1、源码文件

    public static void main(String[] args) {
        String s1 = "1"+"2";
    }

2、运行后,idea有个out文件夹,找到上面文件的class文件

    public static void main(String[] args) {
        String s1 = "12";
    }

确实如上面所说,编译器会给你进行优化

常量折叠发生的条件

  • 必须是编译期常量之间进行运算才会进行常量折叠。
  • 编译期常量就是

    “编译的时候就可以确定其值的常量”,

    • 首先:字面量是编译期常量。(数字字面量,字符串字面量等)
    • 其次:编译期常量进行简单运算的结果也是编译期常量,如1+2,"a"+"b"。
    • 最后:被编译器常量赋值的 final 的基本类型和字符串变量也是编译期常量。

举个栗子

1.第一个栗子

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

相信大家都知道了,输出为true

并且只创建了一个 "abc" 字符串对象,且位于字符串常量池中。

2、第二个栗子

    public static void main(String[] args) {
        String a = "a";
        String bc = "bc";
        String s1 = "a" + "bc";
        String s2 = a + bc;
        System.out.println(s1 == s2);
    }

这个结果呢?false

s1 是字符串字面量相加,但是 s2 却是两个非 final 的变量相加,所以不会进行常量折叠。

而是根据 String 类特有的 + 运算符重载,变成类似这样的代码 (jdk1.8)

String s2 = new StringBuilder(a).append(b).toString();

这里 toString() 会生成新的String变量,显然用 == 运算符比较是会返回 false。

总结,只要牢记常量折叠主要指的是编译期常量加减乘除的运算过程会被折叠

总结

综上所述,在使用 String的比较时,我们最好使用 equals()方法,而==是内存上的比较。不过正是它的引入,才致使我们分析学习了,jvm的内存模型,常量池,常量折叠等编译器层面的知识。因祸得福,希望本节课能巩固我们的底层知识,下期再会了!

原文地址:https://www.cnblogs.com/jiaweixie/p/12672267.html

时间: 2024-10-01 08:01:58

字符串比较--小问题大智慧的相关文章

【分享】小工具大智慧之Sql执行工具

工具概况 情况是这样的,以前我们公司有很多Sql用于完成一些很不起眼但又不得不完成的业务,出于方便就直接在Sql查询分析器里执行,按理说应该写一些专门的工具的,但是这些脚本很多,于是我就写了这样一个小工具,只要Sql可以解决的问题就能用到它了,那么它有什么优点呢 一.以前写好的Sql可以不用怎么更改,只需要将参数部分提取出来按照定好的规范改成一个模版即可,使用工具加载模版时,工具会自动将模块要求的参数提供给用户填写. 二.模版文件里可以配置好连接字符串,这样不用每次都连接数据库,也避免连接错误的

小程序大智慧,sqlserver 注释提取工具

原文:小程序大智慧,sqlserver 注释提取工具 开篇背景 我习惯在写表的创建脚本时将注释直接写在脚本里,比如 ? /*账套*/ CREATE TABLE [dbo].[AccountingBook] (     [IDNO]          NVARCHAR (255) NOT NULL,    /*ID*/     [BH]            NVARCHAR (255) NULL,        /*业务编号*/     [Name]          NVARCHAR (255

C# 字符串处理小工具

之前刚上大学时沉迷于安全方面,当时一直想写一个处理字符串的小程序. 无奈当时没有太多时间,一直拖延到这寒假. 寒假闲来无事,所以就写写小程序来练手,顺便复习一下窗体和基础. 实现的功能有以下: 转换为大写 转换为小写 反转字符串 匹配某字符串出现次数 正则匹配 base64加密 base64解密 ROT13加密解密 MD5 32位加密 程序还是非常简陋的,没有健壮性,也没有输入的校验. 用心创造BUG(比心 还有请不要吐槽我的变量命名以及方法命名,如果你不是从小学开始学拼音肯定看不懂:) 因为一

UVa140 Bandwidth 小剪枝+双射小技巧+枚举全排列+字符串的小处理

给出一个图,找出其中的最小带宽的排列.具体要求见传送门:UVa140 这题有些小技巧可以简化代码的编写. 本题的实现参考了刘汝佳老师的源码,的确给了我许多启发,感谢刘老师. 思路: 建立双射关系:从字符A到字符Z遍历输入的字符串,用strchr函数将输入中出现的字符找出,并将找出的字符进行编号,用letter和id分别存储字符和对应的编号 降维:输入中给出的,是类似于邻接表形式的二维形式,如果我们用二维数据结构,将增加处理时对于输出细节的处理难度,用 2个 vector将输出降低到1维,简化了计

C#为工作Sql而产生的字符串分割小工具(很实用,你值得拥有)

写在前面 为什么要写这个工具? 工作需要,拼接字符串头晕眼花拼接的,特别是in 查询,后面的参数太多,想在数据执行一些这个sql语句老费劲了. 看正文 工作所有的(后台)攻城狮们都会接触到sql语句,最常见的sql语句有很多,特别 in 查询,举个最简单的例子就是 SELECT * FROM SMSSENTLOG201505 WHERE ACCOUNTCODE IN ('PDA','ORDER','APP','WEB','ZHONGTIAN','00006','02100','CALLCENTE

C字符串的小问题

string与char *.char[]之间的转换 提示 string可以被看成是以字符为元素的一种容器.字符构成序列(字符串).有时候在字符序列中进行遍历,标准的string类提供了STL容器接口.具有一些成员函数比如begin().end(),迭代器可以根据他们进行定位. 与char 不同的是,string不一定以NULL('\0')结束.string长度可以根据length()得到,string可以根据下标访问.所以,不能将string直接赋值给char . string转换成char *

字符串的小技巧

前些天在研究字符串,这里记录一些小技巧以便复习. 1.KMP求最短重复子串 令leng=len-next[len] 若要求完全覆盖,则(len%leng==0)?leng:len; 若不要求完全覆盖,则输出leng 2.Exkmp如何构造extend[]数组 可以发现问题最终转化为求t[i]后缀与t[]的最长公共前缀,与kmp写法基本一样(没写过) 3.神奇的Manacher--解决最长回文子串问题的最快方法 设mx为当前已知最长回文串的右端(其实是用来判断当前点位置pla是否被覆盖),回文中心

Java 字符串比较小知识

使用 String.compareTo 方法: compareTo() 的返回值是int, 它是先比较对应字符的大小(ASCII码顺序). 1.如果字符串相等,返回值0. 2.如果第一个字符和参数的第一个字符不等,结束比较,返回他们之间的差值(ascii码值)(负值,前字符串的值小于后字符串:正值,前字符串大于后字符串). 3.如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至比较的字符或被比较的字符有一方全比较完,这时就比较字符的长度. String

小管理 大智慧

五天假期过去了,短短的开学两三天,与五天的清闲相比,遇到的事情真的不是多一点啊,好多次都是连水都顾不上喝一口,大脑一直处于饱和状态. 这样的忙碌固然充实,但是忙碌之后更加发觉:很多事情的处理效率很低. 一.邮件管理 背景1: 部分同学在学术交流中心住宿,开学的时候需要通过邮件,来明确注意事项.邮箱的联系人中创建有小班长的分组,所以我一直以 学术交流中心负责人-->CEO-->小班长-->所有组员,这样的方式下发邮件.晓春组没有住在学术交流中心的同学,但是我前几次发邮件的时候都未经筛选,顺