深入理解java的String(进阶篇)

        String类想必对于java的学习者一点也不陌生。从刚开始接触java开始,我们写的第一个代码估计就是

System.out.println("hello world");

所以它的重要性也就不言而喻了。

       有时候我学东西时就发现了一个问题,有时候难以理解往往不是那些后来的高大上东西,反而是你最开始入门某一方面学的最基础的那一部分。

        我试图去解释这个现象,大概是因为,某一知识点从无到有,这是一个质的变化。变化幅度太大,导致我们在理解上会有些问题。而后续的那些知识,是基于此知识点而扩展出的,它的根基还是此知识点。

有时候,我们常常会忽略那些最基本的东西。比如亲情来说,不正是我们常常容易忽略的嘛。

-----------------好了,不话费了------------

一、char和String

  1.     char是表示的是字符,定义的时候用单引号,只能存储一个字符。例如  char='d';

       而String表示的是字符串,定义的时候用双引号,可以存储一个或者多个字符。例如:String="we  are young"。

     2.    char是基本数据类型, char在Java内存中是16位。而String是个类,属于引用数据类型。String类可以调用方法,具有面        向对象的特征。

二、String类和对象

      一般我们常见的String对象是这样初始化的:

String a = "apple";
String b = new String("apple");

       String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。在Java中,被final修饰的类是不         允许被继承的,并且该类中的成员方法都默认为final方法。

三、String对象在内存中的机制

    阅读以下内容,你得稍微知道点java内存的结构,知道哪些区存储哪些类型的数据

Java为String类型提供了缓冲池机制,当使用双引号定义对象时,Java环境首先去字符串缓冲池寻找内容相同的字符串,      如果存在就拿出来使用,否则就创建一个新的字符串放在缓冲池中。

例如:String a = "apple";

它会先寻找是否内容是apple的字符串,如果有,就直接把引用返回,如果没有,就会创建一个新值是apple的字符串。

因此,上述表达式有可能创建对象,也有可能不创建对象。

原理:

我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM             为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当我们创建字符串常量             时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串            不存在常量池中,就会实例化该字符串并且将其放到常量池中。

通俗一点理解就是:水果池子里有苹果串、香蕉串、西瓜串,当你字符串实例化时,它会到池子里找有没有这个串,如果       有,就把这个串的地址返回,如果没有,就在池子里新增这个串,然后返回其引用。


从官网的文档中,复制过来一句话。前一句大概意思是说:字符串是常量,一旦创建,它的值不能被改变。(后面一句略,      我不知道咋翻译)

这时候,你可能就会有疑问了,怎么不能改变。

String a="apple";
a="pear";

的确,上述表达式确实没有问题。但是你被它表象迷惑了。。

当a是“apple”时,你又把“pear”赋给a,它并没有改变原来的“apple”,只是把a原先指向的字符“apple”修改成了指向“pear”。      也就是说,它只是改变了指向。实际的那个值并没有改变。


Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。

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


现在我们再来看看下面这两个实例化的异同:

1.  

String str1 = "abc"; 
System.out.println(str1 == "abc");//true
步骤: 
1) 栈中开辟一块空间存放引用str1, 
2) String池中开辟一块空间,存放String常量"abc", 
3) 引用str1指向池中String常量"abc", 
4) str1所指代的地址即常量"abc"所在地址。

2. 

String str2 = new String("abc"); 
System.out.println(str2 == "abc"); //false
步骤: 
1) 栈中开辟一块空间存放引用str2, 
2) 堆中开辟一块空间存放一个新建的String对象"abc", 
3) 引用str2指向堆中的新建的String对象"abc", 
4) str2所指代的对象地址为堆中地址,而常量"abc"地址在池中。

四、趁热打铁

所谓实践是检验真理的唯一标准。下面我们来分析下几个表达式。

例1

public  viod test{
   String   str1="apple";
    String  str2="apple";
    System.out.println(str1==str2);//true
}

分析:当执行String str1="apple"时,JVM首先会去字符串池中查找是否存在"apple"这个字符串,如果不存在,则在字符串池中创建"apple"这个对象,然后将池中"apple"这个的引用地址返回给字符串常量str1,这样str1会指向池中"apple"这个字符串对象;如果存在,则不创建任何对象,直接将池中"apple"这个对象的地址返回,赋给字符串常量。当创建字符串对象str2时,字符串池中已经存在"apple"这个串,直接把字符串"apple"的引用地址返回给str2,这样str2指向了池中"apple"这个字符串,也就是说str1和str2指向了同一个字符串。

        注意:str1==str2;比较的是他们的引用值,不是内容值!!!

例2

public void test2(){
    String str3=new String("apple");
    String str4=new String("apple");
    System.out.println(str3==str4);//false 
}

分析: 采用new关键字新建一个字符串对象时,JVM首先在字符串池中查找有没有"apple"这个字符串,如果有,则不在池中再去创建"apple"这个字符串了,直接在堆中创建一个"apple"字符串对象,然后将堆中的这个"apple"对象的地址返回赋给引用str3,这样,str3就指向了堆中创建的这个"apple"字符串对象;如果没有,则首先在字符串池中创建一个"apple"字符串对象,然后再在堆中创建一个"apple"字符串对象,然后将堆中这个"apple"字符串对象的地址返回赋给str3引用,这样,str3指向了堆中创建的这个"apple"字符串对象。当执行String str4=new String("apple")时, 因为采用new关键字创建对象时,每次new出来的都是一个新的对象,也即是说引用str3和str4指向的是两个不同的对象。

         注意:虽然str3、str4的内容是创建在堆中,但是他的内部value还是指向JVM常量池的"apple".。

例3

public void test3(){
    String s1="helloworld";
    String s2="hello"+"world";
    System.out.println(s1==s2); //true 
}

分析:首先需要再次说一点,字符串常量是编译时候确定的,编译完成,生成class文件,那就不会再变了。当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中"helloworld”的一个引用。

例4

public void test4(){
    String s0="helloworld"; 
    String s1=new String("helloworld"); 
    System.out.println( s0==s1 ); //false  
    
}

分析:很显然,创建的方式都不一样。解释看上面两个初始化步骤。我的理解是,s1的引用是在堆中,s0的引用来自常量池中。

例5


public void test7(){
    String s0 = "ab"; 
    String s1 = "b"; 
    String s2 = "a" + s1; 
    System.out.println((s0 == s2)); //false
}

分析:这个时候s2的值不是编译时候能确定的,它已经不再会往常量池存放,是一个字符串变量。这个时候,底层是通过append方法,最终返回new的string。所以s2的地址只的不是常量池区域的地址,而是指向堆内存中的区域。

例6


public void test5(){
    String str1="abc";   
    String str2="def";   
    String str3=str1+str2;
    System.out.println(str3=="abcdef"); //false

分析:同上

总结:字面量"+"拼接是在编译期间进行的,拼接后的字符串存放在字符串池中;而字符串引用的"+"拼接运算是在运行时进行的,新创建的字符串存放在堆中。

对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如"I"+"love"+"java"; 的字符串相加,在编译期间便被优化成了"Ilovejava"。对于间接相加(即包含字符串引用),形如s1+s2; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。

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

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

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

new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致浪费池的空间)!


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

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


 

猜你喜欢

转载自blog.csdn.net/qq_36923376/article/details/84590825