不可变的字符串String
String的不可变性
String本身的特性:
1.String是被final修饰的类,不能被继承;
2.String实现了Serializable和Comparable接口,表示String支持序列化和可以比较大小;
3.String底层是通过char类型的数据实现的,并且被final修饰,所以字符串的值创建之后就不可以被修改,具有不可变性
String 类没有提供修改字符串中某个字符的方法。
String greeting = "Hello";
如果你希望将greeting的内容修改为"Help!",不能直接将greeting的最后两个位置的字符修改为’p’和‘!'。
如何修改这个字符串呢?在Java中实现这项操作非常容易。可以提取想要保留的子串,再与希望替换的字符拼接:
greeting = greeting.substring(0, 3) +"p!";
上面这条语句将greeting变量的当前值修改为"Help!"。
由于不能修改Java字符串中的单个字符,所以在Java文档中将String类对象称为是不可变的(immutable)
如同数字3永远是数字3一样,字符串"Hello"永远包含字符H、e、l、l和o的代码单元序列。你不能修改这些值,不过,可以修改字符串变量greeting,让它引用另外一个字符串,这就如同可以让原本存放3的数值变量改成存放4一样。
这样做是否会降低运行效率呢?看起来好像修改一个代码单元要比从头创建一个新字符串更加简洁。答案是:也对,也不对。的确,通过拼接“Hel"和“p!"来创建一个新字符串的效率确实不高。但是,
不可变字符串却有一个优点:编译器可以让字符串共享。
为了弄清具体的工作方式,可以想象将各种字符串存放在公共的存储池中。字符串变量指向存储池中相应的位置。如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。即:重新分配内存地址进行赋值
总而言之,Java的设计者认为共享带来的高效率远远胜过于提取子串、拼接字符串所带来的低效率。
补充
在JDK8版本之后:
静态常量池在Class文件中。
JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。同时永久地被移除,以元空间代替。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。其主要用于存放一些元数据。
因此,字符串常量池存在于Java堆中。虽然与对象都在堆内,但是存储位置还是不同
//示例1
//通过字面量方式为字符串赋值时,此时的字符串存储在堆的字符串常量池中
String s1="hello";
String s2="hello";
//通过new+构造器方式实例化字符串时,字符串对象存储在堆中
String s3=new String("hello");
System.out.println(s1==s2);//true
System.out.println(s1==s3);//false
//String对象与常量进行拼接
String s4 = "ABC";
String s5 = "ABC" + "hello";
String s6 = "ABChello"
System.out.println(s5==s6);//true
System.out.println(s5==s4+"hello");//false
Java中有常量优化机制,编译器将这个"ABC" + “hello"作为常量表达式,在编译时进行优化,直接取表达式结果"ABC” + “hello”,这里没有创建新的对象,而是从JVM字符串常量池中获取之前已经存在的"ABChello"对象。因此s6,s5具有对同一个string对象的引用,两个引用相等,结果true。
深入理解StringBuffer和StringBuilder
为什么要引入StringBuilder?
在示例一中用变量和常量连接起来,此时编译器又是怎样运行的呢?
String s7 = s4+"hello";
String s8 = "ABC"+s1;
String s9 = "ABC"+s3;
System.out.println(s7==s8);//false
System.out.println(s8==s9);//false
System.out.println(s7==s9);//false
引用一:当变量与字面量或变量与变量进行拼接时,会在堆中创建一个StringBuilde对象,然后使用StringBuilder的append()方法将变量与字面量或变量与变量进行拼接,最后调用toString()方法转成String对象。所以s9、s8、s7指向的都是堆内存中String对象的地址值。
而引用一的实现方法,就是StringBuilder用小段字符串重新构件一个新字符串的过程
大多数情况下都不会修改字符串,而只是需要对字符串进行比较(有一种例外情况,将来自于文件或键盘的单个字符或较短的字符串组装成字符串。
例如,按键或来自文件中的单词。如果用字符串拼接的方式来达到这个目的,效率会比较低。每次拼接字符串时,都会构建一个新String 对象,既耗时,又浪费空间。使用StringBuilder类就可以避免这个问题的发生。而它的前身就StringBuffer。
String,StringBuffer 和 StringBuilder的异同:
相同点:底层都是通过char数组实现的
不同点:
String对象一旦创建,其值是不能修改的,如果要修改,会重新开辟内存空间来存储修改之后的对象;而StringBuffer和StringBuilder对象的值是可以被修改的;
StringBuffer几乎所有的方法都使用synchronized实现了同步,线程比较安全,在多线程系统中可以保证数据同步,但是效率比较低;
而StringBuilder 没有实现同步,线程不安全,在多线程系统中不能使用StringBuilder,但是效率比较高。
如果我们在实际开发过程中需要对字符串进行频繁的修改,不要使用String,否则会造成内存空间的浪费;当需要考虑线程安全的场景下使用 StringBuffer,如果不需要考虑线程安全,追求效率的场景下可以使用 StringBuilder。