深入理解Java中的字符串

一、String类源码

想要了解一个类,最直观的就是去看源码

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** final 的char数组存储对象 */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

    /**
    new string的调用方法
     */
    public String() {
        this.value = "".value;
    }

从如上我们可以看出

  • String是final类,不可被继承;成员变量都被Final修饰;被Final修饰的类其类中的成员方法都默认是final 方法。
  • String是通过char数组来保存字符串的。
    总结一下就是:
  • String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象

二、常量池

JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串(这点对理解上面至关重要)。

Java中的常量池,实际上分为两种形态:

int a = 1;
int b = 2;
string c = “abcdefg”;
string d = “abcdefg”;

Class常量池

Class常量池(静态常量池)可以理解为是Class文件中的资源仓库。 Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)。
常量池中主要存放两大类常量:字面量和符号引用。

  • 字面量
    字面量就是指由字母、数字等构成的字符串或者数值常量
    字面量只可以右值出现,所谓右值是指等号右边的值,如:int a=1 这里的a为左值,1为右值。在这个例子中1就是字面量。
  • 符号引用
    符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量:
    1、类和接口的全限定名
    2、字段的名称和描述符
    3、方法的名称和描述符
    上面的a,b就是字段名称,就是一种符号引用,还有Math类常量池里的 com.huohuo.jvm.Math 是类的全限定名,main和compute是方法名称,()是一种UTF8格式的描述符,这些都是符号引用。
    这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引用,也就是我们说的动态链接了。例如,compute()这个符号引用在运行时就会被转变为compute()方法具体代码在内存中的地址,主要通过对象头里的类型指针去转换直接引用。

运行时常量池

  • 运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

字符串常量池

1)字符串常量池的设计思想

字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
为字符串开辟一个字符串常量池,类似于缓存区
创建字符串常量时,首先查询字符串常量池是否存在该字符串
存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中

2)字符串常量池位置

Jdk1.6及之前: 有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池
Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里
Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里i
JDK版本8测试字符串常量池具体在哪里?

修改一下JVM变量,然后执行代码:

-Xms:表示初始化JAVA堆的大小及该进程刚创建出来的时候,他的专属JAVA堆的大小,一旦对象容量超过了JAVA堆的初始容量,JAVA堆将会自动扩容到-Xmx大小。

-Xmx:表示java堆可以扩展到的最大值,在很多情况下,通常将-Xms和-Xmx设置成一样的,因为当堆不够用而发生扩容时,会发生内存抖动影响程序运行时的稳定性。
XX:MetaspaceSize XX:MaxMetaspaceSize则是对元空间大小的操作

-Xms6M -Xmx6M -XX:MetaspaceSize=6M -XX:MaxMetaspaceSize=6M

public class StringJvmTest {

	public static void main(String[] args) {
		ArrayList<String> list = new ArrayList<String>();
		for (int i = 0; i < 10000000; i++) {
			String str = String.valueOf(i).intern();
			list.add(str);
		}
	}
}

我们执行看结果发现是java heap space,结果表明,字符串常量池在堆空间。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at jvm.gc.StringJvmTest.main(StringJvmTest.java:20)

jdk7及以上:Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
jdk6:Exception in thread “main” java.lang.OutOfMemoryError: PermGen space

三、三种字符串操作

1)直接赋值字符串

 // s指向常量池中的引用
String s = "huohuo"; 

这种方式创建的字符串对象,只会在常量池中。
因为有"huohuo"这个字面量,创建对象s的时候,JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象
如果有,则直接返回该对象在常量池中的引用;
如果没有,则会在常量池中创建一个新对象,再返回引用。

2)new String();

// s1指向内存中的对象引用
String s1 = new String("huohuo");  

这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。
步骤大致如下:
因为有"huohuo"这个字面量,所以会先检查字符串常量池中是否存在字符串"huohuo"
不存在,先在字符串常量池里创建一个字符串对象;再去内存中创建一个字符串对象"huohuo";
存在的话,就直接去堆内存中创建一个字符串对象"huohuo";
最后,将内存中的引用返回。
在这里插入图片描述

3)intern方法

		String s1 =new String("huohuo");
		String s2 = s1.intern();

		System.out.println(s1 == s2);

如上代码返回什么呢?执行一下,结果是False。

String中的intern方法是一个 native 的方法,当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将intern返回的引用指向当前字符串 s1(jdk1.6版本需要将 s1 复制到字符串常量池里)。

在这里插入图片描述

