String类:java.lang.String
Java字符串就是Unicode字符序列,例如字符串"Java"就是4个Unicode字符J,a,v,a组成的。任何一个字符对应两个字节的定长编码,即任何一个字符(无论中文还是英文)都算一个字符长度,占用两个字节。
Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义的类String,每个用双引号括起来的字符串都是String类的一个实例。
String类使用了final修饰,不能被继承。
String的不可变性
String是常量,其对象一旦构造就不能再被改变。换句话说,String对象是不可变的,每一个看起来会修改String值的方法,实际上都是创造了一个全新的String对象,以包含修改后的字符串内容。而最初的String对象则丝毫未动。
String对象具有只读特性,指向它的任何引用都不可能改变它的值,因此,也不会对其他的引用有什么影响。但是字符串引用可以重新赋值。
String不可变性的优点:字符串实例的共享,避免创建的开销。
字符串常量池
常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。Java为了提高性能,静态字符串(字面量/常量/常量连接的结果)在常量池中创建,并尽量使用同一个对象,重用静态字符串。对于重复出现的字符串直接量,JVM会首先在常量池中查找,如果常量池中存在即返回该对象。
public static void main(String[] args){ String str1 = "abc"; //不会创建新的String对象,而是使用常量池中已有的"abc", String str2 = "abc"; System.out.println(str1 == str2); //true //使用new关键字会创建新的String对象 String str3 = new String("abc"); System.out.println(str1 == str3); //false }
String Pool是JVM实例全局共享的,全局只有一个。字符串常量池涉及到一个设计模式,叫“享元模式”,顾名思义->共享元素模式。也就是说:一个系统中如果有多处用到了相同的一个元素,那么我们应该只存储一份此元素,而让所有地方都引用这一个元素。
字符串池的实现有一个前提条件:String对象是不可变的。因为这样可以保证多个引用可以同事指向字符串池中的同一个对象。如果字符串是可变的,那么一个引用操作改变了对象的值,对其他引用会有影响,这样显然是不合理的。
字符串池的优缺点:
字符串池的优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;
字符串池的缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低。
GC回收:字符串池中维护了共享的字符串对象,当常量池中的引用没有被任何变量引用时,就会被GC回收。
String的内存使用
背景知识:
栈(Stack) :存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new出来的对象)或者常量池中(字符串常量对象存放在常量池中)。例如,当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量分配的内存空间,该内存空间可以立即被另作它用。
堆(Heap):存放所有new出来的对象。堆内存用来存放由new创建的对象和数组,在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或者对象之后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或者对象,引用变量就相当于是为 数组或者对象起的一个名称。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。(即使程序运行到使用new产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。)
String Pool:String对象
创建一个String对象,主要就有以下两种方式:
String str1 = new String("abc");
Stirng str2 = "abc";
对于第一种,会创建1或2个对象。JVM首先在字符串池中查找有没有"abc"这个字符串对象,如果有,则不在池中再去创建"abc"这个对象了,直接在堆中创建一个"abc"字符串对象,然后将堆中的这个"abc"对象的地址返回赋给引用str1 ,这样,str1 就指向了堆中创建的这个"abc"字符串对象;如果没有,则首先在字符串池中创建一个"abc"字符串对象,然后再在堆中创建一个"abc"字符串对象,然后将堆中这个"abc"字符串对象的地址返回赋给str1 引用。
注意:用new在JAVA Heap中创建对象时,JVM是不会主动把该对象放到Strings pool里面的,除非程序调用 String的intern方法。(详见下例)
对于第二种,JVM首先会去字符串池中查找是否存在"abc"这个对象,如果不存在,则在字符串池中创建"abc"这个对象,然后将池中"abc"这个对象的引用地址返回给"abc"对象的引用str2 ,这样str2 会指向池中"abc"这个字符串对象;如果存在,则不创建任何对象,直接将池中"abc"这个对象的地址返回。
看一个详细的例子:
public static void main(String[] args) { String s = new String("1"); s.intern(); String s2 = "1"; System.out.println(s == s2); String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4); }
先看 s 和 s2 对象。
String s = new String("1"):生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。
s.intern():s 对象去常量池中寻找后发现“1”已经在常量池里了。
String s2 = "1":这句代码是生成一个 s2的引用指向常量池中的“1”对象。 因此 s 和 s2 的引用地址明显不同。
再看 s3和s4字符串。
String s3 = new String("1") + new String("1"):这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的new String("1")我们不去讨论它们。此时s3引用对象内容是"11",但此时常量池中是没有 “11”对象的。
s3.intern():这一句代码,是将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,因此常规做法是在常量池中生成一个 "11" 的对象,关键点是jdk7中常量池中可以不再存储一份对象,可以直接存储堆中的引用。这份引用指向 s3 引用的对象,也就是说常量池中的引用地址和s3是相同的。
String s4 = "11":这句代码中"11"是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较 s3 == s4 是 true。
String构造方法
1. public String()
无参构造方法,用来创建空字符串的String对象。
String str1 = new String();
2. public String(String value)
用已知的字符串value创建一个String对象。String str2 = new String("aaa"); String str3 = new String(str2);
3. public String(char[] value)
用字符数组value创建一个String对象。char[] value = {"a","b","c","d"}; String str4 = new String(value); //相当于String str4 = new String("abcd");
4. public String(char chars[], int startIndex, int numChars)
用字符数组chars的从位置在startIndex开始的numChars个字符创建一个String对象。
char[] value = {"a","b","c","d"}; String str5 = new String(value, 1, 2); //相当于String str5 = new String("bc");
5. public String(byte[] values)
用比特数组values创建一个String对象。
byte[] str6 = new byte[]{65,66}; String str6 = new String(str6); //相当于String str6 = new String("AB");
String拼接
String str1 = "aaa"; String str2 = "bbb"; String str3 = "aaabbb"; String str4 = "aaa" + "bbb"; //不会产生新的字符串对象 str4 = str1 + "bbb"; //会产生新的字符串对象 str4 = str1 + str2; //会产生新的字符串对象
字符串字面量拼接的"+"运算:
正常情况下,执行声明str4代码会生成2个对象,即 "aaa" 、"aaabbb",其中 "aaa"是中间的临时变量,最后的"aaabbb"才赋值给了str4。因此在使用字符串拼接的时候,拼接的数量越多,性能越低。
但是java编译器在编译的时候做了优化,在编译时新建一个对象StringBuilder来拼接,这样就避免了产生很多临时对象,从而提升了性能。也就是说编译器编译时,直接把"aaa"和"bbb"这2个字面量进行"+"操作得到一个"aaabbb" 常量,并且直接将这个常量放入字符串池中,这样做实际上是一种优化,将2个字面量合成一个,避免了创建多余的字符串对象。
字符串引用的"+"运算:
是在Java运行期间执行的,即str1、str12 在程序执行期间才会进行计算,它会在堆内存中重新创建一个拼接后的字符串对象。
总结来说就是:字面量"+"拼接是在编译期间进行的,拼接后的字符串存放在字符串池中;而字符串引用的"+"拼接运算是在运行时进行的,新创建的字符串存放在堆中。
final String str1 = "aaa"; final String str2 = "bbb"; String str3 = "aaabbb"; String str4 = str1 + str2;
因为str1与str2都定义成了常量,所以编译时就能确定,编译时就会将常量替换,等同于str4 = "aaa"+"bbb",因此不产生新对象
final static String str1; final static String str2; static { str1 ="aaa"; str2 ="bbb"; } public static void main(String[] args){ String str3 = str1 + str2; String str4 ="aaabbb"; System.out.println(str3==str4); //输出为false }
static静态代码块在类加载的时候才会执行,才能确定,因此编译时期此时str1与str2相当于变量,而不是常量。