有三点:java
1)String 在底层是用一个 private final 修饰的字符数组 value 来存储字符串的。final 修饰符保证了 value 这个引用变量是不可变的,private 修饰符则保证了 value 是类私有的,不能经过对象实例去访问和更改 value 数组里存放的字符。面试
注:有不少地方说 String 不可变是 final 起的做用,其实不严谨。由于即便我不用 final 修改 value ,但初始化完成后我能保证之后都不更改 value 这个引用变量和 value[] 数组里存放的值,它也是从没变化过的。final 只是保证了 value 这个引用变量是不能更改的,但不能保证 value[] 数组里存放的字符是不能更改的。若是把 private 改成 public 修饰,String类的对象是能够经过访问 value 去更改 value[] 数组里存放的字符的,这时 String 就再也不是不可变的了。因此不如说 private 起的做用更大一些。后面咱们会经过
代码1处
去验证。数组
2)String 类并无对外暴露能够修改 value[] 数组内容的方法,而且 String 类内部对字符串的操做和改变都是经过新建一个 String 对象去完成的,操做完返回的是新的 String 对象,并无改变原来对象的 value[] 数组。数据结构
注:String 类若是对外暴露能够更改 value[] 数组的方法,如 setter 方法,也是不能保证 String 是不可变的。后面咱们会经过
代码2处
去验证。ide
3)String 类是用 final 修饰的,保证了 String 类是不能经过子类继承去破坏或更改它的不可变性的。学习
注:若是 String 类不是用 final 修饰的,也就是 String 类是能够被子类继承的,那子类就能够改变父类原有的方法或属性。后面咱们会经过
代码3处
去验证。this
以上三个条件同时知足,才让 String 类成了不可变类,才让 String 类具备了一旦实例化就不能改变它的内容的属性。spa
public final class String implements Serializable, Comparable<String>, CharSequence {
private final char[] value; // 用 private final 修饰的字符数组存储字符串
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
public String() {
this.value = "".value;
}
public String(String var1) {
this.value = var1.value;
this.hash = var1.hash;
}
public String(char[] var1) {
this.value = Arrays.copyOf(var1, var1.length);
}
......
}
复制代码
面试问题:String 类是用什么数据结构来存储字符串的?设计
由上面 String 的源码可见,String 类是用数组的数据结构来存储字符串的。code
咱们来看看若是把 private 修饰符换成 public,看看会发生什么?
// 先来模拟一个String类,初始化的时候将 String 转成 value 数组存储
public final class WhyStringImutable {
public final char[] value; // 修饰符改为了 public
public WhyStringImutable() {
this.value = "".toCharArray();
}
public WhyStringImutable(String str){
this.value = str.toCharArray(); // 初始化时转为字符数组
}
public char[] getValue(){
return this.value;
}
}
复制代码
public class WhyStringImutableTest {
public static void main(String[] args) {
WhyStringImutable str = new WhyStringImutable("abcd");
System.out.println("原str中value数组的内容为:");
System.out.println(str.getValue()); // 打印str对象中存放的字符数组
System.out.println("----------");
str.value[1] = 'e'; // 经过对象实例访问value数组并修改其内容
System.out.println("修改后str中value数组的内容为:");
System.out.println(str.getValue()); // 打印str对象中存放的字符数组
}
}
复制代码
输出结果:
原str中value数组的内容为:
abcd
----------
修改后str中value数组的内容为:
aecd
复制代码
因而可知,private 修改成 public 后,String 是能够经过对象实例访问并修改所保存的value 数组的,并不能保证 String 的不可变性。
咱们若是对外暴露能够更改 value[] 数组的方法,如 setter 方法,看看又会发生什么?
public final class WhyStringImutable {
private final char[] value;
public WhyStringImutable() {
this.value = "".toCharArray();
}
public WhyStringImutable(String str){
this.value = str.toCharArray();
}
// 对外暴露能够修改 value 数组的方法
public void setValue(int i, char ch){
this.value[i] = ch;
}
public char[] getValue(){
return this.value;
}
}
复制代码
public class WhyStringImutableTest {
public static void main(String[] args) {
WhyStringImutable str = new WhyStringImutable("abcd");
System.out.println("原str中value数组的内容为:");
System.out.println(str.getValue()); // 打印str对象中存放的字符数组
System.out.println("----------");
str.setValue(1,'e'); // 经过set方法改变指定位置的value数组元素
System.out.println("修改后str中value数组的内容为:");
System.out.println(str.getValue()); // 打印str对象中存放的字符数组
}
}
复制代码
输出结果:
原str中value数组的内容为:
abcd
----------
修改后str中value数组的内容为:
aecd
复制代码
因而可知,若是对外暴露了能够更改 value[] 数组内容的方法,也是不能保证 String 的不可变性的。
若是 WhyStringImutable 类去掉 final 修饰,其余的保持不变,又会怎样呢?
public class WhyStringImutable {
private final char[] value;
public WhyStringImutable() {
this.value = "".toCharArray();
}
public WhyStringImutable(String str){
this.value = str.toCharArray(); // 初始化时转为字符数组
}
public char[] getValue(){
return this.value;
}
}
复制代码
写一个子类继承自WhyStringImutable 并修改原来父类的属性,实现子类本身的逻辑:
public class WhyStringImutableChild extends WhyStringImutable {
public char[] value; // 修改字符数组为 public 修饰,不要 final
public WhyStringImutableChild(String str){
this.value = str.toCharArray();
}
public WhyStringImutableChild() {
this.value = "".toCharArray();
}
@Override
public char[] getValue() {
return this.value;
}
}
复制代码
public class WhyStringImutableTest {
public static void main(String[] args) {
WhyStringImutableChild str = new WhyStringImutableChild("abcd");
System.out.println("原str中value数组的内容为:");
System.out.println(str.getValue());
System.out.println("----------");
str.value[1] = 's';
System.out.println("修改后str中value数组的内容为:");
System.out.println(str.getValue());
}
}
复制代码
运行结果:
原str中value数组的内容为:
abcd
----------
修改后str中value数组的内容为:
ascd
复制代码
因而可知,若是 String 类不是用 final 修饰的,是能够经过子类继承来修改它原来的属性的,因此也是不能保证它的不可变性的。
综上所分析,String 不可变的缘由是 JDK 设计者巧妙的设计了如上三点,保证了String 类是个不可变类,让 String 具备了不可变的属性。考验的是工程师构造数据类型,封装数据的功力,而不是简单的用 final 来修饰,背后的设计思想值得咱们理解和学习。
从上面的分析,咱们知道,String 确实是个不可变的类,但咱们就真的没办法改变 String 对象的值了吗?不是的,经过反射能够改变 String 对象的值。
可是请谨慎那么作,由于一旦经过反射改变对应的 String 对象的值,后面再建立相同内容的 String 对象时都会是反射改变后的值,这时候在后面的代码逻辑执行时就会出现让你 “摸不着头脑” 的现象,具备迷惑性,出了奇葩的问题你也很难排除到缘由。后面在 代码4处
咱们会验证这个问题。
先来看看如何经过反射改变 String 对象的内容:
public class WhyStringImutableTest {
public static void main(String[] args) {
String str = new String("123");
System.out.println("反射前 str:"+str);
try {
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] aa = (char[]) field.get(str);
aa[1] = '1';
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println("反射后 str:"+str);
}
复制代码
打印结果:
反射前 str:123
反射后 str:113 // 可见,反射后,str 的值确实改变了
复制代码
下面咱们来验证由于一旦经过反射改变对应的 String 对象的值,后面再建立相同内容的 String 对象时都会是反射改变后的值的问题:
public class WhyStringImutableTest {
public static void main(String[] args) {
String str = new String("123");
System.out.println("反射前 str:"+str);
try {
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] aa = (char[]) field.get(str);
aa[1] = '1';
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println("反射后 str:"+str);
String str2 = new String("123");
System.out.println("str2:"+str2); // 咱们来看 str2 会输出什么,会输出 113?
System.out.println("判断是不是同一对象:"+str == str2); // 判断 str 和 str2 的内存地址值是否相等
System.out.println("判断内容是否相同:"+str.equals(str2)); // 判断 str 和 str2 的内容是否相等
}
复制代码
执行结果以下:
反射前 str:123
反射后 str:113
str2:113 // 居然不是123??而是输出113,说明 str2 也是反射修改后的值。
判断是不是同一对象:false // 输出 false,说明在内存中确实建立了两个不一样的对象
判断内容是否相同:true // 输出true,说明依然判断为两个对象内容是相等的
复制代码
由上面的输出结果,咱们可知,反射后再新建相同内容的字符串对象时会是反射修改后的值,这就形成了很大迷惑性,在实际开发中要谨慎这么作。