简答一句理解:intern肯定是返回常量池里的东西,有可能是变量,有可能是引用

四、两句代码创建了多少对象?

字符串常量池底层是hotspot的C++实现的,底层类似一个 HashTable, 保存的本质上是字符串对象的引用。

只分析JDK1.7以上版本。这就是当字符串常量池中不存在对象时,返回的是堆内存的引用,所以两者相等。

String s1 = new String("huo") + new String("jing");
String s2 = s1.intern();
 
System.out.println(s1 == s2);

怎么分析短短的两句代码?
new String(“huo”) 和 new String(“jing”)分别在字符串常量池和堆空间创建了对象。加号则在堆空间生成了huojing的对象。
当调用.intern的时候,由于字符串常量池无此对象,所以就会生成一个对堆内huojing对象的引用然后返回给s2,所以最终s1和s2都指向了同一个变量。
在这里插入图片描述

三、String == 测试

1)String ==比较

        String s1 = "abc";
        String s2 = "abc";

        String s3 = new String("abc");
        String s4 = new String ("abc");

        System.out.println(s1==s2); TRUE
        System.out.println(s1==s3); FALSE
        System.out.println(s3==s4); FALSE

如上说了常量池的概念,所以s1==s2其实都是指向了常量池,它们在编译期就被确定了,所以为TRUE;当执行new String的时候,是在堆直接开辟了新空间。

2)String 拼接 ==比较

        String s5="abcdef";
        String s6="ab";
        String s7="cdef";
        String s8=s6+s7;
        String s9="ab"+"cdef";
        final String s10="ab";
		String s11=s10+"cdef";

        System.out.println(s5==s8); FALSE
        System.out.println(s5==s9); TRUE
        System.out.println(s8==s9); FALSE
        System.out.println(s5==s11);TRUE

我们先看一下字节码文件,看为何s5==s9?

L0
LINENUMBER 11 L0
LDC “abcdef”
ASTORE 1
L4
LINENUMBER 15 L4
LDC “abcdef”
ASTORE 5

发现两值是一样的。字符串常量相加,jvm 会进行优化,不会创建 StringBuilder 对象。

”ab”和”cdef”都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s9同样在编译期就被优化为一个字符串常量"abcdef",所以s9也是常量池中” zhuge”的一个引用

然后看一下,s5==s11 为何也是TRUE?

L0
LINENUMBER 11 L0
LDC “abcdef”
ASTORE 1
L6
LINENUMBER 17 L6
LDC “abcdef”
ASTORE 7

我们发现其值也是一样的。
s10为final类型,当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。所以其就等同于s5==s9

然后我们再看其他的为何不等呢?
是因为创建了一个StringBuilder对象,然后用StringBuilder对象执行append方法来创建出字符串对象“ab”,然后再转换成为String。但是这个转换后的String对象,是放在堆里面的。而s5是字符串常量,放在常量池里面。所以返回的是false。
对应的整个字节码文件如下:可以很清楚的看到哪个创建了StringBuilder对象,哪个没创建。

// class version 52.0 (52)
// access flags 0x21
public class baseClassInfo/StringClass1 {

