String类相关知识(包含intern方法)及字符串拼接的底层原理和优化

1、创建字符串

1.1、方式一:字面量声明

String str1 = "hello,mrz";

这种方式会直接将字符串放到字符串常量池中:

注意:在JDK1.7之后(包括1.7),字符串常量池已经从方法区移动到了堆区

当我们再次通过字面量声明一个内容相同的字符串时,不会新建对象,而是直接指向池中的地址,如下图:
在这里插入图片描述
1.2、方式二:new String()

String str2 = new String("hello,mrz");

使用这种方式创建字符串,要分两种情况:

  1. 如果字符串常量池中已经存在了相同内容的字符串对象,只创建一个对象(比如在new之前已经用字面量声明的方法创建了一个相同内容的字符串)

    解释:

    1、"abc"是字符串常量,应该在常量池中创建,JVM在常量池中检查发现已经存在有相同内容的字符串,所以不再创建

    2、遇到new关键字,会在堆中创建一个String对象,返回给str2

在这里插入图片描述
2. 如果字符串常量池中不存在相同内容的常量

1、首先在字符串常量池创建一个"hello,mrz"对象

2、遇到new关键字,在堆中创建一个字符串对象,返回给str2

在这里插入图片描述
1.3、比较以上两种方式可以得到:

1、通过字面量声明的方式最多创建一个对象,最少不创建对象

  • 如果常量池中没有,创建一个对象
  • 如果常量池中已经存在,不创建对象,直接引用

2、通过new String()方式最少创建一个对象,最多创建两个对象

  • 如果常量池中没有,在常量池中创建一个对象,在堆中创建一个对象
  • 如果常量池中已经存在,则只会在堆中创建一个对象
  • 绝对会在堆中创建一个字符串对象,常量池中可能创建可能不创建

1.4、扩展

Strin str = new String(“a”) + new String(“b”) 创建了几个对象?

一共创建了5个对象

解释:

在常量池中创建了2个字符串对象:a"字符串对象,"b"字符串对象

在堆中创建了三个字符串对象:"a"字符串对象,"b"字符串对象,"ab"字符串对象

注意:拼接是在堆中完成的,这里实际上是使用 StringBuilder.append 来完成字符串的拼接, 所以在堆内存中会存在一个 “ab”, 但是在字符串常量池中没有 “ab” 。如果要入池需要使用后文的intern()方法

2、字符串比较相等

2.1、看以下代码

  1. 例一

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

    至于例一为什么这为true不再赘述,看1.1内存图便可明白:str1和str2指向字符串常量池中同一个地址,固然相等

  2. 例二

    String str1 = new String("Hello,mrz");
    String str2 = new String("Hello,mrz");
    System.out.println(str1 == str2); //false
    

    在这里插入图片描述
    解释:new关键字都是在堆中开辟内存创建对象的,所以肯定地址肯定不同,也就是为false

    可是我们想要的效果是:“只要两个字符串内容相同,就为true”;怎么办呢?那就要使用equals()方法进行判断了

2.2、equals()方法

记住一句话:

使用 == 比较的是两个引用是否指向同一个地址(“身份”是否相同),比较字符串的内容是否相等要使用equals方法

  1. 所以在2.1-例二中,如果是str1.equals(str2),那么结果就是true

  2. 对于自定义的类,如果要比较两个对象的内容是否相同,一定要重写equals()方法

  3. 假如现在要比较str和"hello"内容是否相等,推荐下面这种写法:

    "hello".equals(str);
    str.equals("hello");
    //第二种可能会NullPointerException,第一种永远不会
    

3、String.intern()

3.1、先来看看intern()方法的介绍:

  1. 在jdk1.7之前

    <1>如果池中有相同内容的字符串,则直接返回池中该字符串的引用(地址)

    <2>如果池中没有相同内容的字符串,则会把堆中这个字符串对象拷贝一份,放入常量池,然后返回池中字符串对象的引用

    1. 在jdk1.7之后(包含1.7)

    <1>如果池中有相同内容的字符串,则直接返回池中该字符串的引用

    <2>如果池中没有,则会把这个字符串对象的的地址复制一份,放入池中,并返回池中的引用(指向堆中的字符串对象)

3.2、可能文字并不好解释清楚,接下来我将通过几个案例演示(以jdk1.7为准)

案例一:

String str1 = new String("hello,mrz");
String str2 = "hello,mrz";
str1.intern();
System.out.println(str1 == str2);//false

在这里插入图片描述
解释:

new String(“hello,mrz”),首先在池中创建一个"hello,mrz"对象,然后在堆中创建了字符串对象引用池中对象,返回堆中对象的引用给str1

str2直接引用池中对象的0x444

str1.intern()入池,JVM检测到池中已有相同内容的字符串,返回池中字符串的引用,但这里没有接收,str1还是指向堆中的对象

所以为false

//上述代码中,如果改成
str1 = str1.intern();
//str1接收到返回值,也指向池中对象,那么就是true

案例二:

