String不是基本数据类型。
我们可以看一下jdk中的String.java源码(源码使用的是jdk1.8的版本),我简要的摘录如下:
public final class String
implements java.io.Serializable, Comparable, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
首先String类是final类,不能被继承,其次String类其实是通过char数组来保存字符串的,它值是不可变的。同时我们发现,String在它对自身的操作的时候,比如replace、subString等,实际在内部实现都是创建了一个全新的String对象(也就是说进行这些操作后,最原始的字符串并没有被改变)。看下面的源码片段:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
“String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象”。
由于字符串我们使用很频繁,同时分配字符串就像分配其他对象一样,如果这样的话,那么会对性能和内存的小号有一定的影响。所以JVM在实例化字符串的时候进行了一些优化:使用字符串常量池。
那么,什么是字符串常量值?
每当我们创建一个字符串对象时,首先就会检查字符串常量池中是否存在面值相等的字符串,如果有,则不再创建,直接返回字符串常量池中对该对象的引用;若没有,则创建然后放入到字符串常量池中并且返回新建对象的引用。这个机制是非常有用的,因为可以提高效率,减少了内存空间的占用。所以在使用字符串的过程中,推荐使用直接赋值(即String s=”aa”),除非有必要才会新建一个String对象(即String s = new String(”aa”))。
那么这两个有什么区别呢?
在我之前的博客中曾经说过:“= =“其实是比较两个对象的的内存地址”。那么我们使用”= ="来对上面的描述进行一些拓展。
废话少说上代码:
private void test1(){
String s = "s";
String s1 = "s";
System.out.println(s==s1);
}
输出:true
这种场景不必多说了吧,这就是设置字符串常量池的初衷。
解析:
执行String s=“s”:JVM首先会去字符串常量池中查找是否存在"s"这个对象,不存在,则在字符串常量池中创建"s"这个对象,然后将字符串常量池中"s"这个对象的引用地址返回给栈中的字符串常量s,这样s会指向池中"s"这个字符串对象;
执行String s1 = “s”:查找"s",发现存在,则不创建任何对象,直接将池中"s"这个对象的地址返回,赋给字符串常量。
也就是说str1和str2指向了同一个对象,因此语句System.out.println(s == s1)输出true。具体见下图:
private void test2(){
String s2 = new String("s");
String s3 = new String("s");
System.out.println(s2==s3);
}
输出:false
解析:
因为比较的是s2==s3,抛开上面的图不说,既然是使用了new关键字来构造String,那么s2和s3的内存地址肯定是不一样的。图示为了清晰说明。
private void test3() {
String s = "friend";
String s1 = "friend";
String s2 = "fri" + "end";
System.out.println(s == s1);
System.out.println(s1 == s2);
}
输出: true true
解析:这里比较让我们迷惑的是s2这个String,它指向的的是一个堆中的一个String对象,还是直接指向字符串常量池呢?答案是直接指向字符串常量池中的“friend”。因为“fri”和“end”两个都是字符串常量,s2是由两个字符串常量连接起来的,自己也是字符串常量,这是在编译期就确定的事。
private void test4(){
String s = "friend";
String s1 = new String("friend");
String s2 = "fri" + new String("end");
System.out.println(s==s1);
System.out.println(s==s2);
System.out.println(s1==s2);
}
输出: false false false
解析:s直接指向字符串常量池中的“friend”;s1指向堆中的String对象;s2比较让人纠结,其实s2在编译期不能确定它的值,所以它最后会指向堆中的String对象。
private void test5(){
String s = "fri";
String s1 = "end";
String s2 = s1 + s;
System.out.println(s2 == "friend");
}
输出: false
解析:经过了上面的场景4的例子,这里我们就清楚了,s2在编译器是不能确定的,因为s1和s的值是字符串常量池中的引用(如果是这样写String s2 = “fri”+“end”;就能在编译期确定)。那么这句String s2 = s1+s;具体做了什么呢?主要是做了以下几件事:1)+运算符将会在堆中建立两个String对象,然后再在堆中建立s2指向的对象,然后在字符串常量池中创建“friend”,将其引用指向堆中的s2的对象。
private void test6(){
String s = "A1";
String s1 = "A"+1;
String s2 = "A3.4";
String s3 = "A"+ 3.4;
System.out.println(s==s1);
System.out.println(s2==s3);
}
输出:true true
解析:这里s1和s3分别是将字符串分别和int和小数进行“+”操作,可能会对我们造成困扰,我们只要知道在编译期进行相关的优化,可以直接确定s2和s3的值。所以s和s1、s2和s3分别指向字符串常量池中的相同字符串。
private void test7(){
String s = "friend";
String s1 = "end";
String s2 = "fri"+s1;
System.out.println(s==s2);
}
输出:false
解析:编译期无法确定s2的值,所以s2最后指向的是堆中的String对象。
以上总结起来如下:形如"s"、“hello"等使用+拼接是在编译期间进行的,拼接后的字符串存放在字符串常量池中;而字符串引用的”+“拼接运算是在运行时进行的,新创建的字符串存放在堆中。
对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如"fri”+“end”; 的字符串相加,在编译期间便被优化成了"friend"。对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。
String使用private final char value[]来实现字符串的存储,也就是说String对象创建之后,就不能再修改此对象中存储的字符串内容,就是因为如此,才说String类型是不可变的(immutable)。
然而,String类对象确实有编辑字符串的功能,比如replace()。这些编辑功能是通过创建一个新的对象来实现的,而不是对原有对象进行修改。例如:
s = s.replace(“World”, “Universe”);
上面对s.replace()的调用将创建一个新的字符串"Hello Universe!",并返回该对象的引用。通过赋值,引用s将指向该新的字符串。如果没有其他引用指向原有字符串"Hello World!",原字符串对象将被垃圾回收。
创建字符串的方式归纳起来有两类:
(1)使用"“引号创建字符串;
(2)使用new关键字创建字符串。
结合上面例子,总结如下:
(1)单独使用”“引号创建的字符串都是常量,编译期就已经确定存储到String Pool中;
(2)使用new String(”")创建的对象会存储到heap中,是运行期新创建的;
new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致浪费池的空间)!
(3)使用只包含常量的字符串连接符如"aa" + “aa"创建的也是常量,编译期就能确定,已经确定存储到String Pool中;
(4)使用包含变量的字符串连接符如"aa” + s1创建的对象是运行期才创建的,存储在heap中;
对于这三者使用的场景做如下概括(参考:《编写搞质量代码:改善java程序的151个建议》):
1、String:在字符串不经常变化的场景中可以使用String类,如:常量的声明、少量的变量运算等。
2、StringBuffer:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装等。
3、StringBuilder:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,如SQL语句的拼装、JSON封装等。
部分转载自 https://www.cnblogs.com/xiaoxi/p/6036701.html ,如有侵权,请联系我。
上述网址中还有许多干货!