  // compiled from: StringClass1.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 9 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LbaseClassInfo/StringClass1; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 11 L0
    LDC "abcdef"
    ASTORE 1
   L1
    LINENUMBER 12 L1
    LDC "ab"
    ASTORE 2
   L2
    LINENUMBER 13 L2
    LDC "cdef"
    ASTORE 3
   L3
    LINENUMBER 14 L3
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 3
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 4
   L4
    LINENUMBER 15 L4
    LDC "abcdef"
    ASTORE 5
   L5
    LINENUMBER 16 L5
    LDC "ab"
    ASTORE 6
   L6
    LINENUMBER 17 L6
    LDC "abcdef"
    ASTORE 7
   L7
    LINENUMBER 19 L7
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    ALOAD 4
    IF_ACMPNE L8
    ICONST_1
    GOTO L9
   L8
   FRAME FULL [[Ljava/lang/String; java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String] [java/io/PrintStream]
    ICONST_0
   L9
   FRAME FULL [[Ljava/lang/String; java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String] [java/io/PrintStream I]
    INVOKEVIRTUAL java/io/PrintStream.println (Z)V
   L10
    LINENUMBER 20 L10
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    ALOAD 5
    IF_ACMPNE L11
    ICONST_1
    GOTO L12
   L11
   FRAME SAME1 java/io/PrintStream
    ICONST_0
   L12
   FRAME FULL [[Ljava/lang/String; java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String] [java/io/PrintStream I]
    INVOKEVIRTUAL java/io/PrintStream.println (Z)V
   L13
    LINENUMBER 21 L13
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 4
    ALOAD 5
    IF_ACMPNE L14
    ICONST_1
    GOTO L15
   L14
   FRAME SAME1 java/io/PrintStream
    ICONST_0
   L15
   FRAME FULL [[Ljava/lang/String; java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String] [java/io/PrintStream I]
    INVOKEVIRTUAL java/io/PrintStream.println (Z)V
   L16
    LINENUMBER 22 L16
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    LDC "ab"
    IF_ACMPNE L17
    ICONST_1
    GOTO L18
   L17
   FRAME SAME1 java/io/PrintStream
    ICONST_0
   L18
   FRAME FULL [[Ljava/lang/String; java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String] [java/io/PrintStream I]
    INVOKEVIRTUAL java/io/PrintStream.println (Z)V
   L19
    LINENUMBER 23 L19
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    ALOAD 7
    IF_ACMPNE L20
    ICONST_1
    GOTO L21
   L20
   FRAME SAME1 java/io/PrintStream
    ICONST_0
   L21
   FRAME FULL [[Ljava/lang/String; java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String java/lang/String] [java/io/PrintStream I]
    INVOKEVIRTUAL java/io/PrintStream.println (Z)V
   L22
    LINENUMBER 24 L22
    RETURN
   L23
    LOCALVARIABLE args [Ljava/lang/String; L0 L23 0
    LOCALVARIABLE s5 Ljava/lang/String; L1 L23 1
    LOCALVARIABLE s6 Ljava/lang/String; L2 L23 2
    LOCALVARIABLE s7 Ljava/lang/String; L3 L23 3
    LOCALVARIABLE s8 Ljava/lang/String; L4 L23 4
    LOCALVARIABLE s9 Ljava/lang/String; L5 L23 5
    LOCALVARIABLE s10 Ljava/lang/String; L6 L23 6
    LOCALVARIABLE s11 Ljava/lang/String; L7 L23 7
    MAXSTACK = 3
    MAXLOCALS = 8
}

四、在使用 HashMap 的时候,用 String 做 key 有什么好处?

HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以 当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。

五、StringBuffer、StringBuilder

1)可变&& 不可变

StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这 两种对象都是可变的。
String中的对象是不可变的,也就可以理解为常量,线程安全。

2)线程安全 && 不安全

String中的对象是不可变的,也就可以理解为常量,线程安全。
。StringBuffer对方法加了同步锁或者对调用 的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全 的。

3)性能

每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。 StringBuffer每次都会对StringBuffer对象本身进行操 作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获 得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

4)使用总结

如果要操作少量的数据用 String
单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

六、String,StringBuilder ==测试

1)StringBuilder后比较.intern,返回true

String str2 = new StringBuilder("计算机").append("技术").toString();   //没有出现"计算机技术"字面量,所以不会在常量池里生成"计算机技术"对象
System.out.println(str2 == str2.intern());  //true

我们去看StringBuiler源码可以发现,其底层toString是newString,所以是在堆空间创建了对象。
字符串常量池:“计算机"和"技术”
堆内存:str1引用的对象"计算机技术"
堆内存中还有个StringBuilder的对象,但是会被gc回收,StringBuilder的toString方法会new String(),这个String才是真正返回的对象引用。
没有出现"计算机技术"字面量,所以不会在常量池里生成"计算机技术"对象
所以当.intern的时候,返回了字符串常量池对堆内对象的引用,所以str2==str2.intern返回true.

2)StringBuilder后比较.intern,返回false

String str1 = new StringBuilder("ja").append("va").toString();    //没有出现"java"字面量,所以不会在常量池里生成"java"对象
System.out.println(str1 == str1.intern());  //false

唯一的区别就是:java是关键字,在JVM初始化的相关类里肯定早就放进字符串常量池了,所以str1.intern()是说的字符串常量池的“java”对象,而str1是指的堆内的“java”对象,所以返回false.

3)string 和stringBuiler .intern,返回false

String s1=new String("test");  
System.out.println(s1==s1.intern());   //false
//"test"作为字面量,放入了池中,而new时s1指向的是heap中新生成的string对象,s1.intern()指向的是"test"字面量之前在池中生成的字符串对象

String s2=new StringBuilder("abc").toString();
System.out.println(s2==s2.intern());  //false
//同上
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值