⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/o9109003234/article/details/109523691 「田维常」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

String不可变吗?

图片

public class App {
public static void main(String[] args) {
String a = "111";
a = "222";
System.out.println(a);
}
}

有的人会认为上面这段代码应该输出:111

这样才和上面的不变性吻合。

哈哈哈,但是并不是这样滴。

222

这不对呀,不是不变吗?怎么变了呢?

其实在JVM的运行中,会单独给一块地分给String。

上面的:

Stirng a="111"

我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:

使用字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串。

这里先去JVM给常量池里找,找到了就不用创建对象了,直接把对象的引用地址赋给a。找不到会重新创建一个对象,然后把对象的引用地址赋给a。同理a="222";也是先找,找不到就重新创建一个对象,然后把对象的引用地址赋给a。

大家有没有发现我上面的描述中“引用地址”。比如说 Object obj = new Object();很多人喜欢成obj为对象,其实obj不是对象,他只是一个变量,然后这个变量里保存一个Object对象的引用地址罢了。

引用类型声明的变量是指该变量在内存中实际存储的是一个引用地址,实体在堆中。

所以网上很多文章老喜欢这么说

User user = new User()

创建了一个user对象,老喜欢把user称之为对象。这里不接受反驳。

所以上面String a = “111”;表达的是变量a里保存了“111”这个对象的引用地址。变量是可以变的,不能变的是“111”。

String 为什么是不可变的?

图片

简单的来说,String 类中使用 final 关键字字符数组保存字符串。代码如下:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
}

从上面的这段源码中可以看出三点:

String 类是final修饰

String存储内容使用的是char数组

char数组是final修饰

这里就得复习一下,final有啥用?

当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

当final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)

当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化。如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。另外final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,(1.在申明的时候给其赋值,否则必须在其类的所有构造方法中都要为其赋值)

比如:

/**
* Description: final修饰变量
* @author : 田维常
*/
public class FinalDemo {
private final String name;

public FinalDemo(String name) {
this.name = name;
}

public FinalDemo() {
}
}

这是会会报错

图片

关于final就简单说到这里

下面来看一个使用String的 案例

/**
* Description:
*
* @author : 田维常
* @date : 2020/11/3
*/
public class StringDemo {
public static void main(String[] args) {
String name = "老田";
name.concat("!");
System.out.println(name);
System.out.println(name.concat("!"));
}
}

输出

图片

顺道溜达溜达 String中几个常用方法源码

public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
//啥都没有,就直接把当前字符串给你
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
//看到了吗?返回的居然是新的String对象
return new String(buf, true);
}
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
public String replace(char oldChar, char newChar) {
//如果两个是一样的,那就必要替换了,所以返回this
if (oldChar != newChar) {
int len = value.length;
int i = -1;
//把当前的char数组复制给val,然后下面基于val来操作
char[] val = value;

while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
//创建一个新的char数组
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
//创建一个新的String对象
return new String(buf, true);
}
}
return this;
}

public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//正常返回的都是新new出来的String对象
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}

public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */

while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
//如果是该字符串中包含了空格,调用substring方法,否则就是啥都没干原本返回
//就是如果字符串里有空格,那么还是新生一个String对象返回
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

无论是concat、replace、substring还是trim方法的操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。

得出两个结论:

String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何变化性的操作都会生成新的对象。

String对象每次有变化性操作的时候,都会从新new一个String对象(这里指的是有变化的情况)。

回到前面的例子

//String a = "111";相当于
char data [] ={'1','1','1'};
Stirng a = new String(data);
//a = "222";
char data [] ={'2','2','2'};
a = new String(data);

图片

这会变量a里保存的是"222"对应String对象的引用。

继续看下面的代码

public class App {
public static void main(String[] args) {
String a = "111";
String a1 = "111";

String b = new String("111");

//对象地址是同一个
System.out.println(a==a1);
//对象内容是一样的
System.out.println(a.equals(a1));
//对象地址不一样
System.out.println(a==b);
//对象内容是一样的
System.out.println(a.equals(b));
}
}

输出

true
true
false
true

第一个输出true,说明a和a1两个变量保存的引用地址是同一个。

第二个也输出true,说明a和a1引用地址中内容是一样的。

a和a1放在栈上,存放着对象的引用地址。

new的对象是在堆中。

常量其实是要看jdk版本的。

图片

图片

所以String a = "111"; 在JVM申请内存存放"111"对应的对象,并将对象保存起来。当String a1="1111";的时候,会先去JVM的那块地里寻找是否存在"111",刚好前面保存过,所以找到,然后直接把对象的引用地址给了a1。所以此时的a和a1都保存着同一个引用地址。

接触java后都知道可以new一个对象。所以 String b = new String("111");就是创建一个对象然后把对象引用地址赋给变量b。但是这里有个特殊点,那就是(“111”),这里会先去JVM里的那块地里找找,找到了直接存放引用地址。找不到创建一个对象然后把引用地址给String的有参构造方法里。

所以第三个中输出false,因为a和b所保存的对象引用是不一样的。

最后一个输出true。那是因为两个变量所保存的引用地址中的内容都是“111”.

答案:

如果常量池中存在,则只需创建一个对象,否则需要创建两个对象。

文章目录
  1. 1. String不可变吗?
  2. 2. 这里就得复习一下,final有啥用?