作者:维常
blog.csdn.net/o9109003234/article/details/109523691
String 不可变吗?
public class App {
public static void main(String[] args) {
String a = "111";
a = "222";
System.out.println(a);
}
}
复制代码
有的人会认为上面这段代码应该输出:111
这样才和上面的不变性吻合。
哈哈哈,但是并不是这样滴。
222
这不对呀,不是不变吗?怎么变了呢?
其实在 JVM 的运行中,会单独给一块地分给 String。
上面的:
我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM 为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:
使用字符串常量池。每当我们创建字符串常量时,JVM 会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于 String 字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串。
这里先去 JVM 给常量池里找,找到了就不用创建对象了,直接把对象的引用地址赋给 a。找不到会重新创建一个对象,然后把对象的引用地址赋给 a。同理 a="222"; 也是先找,找不到就重新创建一个对象,然后把对象的引用地址赋给 a。
搜索公纵号:MarkerHub,关注回复 [ vue ] 获取前后端入门教程!
大家有没有发现我上面的描述中 “引用地址”。比如说 Object obj = new Object(); 很多人喜欢成 obj 为对象,其实 obj 不是对象,他只是一个变量,然后这个变量里保存一个 Object 对象的引用地址罢了。
引用类型声明的变量是指该变量在内存中实际存储的是一个引用地址,实体在堆中。
所以网上很多文章老喜欢这么说
创建了一个 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 : 田维常
* 欢迎关注:java后端技术全栈
*/
public class FinalDemo {
private final String name;
public FinalDemo(String name) {
this.name = name;
}
public FinalDemo() {
}
}
复制代码
这是会会报错
关于 final 就简单说到这里
下面来看一个使用 String 的 案例
/**
* Description:
*
* @author : 田维常
* @date : 2020/11/3
* 欢迎关注公众号:java后端技术全栈
*/
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,说明 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”.
答案:
如果常量池中存在,则只需创建一个对象,否则需要创建两个对象。
评论