1、String的本质
线程安全
打开String的源码,类注释中有这么一段话“Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings.Because String objects are immutable they can be shared.”。这句话总结归纳了String的一个最重要的特点:String是值不可变(immutable)的常量,是线程安全的(can be shared)。
不可继承
String类使用了final修饰符,表明了String类的第二个特点:String类是不可继承的。
在Java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。在早期的JVM实现版本中,被final修饰的方法会被转为内嵌调用以提升执行效率。而从Java SE5/6开始,就渐渐摈弃这种方式了。因此在现在的Java SE版本中,不需要考虑用final去提升方法调用效率。只有在确定不想让该方法被覆盖时,才将方法设置为final。
值不可变,存储方式为字符数组
private final char value[]; private final int count;
从final的修饰可以看出其不可变性
其中的String类的 concat方法为起扩容,如果方法的参数长度等于0这返回this,否则并组成一个新的string,并紧随其后拼接参数后返回新的string
我们看String类的concat方法。实现该方法第一步要做的肯定是扩大成员变量value的容量,扩容的方法重新定义一个大容量的字符数组buf。第二步就是把原来value中的字符copy到buf中来,再把需要concat的字符串值也copy到buf中来,这样子,buf中就包含了concat之后的字符串值。下面就是问题的关键了,如果value不是final的,直接让value指向buf,然后返回this,则大功告成,没有必要返回一个新的String对象。但是。。。可惜。。。由于value是final型的,所以无法指向新定义的大容量数组buf,那怎么办呢?“return new String(0, count + otherLen, buf);”,这是String类concat实现方法的最后一条语句,重新new一个String对象返回。这下真相大白了吧!
String类其实是通过char数组来保存字符串的。
总结:String实质是字符数组,两个特点:1、该类不可被继承;2、不可变性(immutable)。
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > count) { throw new StringIndexOutOfBoundsException(endIndex); } if (beginIndex > endIndex) { throw new StringIndexOutOfBoundsException(endIndex - beginIndex); } return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); } public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } char buf[] = new char[count + otherLen]; getChars(0, count, buf, 0); str.getChars(0, otherLen, buf, count); return new String(0, count + otherLen, buf); } public String replace(char oldChar, char newChar) { if (oldChar != newChar) { int len = count; int i = -1; char[] val = value; /* avoid getfield opcode */ int off = offset; /* avoid getfield opcode */ while (++i < len) { if (val[off + i] == oldChar) { break; } } if (i < len) { char buf[] = new char[len]; for (int j = 0 ; j < i ; j++) { buf[j] = val[off+j]; } while (i < len) { char c = val[off + i]; buf[i] = (c == oldChar) ? newChar : c; i++; } return new String(0, len, buf); } } return this;
从上面的三个方法可以看出,无论是sub操、concat还是replace操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。
“对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象”。
2、String的内存机制
JVM运行时,会将内存分为两个部分:堆和栈。堆中存放的是创建的对象,而栈中存放的方法调用过程的局部变量或引用。而设计Java字符串对象内存实现的时候,在堆中又开辟了一块很小的内存,称之为字符串常量池,专门用来存放特定的字符串对象。
创建Java字符串对象有两种常用的方式:
String 引用变量名="字符串内容"; String 应用变量名=new String(<参数序列>);
我们先来看看创建字符串对象的第一种方式内存如何分配的,代码如下:
String s1="osEye.net"; String s2="osEye.net";
如上图描述了引用对象的关系,以及内存的分配。Java实现的步骤如下:
- 查看字符串常量池中是否存在内容与“osEye.net”相同的字符串对象。
- 若没有,则新创建一个包含该内容的字符串对象,并让引用变量指向该对象。例如,创建字符串s1的时候,字符串常量池中没有,则创建一个新对象,并让引用s1指向该对象。
- 若已经存在包含该内容的字符串对象,则让字符串引用直接指向该对象。例如,在创建字符串s2的时候,字符串常量池中已经有包含该内容的对象了,所以引用s2直接指向已有的对象。
在来看看第二种创建字符串对象的方式:
String s1="osEye.net"; String s2=new String("osEye.net");
如上图描述了引用对象的关系,以及内存的分配。Java实现的步骤如下:
- 首先在堆(不是常量池)中创建一个包含指定内容的字符串对象,并将字符串引用指向该对象。例如上述代码中,使用new创建字符串s3,其会直接在堆中创建一个内容为“osEye.net”的字符串对对象,并将引用s3指向该对象。
- 去字符串常量池中查看,是否有包含该内容的对象。
- 若有,则将new出来的字符串对象与字符串常量池中内容相同的对象联系起来。例如,本例中s3所指向的对象与s1所指向的联系起来。
- 若没有,则在字符串常量池再创建一个包含该内容的字符串对象,并将堆中的对象与字符串常量池中新创建出来的对象联系起来。
我们知道,new出来的字符串对象和字符串常量池中的对象是有联系的,可以通过intern方法来查看,方法签名:
public String intern()
此方法将指定的字符串引用在字符串常量池中对应的对象,若其指向的对象本身就在字符串常量池中,则直接将自己指向的对象返回;若该字符串引用指向的对象在堆中,则返回字符串常量池中与其联系的对象。实例如下:
package net.oseye; public class ExceptionTest { public static void main(String[] args) { String s1="osEye.net"; String s2=new String("osEye.net"); if(s1==s2){ System.out.println("字符串引用s1和字符串引用s2所指向的是同一个对象"); }else{ System.out.println("字符串引用s1和字符串引用s2所指向的不是同一个对象"); } if(s1.intern()==s2.intern()){ System.out.println("字符串引用s1和字符串引用s2在字符串常量池中联系的是同一个对象"); }else{ System.out.println("字符串引用s1和字符串引用s2在字符串常量池中联系的不是同一个对象"); } } }
输出结果:
字符串引用s1和字符串引用s2所指向的不是同一个对象 字符串引用s1和字符串引用s2在字符串常量池中联系的是同一个对象