当前位置: 首页 > news >正文

【Java基础】String详解

文章目录

  • String
  • 一、String 概述
    • 1、基本特性
    • 2、不可变性
    • 3、String、StringBuilder、StringBuffer
  • 二、字符串 创建与内存分配
    • 1、字面量 / 双引号
    • 2、new关键字
    • 3、StringBuilder.toString()
    • 4、intern() 方法
    • 5、小结
  • 三、字符串 拼接
    • 1、常量+常量
    • 2、变量 拼接
    • 3、final变量 拼接
    • 4、拼接小结
    • 5、+拼接 vs append拼接
    • 6、StringBuilder的扩容
    • 7、优化小结
  • 四、new String() 会创建几个对象
    • 1、单独new
    • 2、常量 拼接 new
    • 3、new 拼接 new
  • 五、intern()
    • 1、intern() 的作用
    • 2、案例分析
      • 1)案例1
      • 2)案例2
      • 3)案例3
    • 3、intern() 的效率:空间角度
    • 4、运行时内存案例

String

一、String 概述

1、基本特性

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {// jdk1.8及之前private final char value[];public String() {this.value = "".value;}
}
  • String 实现了 Serializable 接口:表示字符串是支持序列化的。
  • String 实现了 Comparable 接口:表示 String 可以比较大小
  • String 声明为 final 的,不可被继承,一旦被创建就不会改变,对字符串的操作只是产生了一个新的字符串。
  • String 在 jdk8及以前 定义了 final char[] value 存储字符串数据,jdk9时 改为 final byte[] value

2、不可变性

String 声明为 final 的,一旦被创建就不会改变String的每次操作都是生成一个新的对象,不改变原来的对象

例1:重新赋值

@Test
public void test1() {String s1 = "abc";String s2 = s1;System.out.println(s1.hashCode()); // 96354System.out.println(s2.hashCode()); // 96354// 不会改变原来的对象("abc"),只是新生成一个对象("hello"),并指向新对象s2 = "hello";System.out.println(s1.hashCode()); // 96354System.out.println(s2.hashCode()); // 99162322System.out.println(s1); // abcSystem.out.println(s2); // hello        
}

例2:拼接操作

@Test
public void test2() {String s1 = "abc";String s2 = s1 + "def";System.out.println(s1); // abcSystem.out.println(s2); // abcdef
}

例3:replace() 方法

@Test
public void test3() {String s1 = "abc";String s2 = s1.replace('a', 'm');System.out.println(s1); // abcSystem.out.println(s2); // mbc
}

例4:方法参数传递

@Test
public void test4() {String str = "old";char[] ch = {'t', 'e', 's', 't'};change(str, ch);System.out.println(str); // oldSystem.out.println(ch);  // best
}public void change(String str, char ch[]) {// 拼接和replace同理str = "new";ch[0] = 'b';
}

3、String、StringBuilder、StringBuffer

可变性:

  • String

    使用字符数组 private final char value[] 保存字符串,因此String不可变

  • StringBuilderStringBuffer

    继承于 AbstractStringBuilder,使用字符数组 char[] value 保存字符串,因此这两种对象都是可变的

线程安全性:

  • String:String对象是不可变的,一旦创建后其内容不能更改,线程安全
  • StringBuffer:加了 synchronized 同步锁,线程安全
  • StringBuilder非线程安全

性能:

  • String:每次对 String 类型进行改变的时候,都会生成一个新的String对象。
  • StringBuffer:每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。
  • StirngBuilder:没有加锁操作,比 StringBuffer 能获得 10%~15% 左右的性能提升。

使用场景:

  • 操作少量数据:String
  • 单线程操作大量数据:StringBuilder
  • 多线程操作大量数据:StringBuffer

二、字符串 创建与内存分配

1、字面量 / 双引号

直接由双引号""给出声明的字符串,存储在字符串常量池中(并且相同的字符串只会存在一份)

public static void test() {String str = "ab";
}
0 ldc #2 <ab>
2 astore_0
3 return

使用双引号""创建字符串时,JVM会先去常量池中查找是否存在这个字符串对象。

  • 不存在:在 字符串常量池 创建这个字符串对象,并返回地址。
  • 存在:直接返回 字符串常量池 中 字符串对象的地址。

2、new关键字

new 关键字声明的字符串,先在堆内存中创建一个字符串对象(new),然后在字符串常量池中创建一个字符串常量(ldc)。

public static void test() {String s1 = new String("ab");String s2 = "ab";
}
 0 new #3 <java/lang/String>3 dup4 ldc #2 <ab>6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>9 astore_0
