Java中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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拥抱白熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值