解答 :有三点:
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处
去验证。
3)String 类是用 final 修饰的,保证了 String 类是不能通过子类继承去破坏或更改它的不可变性的。
注:如果 String 类不是用 final 修饰的,也就是 String 类是可以被子类继承的,那子类就可以改变父类原有的方法或属性。后面我们会通过 代码3处
去验证。
以上三个条件同时满足,才让 String 类成了不可变类,才让 String 类具有了一旦实例化就不能改变它的内容的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public final class String implements Serializable , Comparable <String >, CharSequence { private final char [] value; 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 类是用数组的数据结构来存储字符串的 。
代码1:把 private 修饰符换成 public 我们来看看如果把 private 修饰符换成 public,看看会发生什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public final class WhyStringImutable { public final char [] value; 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()); System.out.println("----------" ); str.value[1 ] = 'e' ; System.out.println("修改后str中value数组的内容为:" ); System.out.println(str.getValue()); } }
输出结果:
1 2 3 4 5 原str中value数组的内容为: abcd ---------- 修改后str中value数组的内容为: aecd
由此可见,private 修改为 public 后,String 是可以通过对象实例访问并修改所保存的value 数组的,并不能保证 String 的不可变性。
代码2:对外暴露可以更改 value[] 数组的方法 我们如果对外暴露可以更改 value[] 数组的方法,如 setter 方法,看看又会发生什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public final class WhyStringImutable { private final char [] value; public WhyStringImutable () { this .value = "" .toCharArray(); } public WhyStringImutable (String str) { this .value = str.toCharArray(); } 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()); System.out.println("----------" ); str.setValue(1 ,'e' ); System.out.println("修改后str中value数组的内容为:" ); System.out.println(str.getValue()); } }
输出结果:
1 2 3 4 5 原str中value数组的内容为: abcd ---------- 修改后str中value数组的内容为: aecd
由此可见,如果对外暴露了可以更改 value[] 数组内容的方法,也是不能保证 String 的不可变性的。
代码3:去掉 final 修饰 如果 WhyStringImutable 类去掉 final 修饰,其他的保持不变,又会怎样呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 并修改原来父类的属性,实现子类自己的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class WhyStringImutableChild extends WhyStringImutable { public char [] value; 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()); } }
运行结果:
1 2 3 4 5 原str中value数组的内容为: abcd ---------- 修改后str中value数组的内容为: ascd
由此可见,如果 String 类不是用 final 修饰的,是可以通过子类继承来修改它原来的属性的,所以也是不能保证它的不可变性的。
总结 综上所分析,String 不可变的原因是 JDK 设计者巧妙的设计了如上三点,保证了String 类是个不可变类,让 String 具有了不可变的属性。考验的是工程师构造数据类型,封装数据的功力,而不是简单的用 final 来修饰,背后的设计思想值得我们理解和学习。
拓展 从上面的分析,我们知道,String 确实是个不可变的类,但我们就真的没办法改变 String 对象的值了吗?不是的,通过反射可以改变 String 对象的值 。
但是请谨慎那么做,因为一旦通过反射改变对应的 String 对象的值,后面再创建相同内容的 String 对象时都会是反射改变后的值 ,这时候在后面的代码逻辑执行时就会出现让你 “摸不着头脑” 的现象,具有迷惑性,出了奇葩的问题你也很难排除到原因。后面在 代码4处
我们会验证这个问题。
先来看看如何通过反射改变 String 对象的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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); }
打印结果:
1 2 反射前 str:123 反射后 str:113 // 可见,反射后,str 的值确实改变了
代码4:通过反射改变String 对象的值造成的后果 下面我们来验证因为一旦通过反射改变对应的 String 对象的值,后面再创建相同内容的 String 对象时都会是反射改变后的值 的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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); System.out.println("判断是否是同一对象:" +str == str2); System.out.println("判断内容是否相同:" +str.equals(str2)); }
执行结果如下:
1 2 3 4 5 反射前 str:123 反射后 str:113 str2:113 // 竟然不是123??而是输出113,说明 str2 也是反射修改后的值。 判断是否是同一对象:false // 输出 false,说明在内存中确实创建了两个不同的对象 判断内容是否相同:true // 输出true,说明依然判断为两个对象内容是相等的
由上面的输出结果,我们可知,反射后再新建相同内容的字符串对象时会是反射修改后的值,这就造成了很大迷惑性,在实际开发中要谨慎这么做。
本文整理自
为什么Java中String是不可变的
仅做个人学习总结所用,遵循CC 4.0 BY-SA版权协议,如有侵权请联系删除!