一、String值为什么不能被修改
1.1、首先我们来看个例子:
public class testString {
public static void main(String[] args) {
String str = "abc";
str = "123";
System.out.println(str);
}
}
看到上面的例子,你们会认为str的值改变了啊,从“abc”变为“123”了啊。表面上看确实是这个样子的,但让我们来看下在内存中是如何存储的:
可以看到实际str改变的是引用,在堆中又重新建立一个对象“123”,然后str重新指向它。
String对象内容的改变实际上是通过内存地址“断开-连接”变化来完成的,而原字符串中的内容并没有任何的改变。
1.2、那为什么String类不可变呢?我们来看下String类的不可变原理。
要理解String类的不可变性,首先看一下String类中都有哪些成员变量。在JDK1.8中,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
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
先来看下final关键字的作用:
如果要创建一个不可变对象,关键一步就是要将所有的成员变量声明为final类型。所以下面简单回顾一下final关键字的作用:
- final修饰类,表示该类不能被继承,俗称断子绝孙类,该类的所有方法自动地成为final方法
- final修饰方法,表示子类不可重写该方法
- final修饰基本数据类型变量,表示该变量为常量,值不能再修改
- final修饰引用类型变量,表示该引用在构造对象之后不能指向其他的对象,但该引用指向的对象的值可以改变
首先可以看到,String类使用了final修饰符,表明String类是不可继承的。
然后,我们主要关注String类的成员变量value,value是char[]类型,因此String对象实际上是用这个字符数组进行封装的。再看value的修饰符,使用了private,也没有提供setter方法,所以在String类的外部不能修改value,同时value也使用了final进行修饰,那么在String类的内部也不能修改value,但是上面final修饰引用类型变量的内容提到,这只能保证value不能指向其他的对象,但value指向的对象的值是可以改变的。
1.3、String类真的不可变嘛?
上面提到,value虽然使用了final进行修饰,但是只能保证vaue不能指向其他的对象,但value指向的对象的值是可以改变的,也就是说,可以修改value指向的字符数组里面的元素。因为value是private类型的,所以只能使用反射来获取String对象的value属性,再去修改value指向的字符数组里面的元素。通过下面的代码进行验证:
String s = "Hello World";
System.out.println("s = " + s);
//获取String类中的value属性
Field valueField = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueField.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueField.get(s);
//改变value所引用的数组中的第6个字符
value[5] = '_';
System.out.println("s = " + s);
打印结果:
s = Hello World
s = Hello_World
在上述代码中,s始终指向同一个String对象,但是在反射操作之后,这个String对象的内容发生了变化。也就是说,通过反射是可以修改String这种不可变对象的。
1.4、那为什么要这样设计呢?
在Java中,将String设计成不可变的是综合考虑到内存、同步、数据结构及安全等各种因素的结果,下文将为各种因素做一个小结。
(1)运行时常量池的需要
String s = "abc";
执行上述代码时,JVM首先在运行时常量池中查看是否存在String对象“abc”,如果已存在该对象,则不用创建新的String对象“abc”,而是将引用s直接指向运行时常量池中已存在的String对象“abc”;如果不存在该对象,则先在运行时常量池中创建一个新的String对象“abc”,然后将引用s指向运行时常量池中创建的新String对象。
还有一个原因就是可以节省内存空间:
String s1 = "abc";
String s2 = "abc";
执行上述代码时,在运行时常量池中只会创建一个String对象"abc",这样就节省了内存空间。如下图:
(2)同步
因为String对象是不可变的,所以是多线程安全的,同一个String实例可以被多个线程共享。这样就不用因为线程安全问题而使用同步。
(3)允许String对象缓存hashcode
查看上文JDK1.8中String类源码,可以发现其中有一个字段hash,String类的不可变性保证了hashcode的唯一性,所以可以用hash字段对String对象的hashcode进行缓存,就不需要每次重新计算hashcode。所以Java中String对象经常被用来作为HashMap等容器的键。
(4)安全性
如果String对象是可变的,那么会引起很严重的安全问题。比如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为String对象是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变String引用指向的对象的值,造成安全漏洞。
二、StringBuff和StringBuilder
2.1、String
简要的说, String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象, 所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且大量浪费有限的内存空间。
而如果是使用 StringBuffer 类则结果就不一样了, 每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象 ,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。而在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢, 而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的 :
String S1 = “This is only a” + “ simple” + “ test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);
你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本一点都不占优势。其实这是 JVM 的一个把戏,在 JVM 眼里,这个
String S1 = “This is only a” + “ simple” + “test”; 其实就是:
String S1 = “This is only a simple test”; 所以当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如:
String S2 = “This is only a”;
String S3 = “ simple”;
String S4 = “ test”;
String S1 = S2 +S3 + S4;
这时候 JVM 会规规矩矩的按照原来的方式去做
在大部分情况下 StringBuffer > String
2.2、StringBuff
StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。 每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量。 可将字符串缓冲区安全地用于多个线程。可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
StringBuffer 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。
注:线程安全是用了Synchronized关键字来保证的。
2.3、StringBuilder
StringBuilder一个可变的字符序列是5.0新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被 单个线程 使用的时候(这种情况很普遍),StringBuilder 线程不安全 。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。
一般情况下,速度从快到慢:StringBuilder>StringBuffer>String,这种比较是相对的,不是绝对的。
三、总结
- 如果要操作少量的数据用 = String
- 单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
- 多线程操作字符串缓冲区 下操作大量数据 = StringBuffer
最后引用我很佩服的一个人经常说的话:你知道的越多,你不知道的越多!
文章参考:
https://blog.csdn.net/eydwyz/article/details/88861417
https://blog.csdn.net/guyuealian/article/details/50935168
https://blog.csdn.net/guyuealian/article/details/47059079