String类想必对于java的学习者一点也不陌生。从刚开始接触java开始,我们写的第一个代码估计就是
System.out.println("hello world");
所以它的重要性也就不言而喻了。
有时候我学东西时就发现了一个问题,有时候难以理解往往不是那些后来的高大上东西,反而是你最开始入门某一方面学的最基础的那一部分。
我试图去解释这个现象,大概是因为,某一知识点从无到有,这是一个质的变化。变化幅度太大,导致我们在理解上会有些问题。而后续的那些知识,是基于此知识点而扩展出的,它的根基还是此知识点。
有时候,我们常常会忽略那些最基本的东西。比如亲情来说,不正是我们常常容易忽略的嘛。
-----------------好了,不话费了------------
一、char和String
- 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中;