深入了解String对象
1. String类解析
字符串广泛应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。
1.1 什么是不可变特性?
在Java中,不可变特性可分为两种情况:
- 对于基本类型,初始化后的值不能改变
- 对于引用类型,对象初始化后不能改变其引用的地址,并且对象所有的状态及属性在其生命周期内不会发生任何变化。
1.2 为什么说String是不可变对象?
我们来看下String类的源码,在JDK8中,String类内部是用char数组来存储数据的,并且value数组被声明为final,因此value 数组初始化之后就不能再引用其它数组(char数组是引用类型)。再加上String 内部没有改变 value 数组的方法,所以可以保证 String 不可变(暴力反射可使String值改变,下文会做讲解)。
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 0
}
注: 在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。
1.3 String不可变的好处
- 线程安全,有利于并发编程。
- String作为网络连接的参数,它的不可变性提供了安全性。
- 缓存hashCode。HashMap 或者 HashSet用String做key时,由于String的不可变保证了hashCode的值不改变。这也就意味着,不用每次使用的时候都要计算其hashCode,这样更高效。
- 字符串常量池成立的基础。字符串常量池是方法区的一个特殊的储存区。当定义一个变量等于字符串字面量的时候,如果此字符串字面量在常量池中早已存在,会返回一个已经存在字符串的引用,而不是新建一个对象。如果String可变,那么用某个引用一旦改变了字符串的值将会导致其他引用指向错误的值。
1.4 String Pool(字符串常量池)
字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串使用的非常多。
JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。
每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。
例如:
String s1 = "字面量a";
String s2 = "字面量a";
System.out.println(s1 == s2); // true
那如果是以 new String ("字面量a")创建的字符串,是否会从字符串常量池取已存在的字符串呢? 我们来看下例子:
String s1 = "字面量a";
String s2 = new String("字面量a");
System.out.println(s1 == s2); // false
显然,以new String ("字面量a")创建的字符串是不从字符串常量池中获取对象的,那反过来想,以new String ("字面量a")创建的字符串会不会存到字符串常量池中呢?
public static void main(String[] args) {
String s2 = new String("字面量a");
}
我们用 javap -verbose 进行反编译看下:
Constant pool:
//...
#2 = Class #16 // java/lang/String
#3 = String #17 // 字面量a
//...
#16 = Utf8 java/lang/String
#17 = Utf8 字面量a
//...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String 字面量a
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: return
//...
}
在 Constant Pool 中,#17 存储这字符串字面量 "字面量a",#3 是 String Pool 的字符串对象,它指向 #17 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。
通俗点来讲就是,使用new String ("字面量a")这种方式一共会创建两个字符串对象(前提是字符串常量池中还没有 "字面量a" 字符串)。
- 在编译期间,如果发现有字符串字面量不在字符串常量池中,就会在字符串常量池中创建一个字符串对象,指向这个 "字面量a" 字符串字面量;
- 在程序运行期间,使用 new 的方式在堆中创建一个字符串对象。
注:在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
1.5 String的Intern方法
intern方法的定义:intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串的引用;如果不存在就会将当前字符串放入常量池中,之后再返回这个新字符串的引用。
看下面的代码:
String s1 = new String("字面量a");
String s2 = new String("字面量a");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s3 == s4); // true
s3s4为true的原因是,s1和s2调用intern方法去字符串常量池查找“字面量a”这个字符串时,不管字符串常量池存不存在“字面量a”,只要s1和s2的值相同,那么intern方法最终返回的引用就是一样的,所以s3s4为true。
在实际应用场景中,如果有大量相同字符串重复使用时,可以用intern方法牺牲一些执行时间,来节省大量的内存空间。
1.6 String、StringBuilder和StringBuffer
前面提到过,由于String是不可变的,在对String对象进行拼接、裁剪时,都会创建一个新的String对象。如果拼接和裁剪的次数过多,将会占用大量的内存空间。
和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。
我们通过下面的代码看一下他们的效率差别:
public static void main(String[] args) {
String s="";
StringBuilder s2=new StringBuilder();
StringBuffer s3=new StringBuffer();
long start = System.currentTimeMillis();
for(int i=1;i<=100000;i++){
s+=i;
//s2.append(i);
//s3.append(i);
}
long end = System.currentTimeMillis();
System.out.println("耗时"+(end-start)+"毫秒");
//String耗时18458毫秒 StringBuilder耗时6毫秒 StringBuffer耗时7毫秒
}
从上面代码可以看出,在有大量的字符拼接和裁剪时,String和StringBuilder、StringBuffer的效率天差地别。而
StringBuilder和StringBuffer的区别在于,StringBuilder的效率比StringBuffer高一些。StringBuilder是线程不安全的,StringBuffer是线程安全的。
对这三个类做个总结:
-
String、StringBuffer是线程安全的,StringBuilder 不是线程安全的
-
操作少量数据时,使用String
-
单线程有大量字符串拼接和裁剪操作时,使用StringBuilder
-
多线程有大量字符串拼接和裁剪操作时,使用StringBuffer
1.7 String对象的值真的不能改变吗?
我们前面说,String对象是不可变的,但是String对象的值真的不能改变吗?
其实我们可以通过反射来改变String的值,例子如下:
public static void main(String[] args)
throws NoSuchFieldException, IllegalAccessException {
String str = "123456";
//输出修改前的String
System.out.println("str = " + str);
System.out.println("hashCode = " + str.hashCode());
Field valueField = String.class.getDeclaredField("value");
//暴力破解私有属性
valueField.setAccessible(true);
//获取String类存储数据的char数组(JDK8)
char[] valueCharArr = (char[]) valueField.get(str);
valueCharArr[0] = '改';
//输出修改后的String
System.out.println("str = " + str);
System.out.println("hashCode = " + str.hashCode());
}
上述代码的输出为:
str = 123456
hashCode = 1450575459
str = 改23456
hashCode = 1450575459
我们发现,String的值确实被我们通过反射修改了。既然String可以修改,那么为什么说String是不可变对象呢?其实很好理解,在日常生活中,我们经常会看到一些此处禁止停车,否则后果自负的标语,这说明我们一定不能停车在这个地方吗?实际上我们还是可以停在这个地方,只不过需要承担一定的后果。String对象也一样,正常情况下,它是不可变的,你通过反射把String类的值修改了,说明你对修改String值会导致什么样的结果十分清楚了。
比较有意思的一个地方是,如果把str的定义加上final声明,str字符串反射修改后与修改前输出是一样的。
final String str = "123456";
代码输出为:
str = 123456
hashCode = 1450575459
str = 123456
hashCode = 1450575459
为什么会导致这样的结果呢?
其实这是JDK编译时对String对象的优化,如果对String对象做了final声明,那么编译器就会把声明为final的String对象变成对应的字符串字面量。
即
//输出修改后的String
System.out.println("str = " + str);
被编译器优化成了
//输出修改后的String
System.out.println("str = " + "123456");
实际上str的值已经被我们修改了,我们可以把代码改成下面这样,看下输出结果:
//输出修改后的String
System.out.println("str = " + str.toString());
修改后程序输出如下:
str = 123456
hashCode = 1450575459
str = 改23456
hashCode = 1450575459
可见,str字符串已经被我们修改了,只是输出打印时,被编译器优化成字符串字面量了,自然也就无法在运行时打印修改后的字符串了。
浙公网安备 33010602011771号