深入理解java String

基本概述

    String定义在java.lang包下的一个类,不是基本数据类型, 提供了字符串的比较、查找、截取、大小写转换等操作。

1
2
3
4
5
6
7
8
9
public 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类实现了SerializableComparableCharSequence
3) String实例的值通过字符数组存储的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 获取子串
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 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);
}

    从上面两个方法可以看出, 无论是substring、还是concat都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。 String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何操作都会生成新的对String对象

字符串常量池

    常量池分为两大类:静态常量池和运行时常量池。静态常量池为Class文件(字节码)中的常量池, class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间;运行时常量池 是JVM虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
    JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串

内存区域

    在HotSpot VM中字符串常量池是通过一个StringTable类实现的,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例中只有一份,被所有的线程共享;要注意的是,如果放进String PoolString非常多,就会造成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编译期完成
CreateString

2) 使用new创建字符串对象,String str2 = new String("abc"), 会存储到堆中,是运行期新创建的 。
    先在栈中创建String类型的引用变量str2(对象),JVM会先检查字符串常量池,如果"abc"在常量池中,将字符串对象"abc"复制到堆中(产生新对象),并将地址返回给str2;若不在,则在字符串常量池中实例化该字符串对象"abc",再字符串对象"abc"复制到堆中(产生新对象),并将地址返回给str2运行期创建
CreateString

面试题
String str = "abc"
String str1 = "abc"
上面创建了几个String对象?
编译期栈中创建了2个StrStr1,字符串常量池中创建了1个"abc"
String str2 = new String("abc")
String str3 = new String("abc")
编译期栈中创建了2个Str2Str3,字符串常量池中创建了1个"abc"对象,运行期创建了2个"abc"对象。每次new()必产生一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
String str = "abc";
String str1 = "abc";
System.out.println(str == str1); // true
String str2 = new String("bcd");
String str3 = new String("bcd");
String str4 = new String("abc");
System.out.println(str2 == str3); // false
System.out.println(str == str3); // false
String s1 = new String("111");
String s2 = "sss111";
String s3 = "sss" + "111";
String s4 = "sss" + s1;
System.out.println(s2 == s3); //true
System.out.println(s2 == s4); //false
System.out.println(s2 == s4.intern()); //true
}
  • 单独使用””引号创建的字符串都是常量,编译期就已经确定存储到字符串常量池中;
  • 使用new String("")创建的对象会存储到堆中,是运行期新创建的;
  • 使用只包含常量的字符串连接符如"aa" +"aa"创建的也是常量,编译期就能确定,已经确定存储到字符串常量池中;
  • 使用包含变量的字符串连接符如"aa" +s1创建的对象是运行期才创建的,存储在堆中;
String.intern()

    String.intern()是一个native方法,它的作用是: 把字符串加载到常量池中。
    在jdk6中,如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用;否则,把字符串的值复制到字符串常量池,然后返回字符串常量池里这个字符串的引用;
    在jdk7中,如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用;否则,在字符串常量池记录该字符串首次出现的实例引用,然后返回该引用。
    字符串常量池可以保存字面量也可以保存字符串对象在堆中的引用

1
2
3
4
5
6
7
8
9
// 环境为jdk8
public static void main(String[] args) {
String s = new String("1");
System.out.println(s == s.intern()); // boolean
String s1 = new String("2") + new String("3");
System.out.println(s1 == s1.intern()); // boolean
String s2 = "23";
System.out.println(s1 == s2); // boolean
}

    分析:jdk7及之后,输出falsetruetrue。执行第2行代码后,字符串常量池中有对象"1"s.intern()返回的是字符串常量池中对象"1"的引用,对象s是堆中字符串对象"1"的引用,不相等,故为false;执行第4行代码时,字符串常量池中有对象"2",对象"3",但没有对象"23"s1.intern()则将该字符串对象"23"的引用注册到字符串常量池中,并返回该引用,以后使用相同字面量(双引号形式)声明的字符串对象都指向该引用指向的地址 。s1也是堆中字符串对象"23"的引用,都指向同一个地址,故为true;执行第7行代码时,由于第6行执行了s1.intern(),所以s2指向堆中字符串对象"23"s1s2指向的是同一地址,故为truejdk6中,输出falsefalsefalse
PS:native修饰的方法是本地,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。

拼接字符串"+"
1
2
3
4
5
6
public static void main(String[] args) {
String a = "aa";
String b = "bb";
String x = "xx" + "yy" + a + "zz" + "nn" + b;
System.out.println(x); // xxyyaazznnbb
}

    1) String中使用 + 字符串连接符进行字符串连接时,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接。
    2) 接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。
上面代码执行过程为:
String x = new StringBuilder("xxyy").append(a).append("zz").append("nn").append(b).toString();

StringStringBuilderStringBuffer的区别

    1) String为不可变序列,StringBuilderStringBuffer为可变序列。
    2) 执行速度 StringBuilder> StringBuffer> String
    3) StringBuilder是非线程安全的,StringBuffer中的方法大都采用了synchronized 关键字进行修饰,故是线程安全的,String是线程安全的。