基本概述
String定义在java.lang包下的一个类,不是基本数据类型, 提供了字符串的比较、查找、截取、大小写转换等操作。1
2
3
4
5
6
7
8
9public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to
...
}
1) String类被final修饰,因此Sting类不能被继承(Integer等包装类也不能被继承), 并且它的成员方法都被final修饰,故字符串一旦创建就不能被修改。
2) String类实现了Serializable、Comparable、CharSequence。
3) String实例的值通过字符数组存储的。
1 | // 获取子串 |
从上面两个方法可以看出, 无论是substring、还是concat都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。 String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何操作都会生成新的对String对象
字符串常量池
常量池分为两大类:静态常量池和运行时常量池。静态常量池为Class文件(字节码)中的常量池, class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间;运行时常量池 是JVM虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。
内存区域
在HotSpot VM中字符串常量池是通过一个StringTable类实现的,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例中只有一份,被所有的线程共享;要注意的是,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。
在JDK6及之前版本,字符串常量池是放在方法区中,StringTable的长度是固定的1009;在JDK7版本中,字符串常量池被移到了堆中,StringTable的长度可以通过-XX:StringTableSize=66666参数指定。至于JDK7为什么把常量池移动到堆上实现,原因可能是由于方法区的内存空间太小且不方便扩展,而堆的内存空间比较大且扩展方便。
创建字符串
1) 使用双引号创建字符串,String str = "abc"。 编译期就已经确定存储到字符串常量池中。
先在栈中创建String类型的引用变量str(对象),JVM会先检查字符串常量池,如果"abc"在常量池中,返回"abc"的地址给str;若不在,则在字符串常量池中实例化该字符串"abc",返回"abc"的地址给str。编译期完成
2) 使用new创建字符串对象,String str2 = new String("abc"), 会存储到堆中,是运行期新创建的 。
先在栈中创建String类型的引用变量str2(对象),JVM会先检查字符串常量池,如果"abc"在常量池中,将字符串对象"abc"复制到堆中(产生新对象),并将地址返回给str2;若不在,则在字符串常量池中实例化该字符串对象"abc",再字符串对象"abc"复制到堆中(产生新对象),并将地址返回给str2。运行期创建
面试题String str = "abc"String str1 = "abc"
上面创建了几个String对象?
编译期栈中创建了2个Str和Str1,字符串常量池中创建了1个"abc"。String str2 = new String("abc")String str3 = new String("abc")
编译期栈中创建了2个Str2和Str3,字符串常量池中创建了1个"abc"对象,运行期创建了2个"abc"对象。每次new()必产生一个对象
1 | public static void main(String[] args) { |
- 单独使用””引号创建的字符串都是常量,编译期就已经确定存储到字符串常量池中;
- 使用
new String("")创建的对象会存储到堆中,是运行期新创建的; - 使用只包含常量的字符串连接符如
"aa"+"aa"创建的也是常量,编译期就能确定,已经确定存储到字符串常量池中; - 使用包含变量的字符串连接符如
"aa"+s1创建的对象是运行期才创建的,存储在堆中;
String.intern()
String.intern()是一个native方法,它的作用是: 把字符串加载到常量池中。
在jdk6中,如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用;否则,把字符串的值复制到字符串常量池,然后返回字符串常量池里这个字符串的引用;
在jdk7中,如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用;否则,在字符串常量池记录该字符串首次出现的实例引用,然后返回该引用。
字符串常量池可以保存字面量也可以保存字符串对象在堆中的引用。
1 | // 环境为jdk8 |
分析:jdk7及之后,输出false,true,true。执行第2行代码后,字符串常量池中有对象"1",s.intern()返回的是字符串常量池中对象"1"的引用,对象s是堆中字符串对象"1"的引用,不相等,故为false;执行第4行代码时,字符串常量池中有对象"2",对象"3",但没有对象"23",s1.intern()则将该字符串对象"23"的引用注册到字符串常量池中,并返回该引用,以后使用相同字面量(双引号形式)声明的字符串对象都指向该引用指向的地址 。s1也是堆中字符串对象"23"的引用,都指向同一个地址,故为true;执行第7行代码时,由于第6行执行了s1.intern(),所以s2指向堆中字符串对象"23",s1和s2指向的是同一地址,故为true。jdk6中,输出false,false,false。
PS:native修饰的方法是本地,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。
拼接字符串"+"
1 | public static void main(String[] args) { |
1) String中使用 + 字符串连接符进行字符串连接时,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接。
2) 接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。
上面代码执行过程为:String x = new StringBuilder("xxyy").append(a).append("zz").append("nn").append(b).toString();
String,StringBuilder,StringBuffer的区别
1) String为不可变序列,StringBuilder和StringBuffer为可变序列。
2) 执行速度 StringBuilder> StringBuffer> String。
3) StringBuilder是非线程安全的,StringBuffer中的方法大都采用了synchronized 关键字进行修饰,故是线程安全的,String是线程安全的。