String str1 = new String("hello") + new String(",mrz");
String str2 = "hello,mrz";
str1.intern();
System.out.println(str1 == str2);//false

在这里插入图片描述
解释:

执行第一句:new String(“hello”)+new String(“,mrz”)

在堆中创建了"hello"对象、“,mrz"对象,在常量池中创建了"hello"对象和”,mrz"对象

然后A对象(上图中)和B对象进行拼接,在堆中出现了"hello,mrz"对象

此时常量池中有"hello",“,mrz”,堆中有A、B、C三个对象(拼接底层使用的是 StringBuilder.append 来完成的, 所以在堆内存中会存在一个 “hello,mrz”对象, 但是在字符串常量池中没有 “hello,mrz”)

执行第二句:str2=“hello,mrz”

因为池中没有"hello,mrz",所以会在池中创建一个"hello,mrz"对象,返回引用给str2

在这里插入图片描述
执行第三句:str1.intern()

入池,JVM检查发现池中已有相同内容的字符串,则返回池中对象的引用,这里同案例一,也是因为没有接收,所以str1还是指向堆中的对象

所以str1肯定不等于str2

案例三:

但是如果我们调换一下第二、第三句代码的顺序呢?这时候答案就为true了!!!

String str1 = new String("hello") + new String(",mrz");
str1.intern();//调换
String str2 = "hello,mrz";//调换
System.out.println(str1 == str2);//true

解释:

第一句代码执行结果同案例二;

第二句str1.intern()入池

此时第三句代码还未执行,池中还没有"hello,mrz"对象,当JVM检查发现池中没有相同内容对象时,就会把堆中C对象的引用复制一份放到池中,然后返回池中的引用地址(就是C的引用)

第三句执行str2=“hello,mrz”

JVM检查发现池中已经有了,则返回常量池中的引用(即str2也是指向C对象)
在这里插入图片描述
所以str1和str2都是指向C对象,那么就是true了

综上所述:new String(“a”) + new String(“b”)一共创建了5个对象(池中没有"a"和"b"的前提向下),堆中三个,池中两个

案例四:

4、如何理解字符串是不可变的?

4.1、看以下代码

String s1 = "East";
s1 = "South";

s1="South"是直接将s1的内容"East"修改为了"West"吗?

不是!

通过查看String类的源代码可以知道:
在这里插入图片描述
String底层是用了一个private final修饰的字符数组:

  • private修饰,表明外部类是访问不到value数组的,同时子类也无法访问
  • final修饰的内容是不可以被改变的,所以对String来说,一旦初始化,value[]将无法再被修改
  • 但是注意,通过反射可以打破封装,可以修改value[]

有人就会说:String既然不可变,那为什么String类还有很多方法都能操作字符串?

其实String类中的很多方法,如toLowerCase,都不是在原来的字符串上进行操作的,而是重新new了个字符串,返回的是新创建的字符串对象

4.2、为什么要求字符串不可变?

  1. 方便实现字符串对象池. 如果 String 可变, 那么对象池就需要考虑何时深拷贝字符串的问题了.
  2. 不可变对象是线程安全的.
  3. 不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 HashMap 中.

5、字符串拼接优化

5.1、看以下代码:

String str1 = "hellomrz";
String str2 = "hello" + "mrz";
System.out.println(str1 == str2);//true

解释:

第一:在池中创建对象"hello,mrz"

第二:编译器在编译时发现"hello" 和 "mrz"都是字符串,会自动进行拼接成"hellomrz"再存储

第三:拼接后存储时,JVM检测到常量池中已有"hellomrz",所以str2会直接引用常量池中的"hellomrz"

故str1中存的地址和str2相同,所以为true

通过反汇编也可以看出两种方法是相同的:
在这里插入图片描述
5.2、看以下代码:

String s1 = "hellomrz";
String s2 = "hello";
String s3 = s2 + "mrz";
System.out.println(s1 == s3);//false

为什么是false呢?首先我们需要先来了解常量和变量的区别:

简但来说:变量只有在运行时才知道其中存储的内容,而常量是在量是在编译时就已经知道存的多少

这里s2就是个变量,编译器在编译时并不知道s2是多少;所以s3在编译时也就是个变量,拼接时在堆中new一个"hello,mrz"对象,返回给s3;s1引用常量池中的"hello,mrz",两者当然不同

5.3、看以下代码:

String  s1 = "hellomrz";
final String s2 = "hello";
String s3 = s2 + "mrz";
System.out.println(s1 == s3);//true

相比于5.2,这里s2用final修饰,结果就变为了true

首先我们需要了解的是:对于常量,编译器在编译时就会帮我们进行运算!

例如final修饰的两个int变量,进行相加,编译器在编译时就帮我相加了,即在编译时c就已经等于30,具体可以用javap命令反汇编验证:

final int a = 10;
final int b = 20;
int c = a + b;//c=30
Code:
      0: bipush        10//a
      2: istore_1			
      3: bipush        20//b
      5: istore_2			
      6: bipush        30//c	编译时就已经知道c=30了
      8: istore_3	

