Java-基础 String - 字符串的理解


String - 字符串

整理了一下Java字符串 基础的一些理解,有错请帮忙指出,谢谢~

字符串的创建和存储

创建形式

  • String string = “Hello World”;

    • 在字符串常量区查找是否存在"Hello World",如果不存在,则在字符串常量区开辟一个内存空间,实例化"Hello World";如果存在,则不另外开辟空间,在栈区开辟空间,存放变量名称string ,string 指向字符串常量池中"Hello World"的实例引用(内存地址)。

      先查找字符串常量区,字符串已存在则直接返回引用,若不存在,先创建再返回

      • 栈中变量string 指向-> 字符串常量区对象 "Hello World"
  • String string = new String(“Hello World”);

    • new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在字符串常量区开辟一个内存空间,实例化"Hello World"。再在堆中创建一个对象,然后返回堆中的地址
      • 栈中变量string -> 堆中new String对象

总结:基本类型的变量数据和对象的引用都是放在栈里面的,对象本身放在堆里面,显式的String常量放在常量池,String对象放在堆中。

JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池

由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串

常量池的说明:

常量池之前是放在方法区里面的,也就是在永久代里面的,从JDK7开始移到了堆里面。

代码示例

  1. 显式的String常量
// 在字符串常量区查找"Hello", 没找到,在字符串常量区开辟一个内存空间,存放"Hello",然后把引用返回给strA
String strA = "Hello";	

// 在字符串常量区查找"Hello", 找到,然后把引用返回给strB
String strB = "Hello";	

// 此时,strA和strB变量保存的引用是同一个,字符串常量区中的对象
strA == strB;	// true
  1. String对象
// Class被加载时就在常量池中创建了一个值为Hello的String对象

// 在堆里创建new String("Hello")对象
String strC = new String("Hello");

// 在堆里创建new String("Hello")对象
String strD = new String("Hello");

// 此时,strA和strB变量保存的引用是不同的,分别是堆中保存的两个String对象
strC == strD;	// false