10 ldc #2 <ab>
12 astore_1
13 return

使用 new 创建字符串时,JVM也会先去常量池中查找是否存在这个字符串对象。

  • 不存在:先在堆内存中创建一个字符串对象,然后在字符串常量池中创建一个字符串常量。
  • 存在:直接在堆内存中创建另一个字符串对象。

注意:最后返回的是堆内存中字符串对象的地址,不是常量池中的字符串对象的地址。

public static void test() {String s1 = new String("ab");String s2 = "ab";System.out.println(s1 == s2); // false
}

3、StringBuilder.toString()

从下面的源码可以看到,StringBuildertoString()其实会new一个String对象

public final class StringBuilder extends AbstractStringBuilder implements Serializable, CharSequence {@Overridepublic String toString() {// Create a copy, don't share the arrayreturn new String(value, 0, count);}
}

需要注意的是,StringBuilder.toString() 不会在常量池中创建对象,下面写个例子分析一下。

public static void test() {StringBuilder stringBuilder = new StringBuilder("a");stringBuilder.append("b");String str = stringBuilder.toString();
}
 0 new #5 <java/lang/StringBuilder>3 dup4 ldc #6 <a>6 invokespecial #7 <java/lang/StringBuilder.<init> : (Ljava/lang/String;)V>9 astore_0
10 aload_0
11 ldc #8 <b>
13 invokevirtual #9 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
16 pop
17 aload_0
18 invokevirtual #10 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
21 astore_1
22 return

可以看到没有出现 ldc #x <ab>,可见StringBuilder.toString() 只在堆内存创建了一个字符串,并没有放到字符串常量池

4、intern() 方法

intern()判断常量池中是否存在该字符串,存在,则返回常量池中的地址;不存在,则在常量池中加载一份并返回地址

@Test
public void test() {String s1 = "nb";String s2 = "n";String s3 = s2 + "b";String s4 = s3.intern(); // 常量池中存在,返回常量池中的地址System.out.println(s1 == s3); // falseSystem.out.println(s1 == s4); // true
}

关于intern()方法下面会详细展开。

5、小结

什么情况下,字符串会被放入 字符串常量池 呢?

  1. 直接由双引号""给出声明的字符串,会直接放在字符串常量池中。
  2. 使用new创建的字符串,也会有一份放在字符串常量池中。
  3. 调用intern()方法的字符串,也会被放到字符串常量池中。

注意:StringBuilder.toString()生成的字符串,是不会放到字符串常量池中的,只会在堆中创建一份。

三、字符串 拼接

1、常量+常量

场景1:常量常量拼接,拼接结果在字符串常量池,原理是编译期优化

public class AppendTest {public void test() {String s1 = "a" + "b" + "c";String s2 = "abc";System.out.println(s1 == s2); // true}
}

从 class文件 的 反编译结果 可以看出:编译器做了优化,将 "a" + "b" + "c" 优化成了 "abc"

 0 ldc #2 <abc>2 astore_03 ldc #2 <abc>5 astore_16 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>9 aload_0
10 aload_1
11 if_acmpne 18 (+7)
14 iconst_1
15 goto 19 (+4)
18 iconst_0
19 invokevirtual #4 <java/io/PrintStream.println : (Z)V>
22 return

从 IDEA 的 AppendTest.class 也可以直接看出来

public class AppendTest {public AppendTest() {}public void test() {String s1 = "abc";  // 显示 String s1 = "abc"; 说明做了代码优化String s2 = "abc";System.out.println(s1 == s2);}
}

2、变量 拼接

场景2:拼接中只要有一个是变量,拼接结果就在堆中,原理是StringBuilderappend操作。

public void test2() {String s1 = "n";String s2 = "b";String s3 = "nb";String s4 = "n" + "b"; // 编译期优化String s5 = s1 + "b";String s6 = "n" + s2;String s7 = s1 + s2;System.out.println(s3 == s4); // trueSystem.out.println(s3 == s5); // falseSystem.out.println(s3 == s6); // falseSystem.out.println(s3 == s7); // falseSystem.out.println(s5 == s6); // falseSystem.out.println(s5 == s7); // falseSystem.out.println(s6 == s7); // false// 这里使用intern(),会返回常量池中"nb"的地址并赋给s8(这里先了解,具体用法后续会详细展开)String s8 = s7.intern();System.out.println(s3 == s8); // true
}

下面我们从 class文件 的 反编译结果 进行分析