所以final修饰的s2也是常量!

那么s2 + "mrz"相当于 “hello” + “mrz”,编译时就优化成"hello,mrz"存储;JVM检测到池中已有相同内容的对象,就返回池中对象的引用,所以s1 == s3;

6、StringBuilder和StringBuffer

6.1、StringBuilder和StringBuffer大部分功能都是相同的,两者主要的区别就是StringBuffer是线程安全的,而StringBuilder是非线程安全的;后文以StringBuilder举例,StringBuffer同理

6.2、String和StringBuilder的区别?

String和StringBuilder最大的区别在于:String的内容无法修改,如果改变对象的内容,改变的其实是其引用的指向而已;而StringBuilder的内容可以修改

6.3、String进行拼接的原理是什么?

<1>执行下面的代码:

public static void main(String[] args) {
        String str = "hello";
        for (int i = 0; i < 10; i++) {
            str = str + i;
        }
        System.out.println(str);//hello0123456789
    }

​ <2>使用javap进行反汇编:
在这里插入图片描述
从图中可以看到,String在进行拼接时,首先会构造一个StringBuilder对象,然后调用其append()方法进行拼接,拼接完成后又调用其toString()方法赋值给str,这样str就完成了"一次拼接";

因为使用了for循环,所以第33行时"goto 5",回到第五行重新往下执行,又会重复一遍前面的步骤,直到结束循环退出循环

为了方便理解,这里用java代码来模拟一次拼接的过程:

public static void main(String[] args) {
    String s1 = "hello";
    String s2 = "mrz";
    
    //模拟String str = s1 + s2;
    StringBuilder sb = new Stringbuilder();
    sb.append(s1);
    sb.append(s2);
    String str = sb.toString();//完成s1 拼接 s2
}

综上所述:每拼接一次,都会在底层new一个StringBuilder对象

如果在一个程序中反复需要拼接字符串,那么就会在堆中产生大量的StringBuilder对象,浪费内存

6.4、StringBuilder拼接是什么样的?

<1>执行下面的代码:

public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append("hello");
        for (int i = 0; i < 10; i++) {
            sb.append(i);
        }
        String str = sb.toString();
        System.out.println(str);
    }

<2>javap反汇编:
在这里插入图片描述
可以看到,虽然都是for循环拼接了10次,但是使用String拼接构建了10个StringBuilder对象,而使用StringBuilder拼接只创建了一个StringBuilder对象!!!(在17-32行循环并没有创建新的StringBuilder对象)

综合6.3和6.4可得:如果在程序中经常有拼接操作,我们应该选用StringBuilder,如果需要线程安全的,那应该选用StringBuffer

使用String进行拼接效率太低

7、String类一些方法注意事项

7.1、String类的两种构造方法使用

No方法名类型概述
1String(char[] value)构造以字符数组构建字符串
2String(byte[] bytes)构造以字节数组构建字符串

可以看到,通过char数组或byte数组都可以构建一个新的字符串,那我们怎么选择呢?

回答:

byte[] 是把 数据按照一个字节一个字节的方式处理, 这种方式适合在网络传输, 数据存储这样的场景下使用. 更适合针对二进制数据来操作.

char[] 是把数据按照一个字符一个字符的方式处理, 更适合针对文本数据来操作, 尤其是包含中文的时候

7.2、字符串拆分

No方法名类型概述
1String[] split(String regex)成员方法全部拆分
2String[] split(String regex,int limit)成员方法拆分成limit组
String str = "hello world hello mrz" ; 
String[] result = str.split(" ") ; // 按照空格拆分
for(String s: result) { 
 System.out.println(s); 
}
//"hello"	"world"  "hello"	"mrz"
/... 
String[] result = str.split(" ", 3) ; // 按照空格拆分成三组
/...
//输出: "hello"	"world"	 "hello mrz"

拆分是一种常用到的操作,有一些特殊字符作为分隔符需要加上转义才能正常分割

举例:拆分IP地址

String str = "192.168.1.1"; 
String[] result = str.split(".");//错误:可疑的正则表达式
for(String s: result) { 
 System.out.println(s); 
}//什么都没输出

正确的做法如下:

String str = "192.168.1.1"; 
String[] result = str.split("\\.");//第一个'\'是用来转义第二个'\'的,而第二个'\'用来转义'.'
for(String s: result) { 
 System.out.println(s); 
}//"192"  "168"	 "1"	"1"

注意事项:

  1. 字符 " | " , " * " , " + " 作为分隔符时,都得加上转义字符
  2. 如果一个字符串中有多个分隔符,可以用 " | " 作为连字符

举例:多次拆分

String str = "name=xiaoming&age=8" ; 
String[] strings = str.split("&") ; 
for (int i = 0; i < result.length; i++) { 
 String[] tStr = result[i].split("=") ; 
 System.out.println(tStr[0]+" = "+tStr[1]); 
}

以上就是我个人关于String类及相关内容的一些总结。可能不是很全面,也有可能一些内容有错误,欢迎大家指正。

  • 20
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值