一、String的基本特性
- String类声明是final的,不可以被继承。
- String实现了Serializable接口(序列号接口),Comparable接口(比较接口)
- String在JDK8中是用final char[]存储字符串数据的,JDK9后改为final byte[]存储数据的。因为char类型占两个字节,很多时候的数据只需要一个字节就能存放的,因此会浪费掉一半的空间,之后改成byte[]类型,同时为了能够存放两个字节的数据(例如汉字),会将在使用前判断字符集。
例子:
public class StringExer {
String str = "good";
public static void change1(String str) {
str = "test";
}
public static void change2(StringExer stringExer) {
stringExer.str = "test";
}
public static void main(String[] args) {
StringExer ex = new StringExer();
StringExer.change1(ex.str);
System.out.println(ex.str);//good
StringExer.change2(ex);
System.out.println(ex.str);//test
}
}
这里不管是change1(),还是change2()方法都是引用类型的传递。
change1()方法:由于是引用类型的传递,所以传递的时候内存会再复制出指向"good"字符串的句柄。这时候有两个指向"good"的句柄,在方法内中的str改变成"test",我们知道字符串是不可改变的,所以它实际上是在常量池中创建了"test"字符串,然后复制来的那份句柄指向了新的地址,也就是"test",然而外面的对象还是指向原来的字符串,也就是为什么还是打应good的原因。
change2()方法:通用ex实例的句柄,对str属性进行修改,所以str属性会发生改变
总结:change1()方法是对参数本身的修改,但是change2()却是对对象的数据的修改了
二、字符串常量池
- 字符串常量池使用一个固定大小的HashTable来实现的。
- 在JDK6及之前长度默认为1009,如果字符串经常出现指针碰撞则会导致String类操作的效率变慢(尤其是String.intern()方法),所以在JDK7时默认被修改为60013,而在JDK8及后最短长度不得低于1009。
- 可以通过"-XX:StringTableSize"参数来设置字符串常量池的大小。
- 通过字面量定义的字符串存放在字符串常量池,通过new出来的字符串存放在堆空间。
- 字符串常量池位置变化的原因:
- 永久代的空间较小,容易出现OOM异常。
- 永久代垃圾回收频率低,但字符串大多数生命都是短暂的。
三、字符串的拼接操作
- 使用字面量创建字符串是在字符串常量池中,使用new关键字创建的对象是在先创建一个String对象然后在字符串常量池中寻找是否有该字符串常量,若没有则创建该对象,最后让这个String对象字符串常量。
- 两个字面量字符串拼接会在编译期进行优化。
- 含有变量的字符串拼接:
- 如果在JDK 5之后是先创建StringBuilder对象,再通过append()方法进行拼接,最后通过toString()方法返回新的字符串对象(跟new String()一致),所以最后返回的对象是创建在堆中的。
- 在JDK 5之前使用StringBuffer,其余一样。
- 补充:StringBuilder没有syn关键字,StringBuffer内的方法有syn关键字,因此StringBuilder是线程不安全的,StringBuffer是线程安全的,速度上StringBuilder>StringBuffer
- 使用intern()方法:若该字符串不存在常量池中,则在常量池中创建该字符串。最后返回常量池中的字符串。
- 使用final修饰的对象进行拼接的时候会将其认为字面量
@Test
public void StringTest() {
String s1 = "a";
String s2 = "ab";
String s3 = "a" + "b";
String s4 = s1 + "b";
String s5 = new String("ab");
String s6 = s5.intern();
final String s7 = "c";
String s8 = "cd";
String s9 = s7 + "d";
//在字节码文件中的s3被优化成s3="ab"
System.out.println(s2 == s3);//true
//因为s1是变量,所以最后是通过toString方法返回,对象是创建在堆中的
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//false
//s6是通过intern从字符串常量池中获取的
System.out.println(s3 == s6);//true
System.out.println(s8 == s9);//true
}
String、StringBuffer、StringBuilder的拼接操作
@Test
public void test6() {
long start = System.currentTimeMillis();
// method1(100000);//4014
// method2(100000);//5
method3(100000);//9
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
}
public void method1(int highLevel) {
String src = "";
for (int i = 0; i < highLevel; i++) {
src = src + "a";
}
}
public void method2(int highLevel) {
StringBuilder src = new StringBuilder();
for (int i = 0; i < highLevel; i++) {
src.append("a");
}
}
public void method3(int highLevel) {
StringBuffer src = new StringBuffer();
for (int i = 0; i < highLevel; i++) {
src.append("a");
}
}
String的拼接:创建一个StringBuilder,然后再根据append进行追加,再创建String返回。
StringBuilder的拼接:直接进行append拼接,没有syn关键字。
StringBuffer的拼接:直接进行append拼接,有syn关键字。
所以速度上StringBuilder>StringBuffer>String
如果我们已经能预知到字符串的长度,可以在创建的时候就给它更大的空间,省去扩容的花费的时间。
StringBuilder默认长度是16,扩容是2倍。
可以通过用参数是整形的构造器来让它初始化指定大小的空间。
public StringBuilder(int capacity) {
super(capacity);
}
四、String相关面试题
- 在字符串常量池中没有以下创建的字符串,String s = new String(“ab”)和String s = new String(“a”) + new String(“b”)分别创建了几个对象:
答:前者创建了两个对象,在堆中创建对象,然后在池中创建"ab",再让堆中的对象指向池中的"ab"字符串。后者先是先创建一个StringBuilder对象①,再创建String对象②,再在池中创建字符串"a"③,让②指向③,再创建第二个String对象④与字符串"b"⑤,最后通过StringBuilder中的append()操作后,返回使用toString()返回。在这个toString中实际上是返回new String(value,0,count)⑥,因此是创建了6个对象。
注意:StringBuilder的toString()是通过传递value(byte或者char数组)创建的,所以并不会再去字符串常量池中添加。
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
- 关于String中的intern的问题:
分析以下代码输出的内容分别是什么:
public class StringInternTest {
public static void main(String[] args) {
String s1 = new String("a");
s1.intern();
String s2 = "a";
System.out.println(s1 == s2);//false
String s3 = new String("a") + new String("b");
s3.intern();
String s4 = "ab";
System.out.println(s3 == s4);//在jdk6及之前为false,jdk7及之后为true
String s5 = new String("cd");
s5 = s5.intern();
String s6 = "cd";
System.out.println(s5 == s6);//true,因为intern()方法返回的是常量池的对象
String s7 = new String("c") + new String("d");
s7.intern();
System.out.println(s6 == s7);//false,因为在进行创建s5的同时将"cd“加入常量池,所以此时情况跟s1、s2一致
}
}
因为画的有点乱所以就先分析下s1、s2以及s3、s4的内存结构
总结:
- jdk1.6中,将这个字符串对象尝试放入串池
- 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
- 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
- jdk1.7起,将这个字符串对象尝试放入串池。
- 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
- 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址