  0 ldc #5 <n>2 astore_13 ldc #6 <b>5 astore_26 ldc #7 <nb>8 astore_39 ldc #7 <nb>11 astore 4

s4之前都是【例1】的内容,这里就不赘述了,主要看一下 s5、s6、s7 这三行

可以看出,都是先 new 了一个 StringBuilder 对象,然后使用 append() 拼接,最后调用了 toString() 创建 String对象 并赋值

 13 new #8 <java/lang/StringBuilder>16 dup17 invokespecial #9 <java/lang/StringBuilder.<init> : ()V>20 aload_121 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>24 ldc #6 <b>26 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>29 invokevirtual #11 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>32 astore 5
 34 new #8 <java/lang/StringBuilder>37 dup38 invokespecial #9 <java/lang/StringBuilder.<init> : ()V>41 ldc #5 <n>43 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>46 aload_247 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>50 invokevirtual #11 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>53 astore 6
 55 new #8 <java/lang/StringBuilder>58 dup59 invokespecial #9 <java/lang/StringBuilder.<init> : ()V>62 aload_163 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>66 aload_267 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>70 invokevirtual #11 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>73 astore 7

3、final变量 拼接

场景3:final修饰的String变量,视作String常量

public static void test3() {final String s1 = "n";final String s2 = "b";String s3 = "nb";String s4 = "n" + "b"; // 编译期优化String s5 = s1 + "b";String s6 = "n" + s2;String s7 = s1 + s2;System.out.println(s3 == s4); // trueSystem.out.println(s3 == s5); // trueSystem.out.println(s3 == s6); // trueSystem.out.println(s3 == s7); // trueSystem.out.println(s5 == s6); // trueSystem.out.println(s5 == s7); // trueSystem.out.println(s6 == s7); // true
}

可以看到,我们只是在String变量前加上final,结果就完全不同了。

下面我们看一下 class文件 的 反编译结果

  0 ldc #6 <n>2 astore_03 ldc #7 <b>5 astore_16 ldc #8 <nb>8 astore_29 ldc #8 <nb>11 astore_312 ldc #8 <nb>14 astore 416 ldc #8 <nb>18 astore 520 ldc #8 <nb>22 astore 6

可以看出,String变量被final修饰之后,所有的拼接操作都在编译期优化了,而没有使用StringBuilder

4、拼接小结

  • 常量常量拼接:
    • 拼接结果在字符串常量池,原理是编译期优化
  • 拼接中只要有一个是变量
    • 拼接结果在堆中,原理是 先new 一个StringBuilder,然后用append()拼接,最后调用 toString() 返回结果
    • 补充说明:在 jdk5 之前使用的是 StringBuffer,在 jdk5 之后,改为了 StringBuilder,
  • final修饰的String变量拼接:
    • 拼接结果在字符串常量池,仍然使用编译期优化,而非StringBuilder

因此,在开发中能使用上final的时候还是建议使用

5、+拼接 vs append拼接

public class StringAppendTest {public static void main(String[] args) {long start = System.currentTimeMillis();//        String s1 = append1(100000); // 1670msString s2 = append2(100000);   // 4mslong end = System.currentTimeMillis();System.out.println("拼接花费的时间为:" + (end - start));}public static String append1(int highLevel) {String str = "";for (int i = 0; i < highLevel; i++) {str = str + "a";  // 每次循环都会创建一个StringBuilder、String}return str;}public static String append2(int highLevel) {StringBuilder strBuilder = new StringBuilder();for (int i = 0; i < highLevel; i++) {strBuilder.append("a"); // 只需要创建一个StringBuilder}return strBuilder.toString();}
}

结论:通过StringBuilderappend()的方式拼接字符串的效率,远远高于 + 拼接

原因:

  • StringBuilderappend()的方式:
    • 自始至终中只创建过一个StringBuilder的对象。
  • + 拼接的方式:
    • 每一次 字符串变量 拼接的过程,都会new一个StringBuilder对象(这从之前的反编译结果中也可以看出来)

因此使用字符串变量+拼接会占用更大的内存,产生大量垃圾字符串,如果发生了GC,也会花费额外的时间。

6、StringBuilder的扩容

StringBuilder 空参构造器的初始化大小为16,超过该大小会进行扩容,涉及数组的copy操作

public StringBuilder() {    super(16);
}

如果提前知道需要拼接 String 的长度,就应该直接使用带参构造器指定capacity,以减少扩容的次数

public StringBuilder(int capacity) {    super(capacity);
}

7、优化小结

  • 允许的情况下尽量使用final。这样拼接操作会在编译期优化,而不会创建StringBuilder对象去append。
  • 使用StringBuilderappend()效率要高于使用+拼接。
  • 如果知道最终的字符串长度,应该使用带容量的构造器创建StringBuilder,避免频繁扩容。

四、new String() 会创建几个对象

1、单独new

场景1:new String("ab") 会创建几个对象?(答案是2个)

