基本概述
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
是线程安全的。