String 类

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{
    /** The value is used for character storage. */
    private final char value[];

    /** The offset is the first index of the storage that is used. */
    private final int offset;

    /** The count is the number of characters in the String. */
    private final int count;

    /** 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;

    ........
}
  1. String类 是 final类

    不能被继承,并且它的成员方法都默认为final方法

  2. String类 通过char[] 来保存字符串

    JDK9 将char[] 改成了 byte[],原因是为了节约String占用的内存。

方法实现

// 省略.. 不占用篇幅
public String substring(int beginIndex, int endIndex) {
    ...
}

public String concat(String str) {
    ...
}

public String replace(char oldChar, char newChar) {
    ...
}
  1. 这三个方法都是重新生成了新的字符串,最原始的字符串并没有改变

“String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象”

相关示例

  • 采用字面量赋值
String str1 = "aaa";
String str2 = "aaa";

str1 == str2; 	// true
  • 采用new关键字新建
String str3 = new String("aaa");
String str4 = new String("aaa");

str3 == str4 // false
  • 编译期确定
String s0="helloworld";
String s1="helloworld";
String s2="hello"+"world";

s0==s1;	// true 
s0==s2;	// true 

当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量

  • 编译期无法确定
String s0 = "helloworld"; 
String s1 = new String("helloworld"); 
String s2 = "hello" + new String("world"); 

s0==s1; //false  
s0==s2; //false 
s1==s2; //false

用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。

String str1="abc";   
String str2="def";   
String str3=str1+str2;

str3=="abcdef"; //false

因为str3指向堆中的"abcdef"对象,而"abcdef"是字符串池中的对象,所以结果为false。JVM对String str=“abc"对象放在常量池中是在编译时做的,而String str3=str1+str2是在运行时刻才能知道的。new对象也是在运行时才做的。而这段代码总共创建了5个对象,字符串池中两个、堆中三个。+运算符会在堆中建立来两个String对象,这两个对象的值分别是"abc"和"def”,也就是说从字符串池中复制这两个值,然后在堆中创建两个对象,然后再建立对象str3,然后将"abcdef"的堆地址赋给str3。

  1. 栈中开辟一块中间存放引用str1,str1指向池中String常量"abc"。
  2. 栈中开辟一块中间存放引用str2,str2指向池中String常量"def"。
  3. 栈中开辟一块中间存放引用str3。
  4. str1 + str2通过StringBuilder的最后一步toString()方法还原一个新的String对象"abcdef",因此堆中开辟一块空间存放此对象。
  5. 引用str3指向堆中(str1 + str2)所还原的新String对象。
  6. str3指向的对象在堆中,而常量"abcdef"在池中,输出为false。
String s0 = "ab"; 
String s1 = "b"; 
String s2 = "a" + s1; 

System.out.println((s0 == s2)); //result = false

JVM对于字符串引用,由于在字符串的"+“连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a” + s1无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给s2。所以上面程序的结果也就为false。

  • 编译期优化
String s0 = "a1"; 
String s1 = "a" + 1; 
System.out.println((s0 == s1)); //result = true  

String s2 = "atrue"; 
String s3= "a" + "true"; 
System.out.println((s2 == s3)); //result = true  

String s4 = "a3.4"; 
String s5 = "a" + 3.4; 
System.out.println((s4 == s5)); //result = true

在程序编译期,JVM就将常量字符串的"+“连接优化为连接后的值,拿"a” + 1来说,经编译器优化后在class中就已经是a1。在编译期其字符串常量的值就确定下来,故上面程序最终的结果都为true。

  • 字符串引用的“+” 和 字符串常量的 “+”
String test = "javalanguagespecification";
String str = "java";
String str1 = "language";
String str2 = "specification";

test == "java" + "language" + "specification";	// true
test == str + str1 + str2;						// false
  1. 字符串字面量拼接操作是在Java编译器编译期间就执行了,并且直接将这个常量放入字符串池中,这样做实际上是一种优化,将3个字面量合成一个,避免了创建多余的字符串对象。
  2. 字符串引用的"+"运算是在Java运行期间执行的,它会在堆内存中重新创建一个拼接后的字符串对象。

总结:

  1. 字面量"+"拼接是在编译期间进行的,拼接后的字符串存放在字符串池中;
  2. 而字符串引用的"+"拼接运算实在运行时进行的,新创建的字符串存放在堆中。
String s0 = "ab"; 
final String s1 = "b"; 
String s2 = "a" + s1;  

s0 == s2; //result = true

对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"a" + s1和"a" + "b"效果是一样的。

String s0 = "ab"; 
final String s1 = getS1(); 
String s2 = "a" + s1; 

s0 == s2; //result = false 

虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此s0和s2指向的不是同一个对象,故上面程序的结果为false。

String.intern()

**intern方法使用:**一个初始为空的字符串池,它由类String独自维护。当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。

它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。

存在于.class文件中的常量池,在运行期间被jvm装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,java查找常量池中是否有相同unicode的字符串常量,如果有,则返回其引用,如果没有,则在常量池中增加一个unicode等于str的字符串并返回它的引用。

String s0 = "Jackson"; 
String s1 = new String("Jackson"); 
String s2 = new String("Jackson"); 

System.out.println( s0 == s1 ); // false


s1.intern(); 					// 虽然执行了s1.intern(),但它的返回值没有赋给s1
s2 = s2.intern(); 				// 把常量池中"Jackson"的引用赋给s2 
System.out.println(s0 == s1);   // flase
System.out.println(s0 == s1.intern()); // true 说明s1.intern()返回的是常量池中"Jackson"的引用
System.out.println(s0 == s2);	// true

总结

结合上面例子,总结如下:

  1. 单独使用""引号创建的字符串都是常量,编译期就已经确定存储到String Pool中;

  2. 使用new String("")创建的对象会存储到heap中,是运行期新创建的;

  3. 使用只包含常量的字符串连接符如"aa" + "aa"创建的也是常量,编译期就能确定,已经确定存储到String Pool中;

  4. 使用包含变量的字符串连接符如"aa" + s1创建的对象是运行期才创建的,存储在heap中;

  5. 使用String不一定创建对象

  6. 使用new String,一定创建对象

  7. String相关的+

    1. String中使用 + 字符串连接符进行字符串连接时,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接;
    2. 接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。

    也就是说String c = “xx” + "yy " + a + “zz” + “mm” + b; 实质上的实现过程是:

    String c = new StringBuilder("xxyy ").append(a).append(“zz”).append(“mm”).append(b).toString();

  8. String的不可变性导致字符串变量使用+号的代价:

// 变量s的创建等价于 String s = "abc"; 由上面例子可知编译器进行了优化,这里只创建了一个对象。
String s = "a" + "b" + "c"; 

String s1  =  "a"; 
String s2  =  "b"; 
String s3  =  "c"; 
String s4  =  s1  +  s2  +  s3;

s4不能在编译期进行优化,其对象创建相当于:

StringBuilder temp = new StringBuilder();   
temp.append(a).append(b).append(c);   
String s = temp.toString();
public class Test {
    public static void main(String args[]) {
        String s = "";
        
        for(int i = 0; i < 100; i++) {
            s += "a";	
            // 每次循环循环
            // StringBuilder temp = new StringBuilder(); 
            // s = temp.append(s).append("a").toString();
        }
    }
}

每做一次 + 就产生个StringBuilder对象,然后append后就扔掉。下次循环再到达时重新产生个StringBuilder对象,然后 append 字符串,如此循环直至结束。 如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。

所以对于在循环中要进行字符串连接的应用,一般都是用StringBuffer或StringBulider对象来进行append操作。

  1. String、StringBuffer、StringBuilder的区别

    • 可变与不可变:

      • String是不可变字符串对象
      • StringBuilder和StringBuffer是可变字符串对象
    • 是否多线程安全:

      • String中的对象是不可变的,为常量,显然是线程安全
      • StringBuffer和StringBuilder中的方法和功能完全是等价的
      • StringBuffer中的方法大都采用了synchronized关键字修饰,因此是线程安全
      • StringBuilder,无synchronized修饰,非线程安全
    • 执行效率:

      • StringBuilder > StringBuffer > String

      当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;
      当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。

  2. String中的final:

    **final只对引用的"值"(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。**至于它所指向的对象的变化,final是不负责的。

  3. 关于String str = new String(“abc”)创建了多少个对象?

    该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值