 0: new           	#2                  // class java/lang/String3: dup4: ldc           	#3                  // String ab6: invokespecial 	#4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V9: astore_1
10: return
对象1  new String("ab")
对象2  常量池中的"ab"

2、常量 拼接 new

场景2:"a" + new String("b") 会创建几个对象?(答案是5个)

 0: new            	#2                  // class java/lang/StringBuilder3: dup4: invokespecial  	#3                  // Method java/lang/StringBuilder."<init>":()V7: ldc            	#4                  // String a9: invokevirtual  	#5                  // Method java/lang/StringBuilder.append:
12: new           	#6                  // class java/lang/String
15: dup
16: ldc           	#7                  // String b
18: invokespecial 	#8                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
21: invokevirtual 	#5                  // Method java/lang/StringBuilder.append:
24: invokevirtual 	#9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore_1
28: return
对象1	 new StringBuilder()
对象2  常量池中的"a"
对象3  new String("b")
对象4  常量池中的"b"   
对象5  StringBuilder.toString()new String("ab")

注意:StringBuilder.toString 不会在 字符串常量池中 生成 "ab"

3、new 拼接 new

场景3:new String("a") + new String("b") 会创建几个对象?(答案是6个)

 0: new           	#2                  // class java/lang/StringBuilder3: dup4: invokespecial 	#3                  // Method java/lang/StringBuilder."<init>":()V7: new           	#4                  // class java/lang/String
10: dup
11: ldc           	#5                  // String a
13: invokespecial 	#6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
16: invokevirtual 	#7                  // Method java/lang/StringBuilder.append:
19: new           	#4                  // class java/lang/String
22: dup
23: ldc           	#8                  // String b
25: invokespecial 	#6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual 	#7                  // Method java/lang/StringBuilder.append:
31: invokevirtual 	#9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: return
对象1	 new StringBuilder()
对象2  new String("a")
对象3  常量池中的"a"
对象4  new String("b")
对象5  常量池中的"b"   
对象6  StringBuilder.toString()new String("ab")

注意:StringBuilder.toString 不会在 字符串常量池中 生成 "ab"

五、intern()

intern是一个native方法,底层调用的是C的方法

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {public native String intern();
}

1、intern() 的作用

调用intern()时,会判断 字符串常量池 中 是否已存在当前字符串(通过equals()方法判断)

  • 已存在:返回 字符串常量池 中 已存在的 该字符串对象的地址;
  • 不存在:将 该字符串 放入 字符串常量池,并返回 字符串对象的地址(这里jdk7前后略有不同)
    • JDK1.6中:会把 调用对象复制一份(新的引用地址),放入常量池,并返回 新的引用地址
    • JDK1.7起:会把 调用对象的引用地址 复制一份(相同的引用地址),放入常量池,并返回 调用对象的引用地址

也就是说,任意字符串调用intern() 返回结果所指向的那个类实例,必定和直接以常量形式出现的字符串实例完全相同。

 ("a"+"b"+"c").intern() == "abc" // true;

intern()可以确保字符串在内存里只有一份(即字符串常量池中的),可以节约内存空间,加快字符串操作任务的执行速度。

2、案例分析

1)案例1

public void test() {String s1 = new String("a");s1.intern();String s2 = "a";System.out.println(s1 == s2);  // jdk6/7/8 false
}
  • s1记录的是堆中new String("a")的地址
  • s2记录的是字符串常量池中"a"的地址

2)案例2

public void test() {String s1 = new String("a") + new String("b");s1.intern();String s2 = "ab";System.out.println(s1 == s2);  // jdk6 false ; jdk7/8 true
}
  • s1记录的是堆中"ab"的地址(注意,这个"ab"StringBuilder.toString()生成的,没有往常量池里放)
  • s1.intern()调用这个方法之前,字符串常量池中并不存在"ab",所以要把"ab"放入字符串常量池
    • jdk6中:字符串常量池中的"ab"指向新的地址。
    • jdk7起:字符串常量池中的"ab"指向的是调用intern()s1的地址
  • s2记录的是字符串常量池中的"ab"指向的地址

3)案例3

public void test() {String s1 = new String("a") + new String("b");String s2 = s1.intern();		 // 常量池没有"ab",会放入System.out.println(s1 == "ab");  // jdk6 false ; jdk7/8 trueSystem.out.println(s2 == "ab");  // jdk6 true  ; jdk7/8 true       
}
public void test() {String s1 = "ab";			   // 常量池中创建一个新的对象"ab"String s2 = new String("a") + new String("b");String s3 = s2.intern();	   // 常量池已有"ab",不会再放入System.out.println(s1 == s2);  // jdk6/7/8  falseSystem.out.println(s1 == s3);  // jdk6/7/8  true     
}

3、intern() 的效率:空间角度

public class StringInternTest {    static final int MAX_COUNT = 1000 * 10000;    static final String[] arr = new String[MAX_COUNT];   public static void main(String[] args) {        Integer [] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};      long start = System.currentTimeMillis();  for (int i = 0; i < MAX_COUNT; i++) {            
// 			arr[i] = new String(String.valueOf(data[i%data.length]));    		 // 不用intern   7256ms    arr[i] = new String(String.valueOf(data[i%data.length])).intern();   // 使用intern   1395ms}       long end = System.currentTimeMillis();   System.out.println("花费的时间为:" + (end - start));    try {           Thread.sleep(1000000);      } catch (Exception e) {      e.getStackTrace();    }  System.gc();}
}
  • 直接new:堆 和 字符串常量池 可能会存在相同的字符串的两个对象。
  • 使用intern():保证内存中相同的字符串对象只会有一个。

结论:对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用 intern()方法能够节省内存空间。

4、运行时内存案例

class Memory {public static void main(String[] args) {int i = 1;Object obj = new Object();Memory mem = new Memory();mem.foo(obj);}private void foo(Object param) {String str = param.toString().intern();System.out.println(str);	// java.lang.Object@42a57993}
}

在这里插入图片描述

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • overleaf如何引用文献
  • 时序预测 | Matlab实现SSA-TCN麻雀搜索算法优化时间卷积网络时序预测-递归预测未来数据(单输入单输出)
  • 【每日刷题】Day123
  • Java 21的Enhanced Deprecation的笔记
  • Android生成Java AIDL
  • URL.createObjectURL 与 FileReader:Web 文件处理两大法宝的对比
  • AI客服机器人开启企业客户服务新纪元
  • 『功能项目』眩晕图标显示【52】
  • 『功能项目』怪物的有限状态机【42】
  • 语言的枚举
  • Python编程 - 异常处理与文件读写
  • Rust编写Windows服务
  • Python精选200Tips:121-125
  • Unity-Transform-坐标转换
  • Prometheus+grafana+kafka_exporter监控kafka运行情况
  • 0基础学习移动端适配
  • CSS 三角实现
  • ES6语法详解(一)
  • Just for fun——迅速写完快速排序
  • Magento 1.x 中文订单打印乱码
  • Python代码面试必读 - Data Structures and Algorithms in Python
  • 不用申请服务号就可以开发微信支付/支付宝/QQ钱包支付!附:直接可用的代码+demo...
  • 产品三维模型在线预览
  • 基于组件的设计工作流与界面抽象
  • 聊聊spring cloud的LoadBalancerAutoConfiguration
  • 如何设计一个微型分布式架构?
  • 实现简单的正则表达式引擎
  • 优化 Vue 项目编译文件大小
  • FaaS 的简单实践
  • ​云纳万物 · 数皆有言|2021 七牛云战略发布会启幕,邀您赴约
  • # 利刃出鞘_Tomcat 核心原理解析(七)
  • #window11设置系统变量#
  • $.ajax中的eval及dataType
  • (152)时序收敛--->(02)时序收敛二
  • (160)时序收敛--->(10)时序收敛十
  • (27)4.8 习题课
  • (7)摄像机和云台
  • (附源码)流浪动物保护平台的设计与实现 毕业设计 161154
  • (过滤器)Filter和(监听器)listener
  • (牛客腾讯思维编程题)编码编码分组打印下标题目分析
  • (七)理解angular中的module和injector,即依赖注入
  • (三十五)大数据实战——Superset可视化平台搭建
  • (图文详解)小程序AppID申请以及在Hbuilderx中运行
  • (一)Kafka 安全之使用 SASL 进行身份验证 —— JAAS 配置、SASL 配置
  • ***通过什么方式***网吧
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .libPaths()设置包加载目录
  • .NET Compact Framework 3.5 支持 WCF 的子集
  • .net core使用RPC方式进行高效的HTTP服务访问
  • .net开发日常笔记(持续更新)
  • ?
  • @Documented注解的作用
  • @Resource和@Autowired的区别
  • @Transactional事务注解内含乾坤?
  • [Algorithm][动态规划][简单多状态DP问题][按摩师][打家劫舍Ⅱ][删除并获得点数][粉刷房子]详细讲解